fix: track tool position through sub-programs in ConvertMode

ConvertMode.ToIncremental skipped SubProgramCall codes entirely when
computing deltas, so parent motions after a sub-call were encoded as if
the tool never moved. Several traversal sites (ConvertProgram,
GraphicsHelper, PlateRenderer, CutDirectionArrows, Program.BoundingBox)
worked around this with save/restore hacks that treated sub-calls as
transparent — but DrawRapids legitimately tracks actual tool position,
so after the last hole the first perimeter rapid was applied to the
wrong base, drifting the rendered perimeter past the plate edge by
roughly the distance to the last hole.

Fix the root cause: ToIncremental and ToAbsolute now walk sub-programs
to compute where they leave the tool, and advance pos accordingly. The
other traversals capture a frameOrigin at entry and compute sub-call
placement as frameOrigin + Offset, letting pos advance naturally
through the sub recursion. All the save/restore workarounds are
removed.

Program.BoundingBox also picks up the same frame-origin treatment,
which corrects a latent bug where absolute-mode endpoints and nested
sub-calls dropped the parent's frame origin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 07:51:51 -04:00
parent a6c2235647
commit 572fa06a21
6 changed files with 127 additions and 97 deletions
+15 -8
View File
@@ -288,6 +288,10 @@ namespace OpenNest.CNC
private Box BoundingBox(ref Vector pos) private Box BoundingBox(ref Vector pos)
{ {
// Capture the frame origin at entry. Sub-program Offsets and
// absolute-mode endpoints are relative to this fixed origin.
var frameOrigin = pos;
double minX = 0.0; double minX = 0.0;
double minY = 0.0; double minY = 0.0;
double maxX = 0.0; double maxX = 0.0;
@@ -303,7 +307,7 @@ namespace OpenNest.CNC
{ {
var line = (LinearMove)code; var line = (LinearMove)code;
var pt = Mode == Mode.Absolute ? var pt = Mode == Mode.Absolute ?
line.EndPoint : frameOrigin + line.EndPoint :
line.EndPoint + pos; line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
@@ -325,7 +329,7 @@ namespace OpenNest.CNC
{ {
var line = (RapidMove)code; var line = (RapidMove)code;
var pt = Mode == Mode.Absolute var pt = Mode == Mode.Absolute
? line.EndPoint ? frameOrigin + line.EndPoint
: line.EndPoint + pos; : line.EndPoint + pos;
if (pt.X > maxX) if (pt.X > maxX)
@@ -358,8 +362,8 @@ namespace OpenNest.CNC
} }
else else
{ {
endpt = arc.EndPoint; endpt = frameOrigin + arc.EndPoint;
centerpt = arc.CenterPoint; centerpt = frameOrigin + arc.CenterPoint;
} }
double minX1; double minX1;
@@ -433,10 +437,13 @@ namespace OpenNest.CNC
case CodeType.SubProgramCall: case CodeType.SubProgramCall:
{ {
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
var subPos = subpgm.Offset.X != 0 || subpgm.Offset.Y != 0 if (subpgm.Program == null)
? new Vector(subpgm.Offset.X, subpgm.Offset.Y) break;
: pos;
var box = subpgm.Program.BoundingBox(ref subPos); // Sub-program frame origin in this program's frame
// is frameOrigin + Offset, regardless of current pos.
pos = frameOrigin + subpgm.Offset;
var box = subpgm.Program.BoundingBox(ref pos);
if (box.Left < minX) if (box.Left < minX)
minX = box.Left; minX = box.Left;
+53 -9
View File
@@ -1,4 +1,4 @@
using OpenNest.CNC; using OpenNest.CNC;
using OpenNest.Geometry; using OpenNest.Geometry;
namespace OpenNest.Converters namespace OpenNest.Converters
@@ -9,7 +9,6 @@ namespace OpenNest.Converters
/// Converts the program to absolute coordinates. /// Converts the program to absolute coordinates.
/// Does NOT check program mode before converting. /// Does NOT check program mode before converting.
/// </summary> /// </summary>
/// <param name="pgm"></param>
public static void ToAbsolute(Program pgm) public static void ToAbsolute(Program pgm)
{ {
var pos = new Vector(0, 0); var pos = new Vector(0, 0);
@@ -17,21 +16,27 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++i) for (int i = 0; i < pgm.Codes.Count; ++i)
{ {
var code = pgm.Codes[i]; var code = pgm.Codes[i];
var motion = code as Motion;
if (motion != null) if (code is SubProgramCall subCall && subCall.Program != null)
{ {
motion.Offset(pos); // Sub-program is placed at Offset in this program's frame.
// After it runs, the tool is at Offset + (sub's end in its own frame).
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
continue;
}
if (code is Motion motion)
{
motion.Offset(pos.X, pos.Y);
pos = motion.EndPoint; pos = motion.EndPoint;
} }
} }
} }
/// <summary> /// <summary>
/// Converts the program to intermental coordinates. /// Converts the program to incremental coordinates.
/// Does NOT check program mode before converting. /// Does NOT check program mode before converting.
/// </summary> /// </summary>
/// <param name="pgm"></param>
public static void ToIncremental(Program pgm) public static void ToIncremental(Program pgm)
{ {
var pos = new Vector(0, 0); var pos = new Vector(0, 0);
@@ -39,9 +44,16 @@ namespace OpenNest.Converters
for (int i = 0; i < pgm.Codes.Count; ++i) for (int i = 0; i < pgm.Codes.Count; ++i)
{ {
var code = pgm.Codes[i]; var code = pgm.Codes[i];
var motion = code as Motion;
if (motion != null) if (code is SubProgramCall subCall && subCall.Program != null)
{
// Sub-program is placed at Offset in this program's frame,
// regardless of where the tool was before the call.
pos = ComputeEndPosition(subCall.Program, subCall.Offset);
continue;
}
if (code is Motion motion)
{ {
var pos2 = motion.EndPoint; var pos2 = motion.EndPoint;
motion.Offset(-pos.X, -pos.Y); motion.Offset(-pos.X, -pos.Y);
@@ -49,5 +61,37 @@ namespace OpenNest.Converters
} }
} }
} }
/// <summary>
/// Computes the tool position after executing <paramref name="pgm"/>,
/// given that the program's frame origin is at <paramref name="startPos"/>
/// in the caller's frame. Walks nested sub-program calls recursively.
/// </summary>
private static Vector ComputeEndPosition(Program pgm, Vector startPos)
{
var pos = startPos;
for (int i = 0; i < pgm.Codes.Count; ++i)
{
var code = pgm.Codes[i];
if (code is SubProgramCall subCall && subCall.Program != null)
{
// Nested sub's frame origin in the caller's frame is startPos + Offset.
pos = ComputeEndPosition(subCall.Program, startPos + subCall.Offset);
continue;
}
if (code is Motion motion)
{
if (pgm.Mode == Mode.Incremental)
pos = pos + motion.EndPoint;
else
pos = startPos + motion.EndPoint;
}
}
return pos;
}
} }
} }
+6 -10
View File
@@ -20,6 +20,9 @@ namespace OpenNest.Converters
private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry) private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List<Entity> geometry)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = program.Mode; mode = program.Mode;
for (int i = 0; i < program.Length; ++i) for (int i = 0; i < program.Length; ++i)
@@ -43,20 +46,13 @@ namespace OpenNest.Converters
case CodeType.SubProgramCall: case CodeType.SubProgramCall:
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
var savedMode = mode; var savedMode = mode;
var savedPos = curpos;
// Position the sub-program at savedPos + Offset. // The sub-program's frame origin in this program's frame is
// savedPos is the base position ((0,0) here, Part.Location in rendering). // frameOrigin + Offset — independent of current tool position.
// Offset is the hole center in drawing-local coordinates. curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry); AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry);
mode = savedMode; mode = savedMode;
// Restore curpos: ConvertMode.ToIncremental skips SubProgramCalls
// when computing deltas, so subsequent incremental codes expect
// curpos to be where it was before the call.
curpos = savedPos;
break; break;
} }
} }
+11 -6
View File
@@ -9,6 +9,12 @@ namespace OpenNest.Controls
{ {
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos, public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
Pen pen, double spacing, float arrowSize) Pen pen, double spacing, float arrowSize)
{
DrawProgram(g, view, pgm, pos, ref pos, pen, spacing, arrowSize);
}
private static void DrawProgram(Graphics g, DrawControl view, Program pgm, Vector basePos, ref Vector pos,
Pen pen, double spacing, float arrowSize)
{ {
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
{ {
@@ -19,10 +25,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = pos; var holeBase = basePos + subpgm.Offset;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y); pos = holeBase;
DrawProgram(g, view, subpgm.Program, ref pos, pen, spacing, arrowSize); DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize);
pos = savedPos;
} }
continue; continue;
} }
@@ -31,7 +36,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos ? motion.EndPoint + pos
: motion.EndPoint; : motion.EndPoint + basePos;
if (code.Type == CodeType.LinearMove) if (code.Type == CodeType.LinearMove)
{ {
@@ -46,7 +51,7 @@ namespace OpenNest.Controls
{ {
var center = pgm.Mode == Mode.Incremental var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos ? arc.CenterPoint + pos
: arc.CenterPoint; : arc.CenterPoint + basePos;
DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize); DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
} }
} }
+34 -58
View File
@@ -395,8 +395,8 @@ namespace OpenNest.Controls
var piercePoint = GetFirstPiercePoint(pgm, part.Location); var piercePoint = GetFirstPiercePoint(pgm, part.Location);
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen); DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
pos = part.Location; pos = piercePoint;
DrawRapids(g, pgm, ref pos, skipFirstRapid: true); DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
} }
} }
@@ -409,15 +409,13 @@ namespace OpenNest.Controls
if (pgm[i] is Motion motion) if (pgm[i] is Motion motion)
{ {
if (pgm.Mode == Mode.Incremental) return motion.EndPoint + partLocation;
return motion.EndPoint + partLocation;
return motion.EndPoint;
} }
} }
return partLocation; return partLocation;
} }
private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false) private void DrawRapids(Graphics g, Program pgm, Vector basePos, ref Vector pos, bool skipFirstRapid = false)
{ {
var firstRapidSkipped = false; var firstRapidSkipped = false;
@@ -425,62 +423,41 @@ namespace OpenNest.Controls
{ {
var code = pgm[i]; var code = pgm[i];
if (code.Type == CodeType.SubProgramCall) if (code is SubProgramCall { Program: { } program } call)
{ {
var subpgm = (SubProgramCall)code; var holeBase = basePos + call.Offset;
var program = subpgm.Program;
if (program != null) if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
{ DrawLine(g, pos, holeBase, view.ColorScheme.RapidPen);
var holePos = new Vector(pos.X + subpgm.Offset.X, pos.Y + subpgm.Offset.Y);
// Draw rapid from current position to hole center var subPos = holeBase;
if (!(skipFirstRapid && !firstRapidSkipped)) DrawRapids(g, program, holeBase, ref subPos);
DrawLine(g, pos, holePos, view.ColorScheme.RapidPen); pos = subPos;
else
firstRapidSkipped = true;
pos = holePos;
DrawRapids(g, program, ref pos);
// Don't restore pos — let it advance so the next hole's
// rapid starts from where this one ended.
}
} }
else else if (code is Motion motion)
{ {
var motion = code as Motion; var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
if (motion != null) if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
{ DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
if (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove) pos = endpt;
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
}
pos = endpt;
}
else
{
if (code.Type == CodeType.RapidMove)
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen);
}
pos = motion.EndPoint;
}
}
} }
} }
} }
private static bool ShouldDrawRapid(bool skipFirstRapid, ref bool firstRapidSkipped)
{
if (skipFirstRapid && !firstRapidSkipped)
{
firstRapidSkipped = true;
return false;
}
return true;
}
private void DrawAllPiercePoints(Graphics g) private void DrawAllPiercePoints(Graphics g)
{ {
using var brush = new SolidBrush(Color.Red); using var brush = new SolidBrush(Color.Red);
@@ -491,11 +468,11 @@ namespace OpenNest.Controls
var part = view.Plate.Parts[i]; var part = view.Plate.Parts[i];
var pgm = part.Program; var pgm = part.Program;
var pos = part.Location; var pos = part.Location;
DrawProgramPiercePoints(g, pgm, ref pos, brush, pen); DrawProgramPiercePoints(g, pgm, part.Location, ref pos, brush, pen);
} }
} }
private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen) private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen)
{ {
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
{ {
@@ -506,10 +483,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code; var subpgm = (SubProgramCall)code;
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = pos; var holeBase = basePos + subpgm.Offset;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y); pos = holeBase;
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen); DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
pos = savedPos;
} }
} }
else else
@@ -519,7 +495,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos ? motion.EndPoint + pos
: motion.EndPoint; : motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove) if (code.Type == CodeType.RapidMove)
{ {
+8 -6
View File
@@ -98,6 +98,9 @@ namespace OpenNest
private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath, private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath,
Program pgm, Mode mode, ref Vector curpos) Program pgm, Mode mode, ref Vector curpos)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = pgm.Mode; mode = pgm.Mode;
for (var i = 0; i < pgm.Length; ++i) for (var i = 0; i < pgm.Length; ++i)
@@ -147,10 +150,8 @@ namespace OpenNest
{ {
cutPath.StartFigure(); cutPath.StartFigure();
leadPath.StartFigure(); leadPath.StartFigure();
var savedPos = curpos; curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos); AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
curpos = savedPos;
} }
mode = tmpmode; mode = tmpmode;
break; break;
@@ -240,6 +241,9 @@ namespace OpenNest
private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos) private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos)
{ {
// Capture the frame origin at entry. Sub-program Offsets are relative
// to this fixed origin, not to the current tool position.
var frameOrigin = curpos;
mode = pgm.Mode; mode = pgm.Mode;
GraphicsPath currentFigure = null; GraphicsPath currentFigure = null;
@@ -308,10 +312,8 @@ namespace OpenNest
if (subpgm.Program != null) if (subpgm.Program != null)
{ {
var savedPos = curpos; curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
AddProgram(path, subpgm.Program, mode, ref curpos); AddProgram(path, subpgm.Program, mode, ref curpos);
curpos = savedPos;
} }
mode = tmpmode; mode = tmpmode;