using System; using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.Posts.GravographIS { /// /// Lifts polylines out of an OpenNest for the Gravograph /// backend. Walks each 's , breaks /// polylines at rapid moves, and tessellates arcs to a chord-deviation /// tolerance (the wire format takes line segments only). /// public sealed class NestPolylineExtractor { public double ArcChordToleranceInches { get; set; } = 0.001; /// /// Extracts polylines from every non-cutoff part in every plate of the nest, /// returning them in plate coordinates (inches). /// public List> Extract(Nest nest) { if (nest == null) throw new ArgumentNullException(nameof(nest)); var result = new List>(); foreach (var plate in nest.Plates) { foreach (var part in plate.Parts) { if (part.BaseDrawing != null && part.BaseDrawing.IsCutOff) continue; ExtractPart(part, result); } } return result; } /// /// Extracts polylines for a single part. Public so callers driving the /// writer directly (e.g. from a console one-off) can use it. /// public List> ExtractPart(Part part) { var list = new List>(); ExtractPart(part, list); return list; } private void ExtractPart(Part part, List> sink) { var program = part.Program; if (program == null) return; // The walk below treats Motion.EndPoint as absolute. Convert a working // copy to absolute mode so G91 programs (the form OpenNest's UI writes) // produce correct geometry. Cloning keeps part.Program untouched. if (program.Mode == Mode.Incremental) { program = (Program)program.Clone(); program.Mode = Mode.Absolute; } var offset = part.Location; var pos = new Vector(0, 0); List current = null; foreach (var code in program.Codes) { if (code is Motion m && m.Suppressed) continue; switch (code) { case RapidMove rapid: { FlushCurrent(sink, ref current); pos = rapid.EndPoint; break; } case LinearMove linear: { if (current == null) { current = new List { pos + offset }; } var end = linear.EndPoint; current.Add(end + offset); pos = end; break; } case ArcMove arc: { if (current == null) { current = new List { pos + offset }; } TessellateArc(pos, arc, offset, ArcChordToleranceInches, current); pos = arc.EndPoint; break; } } } FlushCurrent(sink, ref current); } private static void FlushCurrent(List> sink, ref List current) { if (current != null && current.Count >= 2) sink.Add(current); current = null; } // Sample points along an arc to within chordTol of the true curve. start is // the arc's start point (current pen position), arc.CenterPoint is absolute // (G-code I/J in this codebase are stored as the absolute center), arc.EndPoint // is absolute end. The starting point is assumed to already be in the polyline; // intermediate samples and the endpoint are appended. private static void TessellateArc(Vector start, ArcMove arc, Vector offset, double chordTol, List sink) { var c = arc.CenterPoint; var r = c.DistanceTo(start); if (r < 1e-9) { sink.Add(arc.EndPoint + offset); return; } var a0 = System.Math.Atan2(start.Y - c.Y, start.X - c.X); var a1 = System.Math.Atan2(arc.EndPoint.Y - c.Y, arc.EndPoint.X - c.X); double sweep; if (arc.Rotation == RotationType.CW) { sweep = a0 - a1; if (sweep <= 0) sweep += 2 * System.Math.PI; } else { sweep = a1 - a0; if (sweep <= 0) sweep += 2 * System.Math.PI; } // Treat a near-zero sweep with coincident start/end as a full circle. if (sweep < 1e-9 && System.Math.Abs(start.X - arc.EndPoint.X) < 1e-9 && System.Math.Abs(start.Y - arc.EndPoint.Y) < 1e-9) { sweep = 2 * System.Math.PI; } // Max angle step from chord-deviation tolerance: dev = r * (1 - cos(t/2)). var maxAngleStep = 2.0 * System.Math.Acos(System.Math.Max(0.0, 1.0 - chordTol / r)); if (double.IsNaN(maxAngleStep) || maxAngleStep <= 0) maxAngleStep = System.Math.PI / 32; var steps = (int)System.Math.Ceiling(sweep / maxAngleStep); if (steps < 1) steps = 1; var direction = arc.Rotation == RotationType.CW ? -1.0 : 1.0; for (int i = 1; i < steps; i++) { var t = sweep * (i / (double)steps); var ang = a0 + direction * t; var pt = new Vector(c.X + r * System.Math.Cos(ang), c.Y + r * System.Math.Sin(ang)); sink.Add(pt + offset); } sink.Add(arc.EndPoint + offset); } } }