mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-12 17:01:09 -05:00
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>
174 lines
6.1 KiB
C#
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,
|
|
};
|
|
}
|