diff --git a/OpenNest.Core/CNC/RapidEnumerator.cs b/OpenNest.Core/CNC/RapidEnumerator.cs new file mode 100644 index 0000000..365e45d --- /dev/null +++ b/OpenNest.Core/CNC/RapidEnumerator.cs @@ -0,0 +1,80 @@ +using OpenNest.Geometry; +using System.Collections.Generic; + +namespace OpenNest.CNC +{ + public static class RapidEnumerator + { + public readonly record struct Segment(Vector From, Vector To); + + public static List Enumerate(Program pgm, Vector basePos, Vector startPos) + { + var results = new List(); + + // Draw the rapid from the previous tool position to the program's first + // pierce point. This also primes pos so the interior walk interprets + // Incremental deltas from the correct absolute location (basePos), which + // matters for raw pre-lead-in programs that are emitted Incremental. + var firstPierce = FirstPiercePoint(pgm, basePos); + results.Add(new Segment(startPos, firstPierce)); + + var pos = firstPierce; + Walk(pgm, basePos, ref pos, skipFirst: true, results); + return results; + } + + private static Vector FirstPiercePoint(Program pgm, Vector basePos) + { + for (var i = 0; i < pgm.Length; i++) + { + if (pgm[i] is SubProgramCall call && call.Program != null) + return FirstPiercePoint(call.Program, basePos + call.Offset); + + if (pgm[i] is Motion motion) + return motion.EndPoint + basePos; + } + return basePos; + } + + private static void Walk(Program pgm, Vector basePos, ref Vector pos, bool skipFirst, List results) + { + var skipped = !skipFirst; + + for (var i = 0; i < pgm.Length; ++i) + { + var code = pgm[i]; + + if (code is SubProgramCall { Program: { } program } call) + { + var holeBase = basePos + call.Offset; + var firstPierce = FirstPiercePoint(program, holeBase); + + if (!skipped) + skipped = true; + else + results.Add(new Segment(pos, firstPierce)); + + var subPos = holeBase; + Walk(program, holeBase, ref subPos, skipFirst: true, results); + pos = subPos; + } + else if (code is Motion motion) + { + var endpt = pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint + basePos; + + if (code.Type == CodeType.RapidMove) + { + if (!skipped) + skipped = true; + else + results.Add(new Segment(pos, endpt)); + } + + pos = endpt; + } + } + } + } +} diff --git a/OpenNest.Tests/CNC/RapidEnumeratorTests.cs b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs new file mode 100644 index 0000000..9db9903 --- /dev/null +++ b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs @@ -0,0 +1,84 @@ +using OpenNest.CNC; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Tests.CNC +{ + public class RapidEnumeratorTests + { + [Fact] + public void Enumerate_AbsoluteProgram_OffsetsMotionsByBasePos() + { + var pgm = new Program(Mode.Absolute); + pgm.Codes.Add(new RapidMove(1, 0)); + pgm.Codes.Add(new LinearMove(2, 0)); + pgm.Codes.Add(new RapidMove(3, 3)); + + var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0)); + + // Origin → first pierce, then interior rapid from contour end to next rapid target. + Assert.Equal(2, segments.Count); + Assert.Equal(new Vector(0, 0), segments[0].From); + Assert.Equal(new Vector(101, 200), segments[0].To); + Assert.Equal(new Vector(102, 200), segments[1].From); + Assert.Equal(new Vector(103, 203), segments[1].To); + } + + [Fact] + public void Enumerate_IncrementalProgram_InterpretsDeltasFromBasePos() + { + // Pre-lead-in raw program: first rapid normalized to (0,0), Mode=Incremental + // (matches ConvertGeometry.ToProgram output). + var pgm = new Program(Mode.Incremental); + pgm.Codes.Add(new RapidMove(0, 0)); + pgm.Codes.Add(new LinearMove(5, 0)); + pgm.Codes.Add(new LinearMove(0, 5)); + pgm.Codes.Add(new RapidMove(1, 1)); + + var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0)); + + Assert.Equal(2, segments.Count); + // First rapid: plate origin → part pierce at basePos. + Assert.Equal(new Vector(0, 0), segments[0].From); + Assert.Equal(new Vector(100, 200), segments[0].To); + // Interior rapid: after deltas (5,0) and (0,5) from basePos, rapid delta (1,1). + Assert.Equal(new Vector(105, 205), segments[1].From); + Assert.Equal(new Vector(106, 206), segments[1].To); + } + + [Fact] + public void Enumerate_SubProgramCall_RapidEndsAtAbsoluteHolePierce() + { + // Main program: lead-in rapid, a line, then a SubProgramCall for a hole. + // Sub-program (incremental) starts with RapidMove(radius, 0) to the hole pierce. + var sub = new Program(Mode.Incremental); + sub.Codes.Add(new RapidMove(0.5, 0)); + sub.Codes.Add(new LinearMove(0, 0.1)); + + var pgm = new Program(Mode.Absolute); + pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in) + pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move + pgm.Codes.Add(new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(2, 2), // hole center (drawing-local) + }); + + var basePos = new Vector(100, 200); // part.Location + var segments = RapidEnumerator.Enumerate(pgm, basePos, startPos: new Vector(0, 0)); + + // Expected rapids: + // 1. origin → first pierce (0.2+100, 0.3+200) = (100.2, 200.3) + // 2. end of contour (1+100, 1+200) = (101, 201) → hole pierce (2+100+0.5, 2+200) = (102.5, 202) + // The sub's internal first rapid is skipped (already drawn in #2). + Assert.Equal(2, segments.Count); + + Assert.Equal(new Vector(0, 0), segments[0].From); + Assert.Equal(new Vector(100.2, 200.3), segments[0].To); + + Assert.Equal(new Vector(101, 201), segments[1].From); + Assert.Equal(new Vector(102.5, 202), segments[1].To); + } + } +} diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index 9af15dc..ef5f1b5 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -385,85 +385,20 @@ namespace OpenNest.Controls private void DrawRapids(Graphics g) { + var pen = view.ColorScheme.RapidPen; var pos = new Vector(0, 0); for (var i = 0; i < view.Plate.Parts.Count; ++i) { var part = view.Plate.Parts[i]; - var pgm = part.Program; + var segments = RapidEnumerator.Enumerate(part.Program, part.Location, pos); - var piercePoint = GetFirstPiercePoint(pgm, part.Location); - DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen); - - pos = piercePoint; - DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true); - } - } - - private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation) - { - for (var i = 0; i < pgm.Length; i++) - { - if (pgm[i] is SubProgramCall call && call.Program != null) - return GetFirstPiercePoint(call.Program, partLocation + call.Offset); - - if (pgm[i] is Motion motion) + foreach (var seg in segments) { - return motion.EndPoint + partLocation; + DrawLine(g, seg.From, seg.To, pen); + pos = seg.To; } } - return partLocation; - } - - private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false) - { - var firstRapidSkipped = false; - - for (var i = 0; i < pgm.Length; ++i) - { - var code = pgm[i]; - - if (code is SubProgramCall { Program: { } program } call) - { - // A SubProgramCall is a coordinate-frame shift, not a physical - // rapid to the hole center. The Cincinnati post emits it as a - // G52 bracket, so the physical rapid is the sub-program's first - // motion, which goes straight from here to the lead-in pierce. - // Look ahead for that pierce point and draw the direct rapid, - // then recurse with skipFirstRapid so the sub doesn't also draw - // its first rapid on top. See docs/cincinnati-post-output.md. - var holeBase = basePos + call.Offset; - var firstPierce = GetFirstPiercePoint(program, holeBase); - - if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped)) - DrawLine(g, pos, firstPierce, view.ColorScheme.RapidPen); - - var subPos = holeBase; - DrawRapids(g, program, holeBase, ref subPos, skipFirstRapid: true); - pos = subPos; - } - else if (code is Motion motion) - { - var endpt = pgm.Mode == Mode.Incremental - ? motion.EndPoint + pos - : motion.EndPoint; - - if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped)) - DrawLine(g, pos, endpt, view.ColorScheme.RapidPen); - - pos = endpt; - } - } - } - - private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped) - { - if (skipFirstRapid && !firstRapidSkipped) - { - firstRapidSkipped = true; - return false; - } - return true; } private void DrawAllPiercePoints(Graphics g)