Files
OpenNest/OpenNest.Core/CNC/Posting/PostContours.cs
T
ajandClaude Opus 5.5 fa6f664296 fix(posts): pick Cincinnati CL kerf side from the shared contour winding
The CL post wrote the configured default side (G41) on every contour,
so a contour wound against OpenNest's convention was compensated into
the part. It now takes each feature's side from Core's shared
PostContours winding rule, as the CI Fiber post does: the perimeter is
picked across the part's cut features and hole sub-program contours
are always holes. Correctly wound drawings post unchanged.

Remove the "Default kerf side" post setting; saved configs that still
carry it load as before. Expose PostContours.Flatten and Classify so a
post can classify one part's features itself.

Add CL tests for a square with a hole: the exact inline program, cuts
that match the shared contours on the sheet, G41/G42 keeping the part
out of the kerf inline and through part sub-programs, and identical
output from absolute and incremental part and hole programs. The
replay now records the compensation in force on each cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:52:10 -05:00

174 lines
6.1 KiB
C#

using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.CNC.Posting;
/// <summary>
/// Reduces a part program to the contours a post writes: hole calls and incremental programs
/// flattened to one absolute frame, each contour split into pierce, lead-in and cuts, the
/// perimeter picked out and the kerf side chosen. Posts format these; they do not redo the
/// geometry. Caller programs are never modified.
/// </summary>
public static class PostContours
{
/// <summary>
/// The part's contours in sheet coordinates. A display perimeter's tail is never cut and an
/// unplanned one is refused (<see cref="DisplayPerimeterTail.ForPosting"/>). Cut-off contours
/// are open lines on the beam centreline: no perimeter and no kerf.
/// </summary>
public static List<PostContour> Extract(Part part)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
var program = DisplayPerimeterTail.ForPosting(part.Program, DisplayPerimeterTail.PartName(part));
var contours = Extract(program, part.Location);
if (part.BaseDrawing?.IsCutOff == true)
{
foreach (var contour in contours)
{
contour.IsPerimeter = false;
contour.Kerf = KerfType.None;
}
}
return contours;
}
/// <summary>
/// The program's contours with <paramref name="origin"/> added to every point: the part
/// location for sheet coordinates, <see cref="Vector.Zero"/> for the part's own frame.
/// </summary>
public static List<PostContour> Extract(Program program, Vector origin)
{
var contours = Flatten(program, origin);
Classify(contours);
return contours;
}
/// <summary>
/// The program's contours in the <paramref name="origin"/> frame, with no perimeter or kerf
/// side chosen yet. A post that splits one part across several calls gathers them here and
/// classifies the whole part with <see cref="Classify"/>.
/// </summary>
public static List<PostContour> Flatten(Program program, Vector origin)
{
var contours = new List<PostContour>();
FlattenInto(program, origin, contours);
return contours;
}
/// <summary>Picks one part's perimeter (marks excluded) and each contour's kerf side.</summary>
public static void Classify(IReadOnlyList<PostContour> contours)
{
var perimeter = ContourWinding.PerimeterIndex(contours);
for (var i = 0; i < contours.Count; i++)
{
var contour = contours[i];
contour.IsPerimeter = i == perimeter;
contour.Kerf = contour.IsMark
? KerfType.None
: ContourWinding.CompensationSide(contour, contour.IsPerimeter);
}
}
/// <summary>
/// Walks one program whose coordinates are relative to <paramref name="origin"/>; hole calls
/// recurse with their offset added. A contour starts at each rapid. Moves before the first
/// rapid start at the frame origin, where an absolute program begins. A cut after a run of
/// suppressed moves starts a new contour pierced where the run ends, so the gap is crossed
/// with the beam off rather than cut as a chord.
/// </summary>
private static void FlattenInto(Program program, Vector origin, List<PostContour> contours)
{
if (program == null)
return;
var codes = program.Codes;
if (program.Mode == Mode.Incremental)
{
var clone = (Program)program.Clone();
clone.Mode = Mode.Absolute;
codes = clone.Codes;
}
PostContour current = null;
Vector? resumeAt = null;
void Flush()
{
if (current != null && (current.LeadIn != null || current.Cuts.Count > 0))
contours.Add(current);
current = null;
}
foreach (var code in codes)
{
if (code is Motion { Suppressed: true } suppressed)
{
resumeAt = suppressed.EndPoint;
continue;
}
if (HeadHeightLock.IsLock(code) || HeadHeightLock.IsUnlock(code))
{
current ??= new PostContour { Pierce = origin };
current.HeadLocks.Add((current.Cuts.Count, HeadHeightLock.IsLock(code)));
continue;
}
switch (code)
{
case RapidMove rapid:
Flush();
resumeAt = null;
current = new PostContour { Pierce = rapid.EndPoint + origin };
break;
case SubProgramCall call:
// A hole call is its own contour.
Flush();
resumeAt = null;
FlattenInto(call.Program, origin + call.Offset, contours);
break;
case Motion motion:
if (resumeAt.HasValue)
{
Flush();
current = new PostContour { Pierce = resumeAt.Value + origin };
resumeAt = null;
}
current ??= new PostContour { Pierce = origin };
var absolute = (Motion)motion.Clone();
absolute.Offset(origin);
if (LayerOf(motion) == LayerType.Leadin)
{
if (current.LeadIn == null)
current.LeadIn = absolute;
else
current.LeadInExtra.Add(absolute);
}
else
{
// Contour moves are tagged Display by default and still cut.
current.Cuts.Add(absolute);
}
break;
}
}
Flush();
}
internal static LayerType LayerOf(Motion motion) =>
motion switch
{
LinearMove linear => linear.Layer,
ArcMove arc => arc.Layer,
_ => LayerType.Cut,
};
}