using System; using System.Collections.Generic; using System.Linq; using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Desktop.Rendering; /// A program's display layers in its own frame, before placement. /// /// Whether a cut contour is open, such as a cutout left attached by tabs. Filling /// would close it with a chord; fill instead. /// public sealed record ProgramLayers( IReadOnlyList Cut, IReadOnlyList Lead, IReadOnlyList Etch, bool HasOpenCuts); /// /// A CNC program's moves as toolkit-neutral figures, routed by layer as the WinForms plate view /// routes them (its GraphicsHelper): cuts, lead-ins/outs, etches and the filled material. /// Suppressed moves are skipped, a tabbed cutout's tabs are filled but never drawn as cuts, and /// break cuts are drawn but never bound the fill. Coordinates stay in the program frame: callers /// place the figures at the part's location. /// public static class ProgramGeometry { /// Cut strokes: everything but etches, leads and tabs (tabs are never cut). public static bool IsCutLayer(LayerType layer) => layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Tab); public static bool IsLeadLayer(LayerType layer) => layer is LayerType.Leadin or LayerType.Leadout; public static bool IsEtchLayer(LayerType layer) => layer == LayerType.Scribe; /// /// Material outline layers: everything but etches, leads and break cuts. Break cuts are open /// lines through material: a fill would close each with a chord. /// public static bool IsFillLayer(LayerType layer) => layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Break); /// The cut, lead and etch figures of . public static ProgramLayers Layers(Program program) { ArgumentNullException.ThrowIfNull(program); var cut = Figures(program, IsCutLayer, bridgeTabs: false, out var hasOpenCuts); var lead = Figures(program, IsLeadLayer, bridgeTabs: false, out _); var etch = Figures(program, IsEtchLayer, bridgeTabs: false, out _); return new ProgramLayers(cut, lead, etch, hasOpenCuts); } /// /// The closed material outlines to fill. A contour cut in pieces around tabs is rejoined /// across its tab gaps, so it fills as one region rather than as chord-closed pieces. /// public static IReadOnlyList Fill(Program program) { ArgumentNullException.ThrowIfNull(program); return Figures(program, IsFillLayer, bridgeTabs: true, out _); } /// Whether or a sub-program has a break cut. public static bool HasBreakCuts(Program program) => program.Codes.Any(code => code is LinearMove { Layer: LayerType.Break } or ArcMove { Layer: LayerType.Break } || code is SubProgramCall { Program: { } sub } && HasBreakCuts(sub)); /// /// The unsuppressed moves on layers accepts, one figure per /// continuous run. A run ends at a skipped move, a rapid that moves, or a sub-program call. /// With , open figures are chained end to start (see ). /// public static List Figures(Program program, Func includeLayer, bool bridgeTabs, out bool hasOpenFigures) { ArgumentNullException.ThrowIfNull(program); ArgumentNullException.ThrowIfNull(includeLayer); var figures = new List(); var position = Vector.Zero; AddProgram(figures, program, ref position, includeLayer); hasOpenFigures = figures.Exists(figure => !figure.IsClosed); return Combine(figures, bridgeTabs); } /// /// With , each open figure is joined end-to-start with the open /// figure starting nearest its end, until its own start is nearer, so the tab-separated pieces /// of one contour fill as a single region. Closed figures pass through unchanged. /// public static List Combine(IReadOnlyList figures, bool bridgeTabs) { var result = new List(figures.Count); var pending = new List(figures); while (pending.Count > 0) { var figure = pending[0]; pending.RemoveAt(0); if (!bridgeTabs || figure.IsClosed) { result.Add(figure); continue; } var segments = new List(figure.Segments); var end = figure.End; while (true) { PathFigure next = null; var nextGap = end.DistanceTo(figure.Start); foreach (var candidate in pending) { if (candidate.IsClosed) continue; var gap = end.DistanceTo(candidate.Start); if (gap < nextGap) { next = candidate; nextGap = gap; } } if (next == null) break; pending.Remove(next); // Bridge the tab gap with a line, as GDI+ AddPath(connect: true) does. if (nextGap > 0) segments.Add(new LineTo(next.Start)); segments.AddRange(next.Segments); end = next.End; } result.Add(new PathFigure(figure.Start, segments)); } return result; } private static void AddProgram(List figures, Program program, ref Vector position, Func includeLayer) { // Sub-program offsets are relative to the frame origin at entry, not the tool position. var frameOrigin = position; var mode = program.Mode; List current = null; var figureStart = position; // Call before moving past a skipped code so the figure records where its cut ended. void Flush() { if (current != null) { figures.Add(new PathFigure(figureStart, current)); current = null; } } void Begin(Vector start) { if (current == null) { current = new List(); figureStart = start; } } for (var i = 0; i < program.Length; ++i) { switch (program[i]) { case ArcMove arc: { if (!arc.Suppressed && includeLayer(arc.Layer)) { Begin(position); current.Add(Arc(arc, mode, position)); } else Flush(); position = mode == Mode.Incremental ? arc.EndPoint + position : arc.EndPoint; break; } case LinearMove line: { var end = mode == Mode.Incremental ? line.EndPoint + position : line.EndPoint; if (!line.Suppressed && includeLayer(line.Layer)) { Begin(position); current.Add(new LineTo(end)); } else Flush(); position = end; break; } case RapidMove rapid: { var end = mode == Mode.Incremental ? rapid.EndPoint + position : rapid.EndPoint; var dx = end.X - position.X; var dy = end.Y - position.Y; if (dx * dx + dy * dy > 0.001 * 0.001) Flush(); position = end; break; } case SubProgramCall call: { Flush(); if (call.Program != null) { position = new Vector(frameOrigin.X + call.Offset.X, frameOrigin.Y + call.Offset.Y); AddProgram(figures, call.Program, ref position, includeLayer); } break; } } } Flush(); } /// An arc move from , as GraphicsHelper.AddArc draws it. private static ArcTo Arc(ArcMove arc, Mode mode, Vector position) { var end = arc.EndPoint; var center = arc.CenterPoint; if (mode == Mode.Incremental) { end += position; center += position; } var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(position.Y - center.Y, position.X - center.X))); var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(end.Y - center.Y, end.X - center.X))); var radius = center.DistanceTo(end); if (startAngle.IsEqualTo(endAngle)) return new ArcTo(end, center, radius, startAngle, arc.Rotation == RotationType.CW ? -360.0 : 360.0); if (arc.Rotation == RotationType.CCW && endAngle < startAngle) endAngle += 360.0; else if (arc.Rotation == RotationType.CW && startAngle < endAngle) startAngle += 360.0; return new ArcTo(end, center, radius, startAngle, endAngle - startAngle); } /// /// Geometry entities (display-only curves) as figures, one per entity, skipping lead-in and /// lead-out layers as the WinForms shape path does. /// public static List EntityFigures(IEnumerable entities) { var figures = new List(); foreach (var entity in entities) AddEntity(figures, entity); return figures; } private static void AddEntity(List figures, Entity entity) { if (entity.Layer != null && (string.Equals(entity.Layer.Name, SpecialLayers.Leadin.Name, StringComparison.OrdinalIgnoreCase) || string.Equals(entity.Layer.Name, SpecialLayers.Leadout.Name, StringComparison.OrdinalIgnoreCase))) return; switch (entity) { case Line line: figures.Add(new PathFigure(line.StartPoint, new[] { new LineTo(line.EndPoint) })); break; case Arc arc: { var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.EndAngle)); var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.StartAngle)); if (arc.Rotation == RotationType.CCW && endAngle < startAngle) endAngle += 360.0; else if (arc.Rotation == RotationType.CW && startAngle < endAngle) startAngle += 360.0; var start = PointOn(arc.Center, arc.Radius, startAngle); var end = PointOn(arc.Center, arc.Radius, endAngle); figures.Add(new PathFigure(start, new[] { new ArcTo(end, arc.Center, arc.Radius, startAngle, endAngle - startAngle) })); break; } case Circle circle: { var start = new Vector(circle.Center.X + circle.Radius, circle.Center.Y); figures.Add(new PathFigure(start, new[] { new ArcTo(start, circle.Center, circle.Radius, 0, 360.0) })); break; } case Polygon polygon when polygon.Vertices.Count > 0: { var vertices = polygon.Vertices; var segments = new List(vertices.Count); for (var i = 1; i < vertices.Count; i++) segments.Add(new LineTo(vertices[i])); // GDI+ AddPolygon closes the figure. if (vertices[^1].DistanceTo(vertices[0]) > 0) segments.Add(new LineTo(vertices[0])); figures.Add(new PathFigure(vertices[0], segments)); break; } case Shape shape: foreach (var child in shape.Entities) AddEntity(figures, child); break; } } private static Vector PointOn(Vector center, double radius, double degrees) { var radians = Angle.ToRadians(degrees); return new Vector(center.X + radius * System.Math.Cos(radians), center.Y + radius * System.Math.Sin(radians)); } /// The bounding box of the figures' points and arc extremes, or null when empty. public static Box Bounds(IEnumerable figures) { double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue; var any = false; void Add(Vector p) { any = true; minX = System.Math.Min(minX, p.X); minY = System.Math.Min(minY, p.Y); maxX = System.Math.Max(maxX, p.X); maxY = System.Math.Max(maxY, p.Y); } foreach (var figure in figures) { Add(figure.Start); foreach (var segment in figure.Segments) { if (segment is ArcTo arc) { Add(PointOn(arc.Center, arc.Radius, arc.StartAngle)); var from = System.Math.Min(arc.StartAngle, arc.StartAngle + arc.SweepAngle); var to = System.Math.Max(arc.StartAngle, arc.StartAngle + arc.SweepAngle); for (var quadrant = System.Math.Ceiling(from / 90.0) * 90.0; quadrant <= to; quadrant += 90.0) Add(PointOn(arc.Center, arc.Radius, quadrant)); } Add(segment.End); } } return any ? new Box(minX, minY, maxX - minX, maxY - minY) : null; } }