using System; using System.Collections.Generic; using OpenNest.Geometry; namespace OpenNest.CNC.Posting; /// /// 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. /// public static class PostContours { /// /// The part's contours in sheet coordinates. A display perimeter's tail is never cut and an /// unplanned one is refused (). Cut-off contours /// are open lines on the beam centreline: no perimeter and no kerf. /// public static List 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; } /// /// The program's contours with added to every point: the part /// location for sheet coordinates, for the part's own frame. /// public static List Extract(Program program, Vector origin) { var contours = Flatten(program, origin); Classify(contours); return contours; } /// /// The program's contours in the 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 . /// public static List Flatten(Program program, Vector origin) { var contours = new List(); FlattenInto(program, origin, contours); return contours; } /// /// Picks one part's perimeter (marks excluded) and each contour's kerf side. A common-line /// member's outside is its remainders, each compensated on its perimeter's side, so its /// closed contours are all holes; a shared cut has no kerf side. /// public static void Classify(IReadOnlyList contours) { var remainders = false; foreach (var contour in contours) remainders |= contour.RemainderWinding != null; var perimeter = remainders ? -1 : ContourWinding.PerimeterIndex(contours); for (var i = 0; i < contours.Count; i++) { var contour = contours[i]; if (contour.RemainderWinding is { } winding) { // G41 leaves the product right of travel: a clockwise perimeter's side. contour.IsPerimeter = true; contour.Kerf = winding == RotationType.CW ? KerfType.Left : KerfType.Right; continue; } contour.IsPerimeter = i == perimeter; contour.Kerf = contour.IsMark || contour.IsCommonLine ? KerfType.None : ContourWinding.CompensationSide(contour, contour.IsPerimeter); } } /// /// Walks one program whose coordinates are relative to ; 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. A head-raise marker raises the head for /// the next contour's rapid; carries a marker before the hole /// call that runs this program. /// private static void FlattenInto(Program program, Vector origin, List contours, bool raiseFirst = false) { 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; var raise = raiseFirst; PostContour Start(Vector pierce) { var contour = new PostContour { Pierce = pierce, Raised = raise }; raise = false; return contour; } 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 (HeadRaise.IsMarker(code)) { raise = true; continue; } if (CommonLineCuts.RemainderWinding(code) is { } winding) { current ??= Start(origin); current.RemainderWinding = winding; continue; } if (HeadHeightLock.IsLock(code) || HeadHeightLock.IsUnlock(code)) { current ??= Start(origin); current.HeadLocks.Add((current.Cuts.Count, HeadHeightLock.IsLock(code))); continue; } switch (code) { case RapidMove rapid: Flush(); resumeAt = null; current = Start(rapid.EndPoint + origin); break; case SubProgramCall call: // A hole call is its own contour. Flush(); resumeAt = null; FlattenInto(call.Program, origin + call.Offset, contours, raise); raise = false; break; case Motion motion: if (resumeAt.HasValue) { Flush(); current = Start(resumeAt.Value + origin); resumeAt = null; } current ??= Start(origin); var absolute = (Motion)motion.Clone(); absolute.Offset(origin); var layer = LayerOf(motion); if (layer == 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); if (layer == LayerType.CommonLine) current.IsCommonLine = true; } break; } } Flush(); } internal static LayerType LayerOf(Motion motion) => motion switch { LinearMove linear => linear.Layer, ArcMove arc => arc.Layer, _ => LayerType.Cut, }; }