diff --git a/OpenNest.Core/CNC/RapidEnumerator.cs b/OpenNest.Core/CNC/RapidEnumerator.cs index bbedb01..51154f6 100644 --- a/OpenNest.Core/CNC/RapidEnumerator.cs +++ b/OpenNest.Core/CNC/RapidEnumerator.cs @@ -31,16 +31,16 @@ namespace OpenNest.CNC private static Vector AppendProgram(Program pgm, Vector basePos, Vector startPos, List results) { - // Draw the rapid from the previous tool position to the program's first - // pierce point. The walk then starts at the program origin (basePos), not - // the pierce: the skipped first rapid still advances pos, so starting at - // the pierce would apply a nonzero Incremental first delta twice (as in - // lead-in programs) and shift every later rapid by it. + // Draw the approach to the first rapid destination, or to the program + // origin when it starts cutting without a rapid. Only an initial rapid + // (or call arrival) is already represented by this approach; a later + // first rapid must remain visible. Walk starts at the program origin, + // so an incremental initial rapid is not applied twice. var firstPierce = FirstPiercePoint(pgm, basePos); results.Add(new Segment(startPos, firstPierce)); var pos = basePos; - Walk(pgm, basePos, ref pos, skipFirst: true, results); + Walk(pgm, basePos, ref pos, skipFirst: StartsWithRapidOrCall(pgm), results); // The last rapid ends at a pierce, not necessarily the final tool position. return pos; } @@ -53,11 +53,23 @@ namespace OpenNest.CNC return FirstPiercePoint(call.Program, basePos + call.Offset); if (pgm[i] is Motion motion) - return motion.EndPoint + basePos; + return motion is RapidMove ? motion.EndPoint + basePos : basePos; } return basePos; } + private static bool StartsWithRapidOrCall(Program pgm) + { + for (var i = 0; i < pgm.Length; i++) + { + if (pgm[i] is SubProgramCall { Program: not null }) + return true; + if (pgm[i] is Motion motion) + return motion is RapidMove; + } + return false; + } + private static void Walk( Program pgm, Vector basePos, @@ -83,7 +95,7 @@ namespace OpenNest.CNC results.Add(new Segment(pos, firstPierce)); var subPos = holeBase; - Walk(program, holeBase, ref subPos, skipFirst: true, results); + Walk(program, holeBase, ref subPos, skipFirst: StartsWithRapidOrCall(program), results); pos = subPos; } else if (code is Motion motion) diff --git a/OpenNest.Tests/CNC/RapidEnumeratorTests.cs b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs index 1d17fc2..908f685 100644 --- a/OpenNest.Tests/CNC/RapidEnumeratorTests.cs +++ b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs @@ -76,6 +76,70 @@ namespace OpenNest.Tests.CNC Assert.Equal(new Vector(108, 204), segments[1].To); } + [Theory] + [InlineData(Mode.Absolute)] + [InlineData(Mode.Incremental)] + public void Enumerate_ProgramStartingWithCut_RetainsFirstInternalRapid(Mode mode) + { + // A saved placed program can start with a lead-in cut rather than a G00. + // The first G00 then travels from that cutout to the next one. + var pgm = new Program(mode); + pgm.Codes.Add(new LinearMove(2, 0)); + pgm.Codes.Add(new RapidMove(4, 1)); + pgm.Codes.Add(new LinearMove(5, 1)); + pgm.Codes.Add(new RapidMove(6, 2)); + + var segments = RapidEnumerator.Enumerate( + pgm, + basePos: new Vector(100, 200), + startPos: Vector.Zero + ); + + Assert.Equal(3, segments.Count); + Assert.Equal(new RapidEnumerator.Segment(Vector.Zero, new Vector(100, 200)), segments[0]); + Assert.Equal(new Vector(102, 200), segments[1].From); + Assert.Equal(mode == Mode.Absolute ? new Vector(104, 201) : new Vector(106, 201), segments[1].To); + Assert.Equal(mode == Mode.Absolute ? new Vector(105, 201) : new Vector(111, 202), segments[2].From); + } + + [Fact] + public void Enumerate_EveryPartStartingWithCut_RetainsItsFirstRapid() + { + var program = new Program(Mode.Incremental); + program.Codes.Add(new LinearMove(1, 0)); + program.Codes.Add(new RapidMove(1, 1)); + program.Codes.Add(new LinearMove(0, 1)); + var drawing = new Drawing("cut-first", program); + var parts = new[] + { + new Part(drawing, new Vector(10, 20)), + new Part(drawing, new Vector(30, 40)), + }; + + var segments = RapidEnumerator.Enumerate(parts); + + Assert.Equal(4, segments.Count); + Assert.Equal(new RapidEnumerator.Segment(new Vector(11, 20), new Vector(12, 21)), segments[1]); + Assert.Equal(new RapidEnumerator.Segment(new Vector(31, 40), new Vector(32, 41)), segments[3]); + } + + [Fact] + public void Enumerate_SubProgramStartingWithCut_RetainsFirstInternalRapid() + { + var hole = new Program(Mode.Incremental); + hole.Codes.Add(new LinearMove(1, 0)); + hole.Codes.Add(new RapidMove(2, 1)); + hole.Codes.Add(new LinearMove(0, 1)); + var pgm = new Program(Mode.Absolute); + pgm.Codes.Add(new SubProgramCall { Id = 1, Program = hole, Offset = new Vector(5, 5) }); + + var segments = RapidEnumerator.Enumerate(pgm, new Vector(100, 200), Vector.Zero); + + Assert.Equal(2, segments.Count); + Assert.Equal(new RapidEnumerator.Segment(Vector.Zero, new Vector(105, 205)), segments[0]); + Assert.Equal(new RapidEnumerator.Segment(new Vector(106, 205), new Vector(108, 206)), segments[1]); + } + [Fact] public void Enumerate_SubProgramCall_RapidEndsAtAbsoluteHolePierce() {