using System; using System.Collections.Generic; using System.Linq; using OpenNest.Geometry; namespace OpenNest.CNC { /// /// The never-cut outline of a supplied blank, kept at the end of an applied cutting program. /// An applied program for a blank with a outline cuts /// only its cutouts; the outline follows them after so the part keeps the /// blank's extent for bounds, overlap and cut-offs. Nothing that executes or posts a program may /// run the tail: executed reads stop at the marker and posts take . /// public static class DisplayPerimeterTail { /// The comment that starts the tail; every code after it is display only. public const string Marker = "DISPLAY PERIMETER NEVER CUT"; /// Why the manual and legacy lead-in tools leave a display-perimeter part alone. public const string LeadInRefusal = "This part's outline is a display perimeter: a supplied blank that is never cut. " + "Lead-ins for its cutouts come from Plan Cutting, which keeps every pierce inside a cutout."; /// /// The index of the tail's marker in 's codes, or -1. A marker that is /// repeated, sits in a sub-program, or is followed by anything but one rapid and moves on the /// display perimeter layer is a malformed program (): cuts after /// it must never be dropped silently. /// public static int IndexOf(Program program) { ArgumentNullException.ThrowIfNull(program); var marker = -1; for (var i = 0; i < program.Codes.Count; i++) { if (program.Codes[i] is Comment { Value: Marker }) { if (marker >= 0) throw new ArgumentException("A program has more than one display perimeter tail."); marker = i; } else if (program.Codes[i] is SubProgramCall call && HasMarker(call.Program, new HashSet(ReferenceEqualityComparer.Instance))) throw new ArgumentException("A display perimeter tail can only end the part's own program, not a sub-program."); } if (marker < 0) return -1; var tail = program.Codes.Count - marker - 1; if (tail < 2 || program.Codes[marker + 1] is not RapidMove || !program.Codes.Skip(marker + 2).All(code => code is LinearMove { Layer: LayerType.DisplayPerimeter } or ArcMove { Layer: LayerType.DisplayPerimeter })) throw new ArgumentException( "A display perimeter tail must be one rapid followed only by the outline on the display perimeter layer."); return marker; } private static bool HasMarker(Program program, HashSet visited) { if (program?.Codes == null || !visited.Add(program)) return false; foreach (var code in program.Codes) if (code is Comment { Value: Marker } || code is SubProgramCall call && HasMarker(call.Program, visited)) return true; return false; } public static bool HasTail(Program program) => IndexOf(program) >= 0; /// /// Appends the outline after the marker: a rapid to its start, then its entities on the /// display perimeter layer. must still be in absolute mode. /// internal static void Append(Program program, IReadOnlyList outline, Vector start) { if (program.Mode != Mode.Absolute) throw new InvalidOperationException("Append the display tail before switching to incremental mode."); program.Codes.Add(new Comment(Marker)); program.Codes.Add(new RapidMove(start)); program.Codes.AddRange(outline); } /// /// The executed program for estimates and drawing: itself when it /// has no well-formed tail, otherwise a shallow copy of the codes before the marker. Posts /// and verification use the strict and executed reads instead. /// public static Program WithoutTail(Program program) { try { var marker = IndexOf(program); return marker < 0 ? program : program.HeadCopy(marker); } catch (ArgumentException) { return program; } } /// Whether or any sub-program it calls moves on the display perimeter layer. public static bool HasDisplayPerimeterMotion(Program program) => HasDisplayPerimeterMotion(program, new HashSet(ReferenceEqualityComparer.Instance)); private static bool HasDisplayPerimeterMotion(Program program, HashSet visited) { if (program?.Codes == null || !visited.Add(program)) return false; foreach (var code in program.Codes) { if (code is LinearMove { Layer: LayerType.DisplayPerimeter } or ArcMove { Layer: LayerType.DisplayPerimeter }) return true; if (code is SubProgramCall call && HasDisplayPerimeterMotion(call.Program, visited)) return true; } return false; } /// /// The program a post may write for a part: its tail removed. A display perimeter anywhere /// else means the part was never planned, and posting it would cut the blank's outline. /// public static Program ForPosting(Program program, string partName) { int marker; try { marker = IndexOf(program); } catch (ArgumentException ex) { throw new InvalidOperationException($"{partName} cannot be posted: {ex.Message}", ex); } var executed = marker < 0 ? program : program.HeadCopy(marker); if (HasDisplayPerimeterMotion(executed)) throw new InvalidOperationException( $"{partName} has a display perimeter (a supplied blank's outline that is never cut) but no " + "cutting plan. Plan cutting first (Plate > Plan Cutting..., or --plan-cutting) so only its cutouts are cut."); return executed; } /// The name posts give a part in a refusal. public static string PartName(Part part) => $"Part '{part.BaseDrawing?.Name}'"; /// /// Refuses a nest a post cannot write, before any output is created: every part's program /// must pass , so a refusal never leaves an empty or partial file. /// public static void EnsurePostable(Nest nest) { ArgumentNullException.ThrowIfNull(nest); foreach (var plate in nest.Plates) foreach (var part in plate.Parts) if (part?.Program != null && part.BaseDrawing?.IsCutOff != true) ForPosting(part.Program, PartName(part)); } } }