fix(cnc): show first internal rapid after cut-first programs

This commit is contained in:
aj committed 2026-10-11 15:48:07 -04:00
1 parent 08acb092a7
commit 3d512c794c
2 files changed
+84 -8

No files matched your search

+20 -8
View File
@@ -31,16 +31,16 @@ namespace OpenNest.CNC
private static Vector AppendProgram(Program pgm, Vector basePos, Vector startPos, List<Segment> 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)
@@ -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()
{