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
+11 -6
View File
@@ -9,6 +9,12 @@ namespace OpenNest.Controls
{
public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos,
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)
{
@@ -19,10 +25,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
var savedPos = pos;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
DrawProgram(g, view, subpgm.Program, ref pos, pen, spacing, arrowSize);
pos = savedPos;
var holeBase = basePos + subpgm.Offset;
pos = holeBase;
DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize);
}
continue;
}
@@ -31,7 +36,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
: motion.EndPoint + basePos;
if (code.Type == CodeType.LinearMove)
{
@@ -46,7 +51,7 @@ namespace OpenNest.Controls
{
var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos
: arc.CenterPoint;
: arc.CenterPoint + basePos;
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);
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
pos = part.Location;
DrawRapids(g, pgm, ref pos, skipFirstRapid: true);
pos = piercePoint;
DrawRapids(g, pgm, part.Location, ref pos, skipFirstRapid: true);
}
}
@@ -409,15 +409,13 @@ namespace OpenNest.Controls
if (pgm[i] is Motion motion)
{
if (pgm.Mode == Mode.Incremental)
return motion.EndPoint + partLocation;
return motion.EndPoint;
return motion.EndPoint + 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;
@@ -425,62 +423,41 @@ namespace OpenNest.Controls
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
if (code is SubProgramCall { Program: { } program } call)
{
var subpgm = (SubProgramCall)code;
var program = subpgm.Program;
var holeBase = basePos + call.Offset;
if (program != null)
{
var holePos = new Vector(pos.X + subpgm.Offset.X, pos.Y + subpgm.Offset.Y);
if (ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
DrawLine(g, pos, holeBase, view.ColorScheme.RapidPen);
// Draw rapid from current position to hole center
if (!(skipFirstRapid && !firstRapidSkipped))
DrawLine(g, pos, holePos, view.ColorScheme.RapidPen);
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.
}
var subPos = holeBase;
DrawRapids(g, program, holeBase, ref subPos);
pos = subPos;
}
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 (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove && ShouldDrawRapid(skipFirstRapid, ref firstRapidSkipped))
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
if (code.Type == CodeType.RapidMove)
{
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;
}
}
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)
{
using var brush = new SolidBrush(Color.Red);
@@ -491,11 +468,11 @@ namespace OpenNest.Controls
var part = view.Plate.Parts[i];
var pgm = part.Program;
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)
{
@@ -506,10 +483,9 @@ namespace OpenNest.Controls
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
var savedPos = pos;
pos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen);
pos = savedPos;
var holeBase = basePos + subpgm.Offset;
pos = holeBase;
DrawProgramPiercePoints(g, subpgm.Program, holeBase, ref pos, brush, pen);
}
}
else
@@ -519,7 +495,7 @@ namespace OpenNest.Controls
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
: motion.EndPoint + basePos;
if (code.Type == CodeType.RapidMove)
{
+8 -6
View File
@@ -98,6 +98,9 @@ namespace OpenNest
private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath,
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;
for (var i = 0; i < pgm.Length; ++i)
@@ -147,10 +150,8 @@ namespace OpenNest
{
cutPath.StartFigure();
leadPath.StartFigure();
var savedPos = curpos;
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
curpos = savedPos;
}
mode = tmpmode;
break;
@@ -240,6 +241,9 @@ namespace OpenNest
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;
GraphicsPath currentFigure = null;
@@ -308,10 +312,8 @@ namespace OpenNest
if (subpgm.Program != null)
{
var savedPos = curpos;
curpos = new Vector(savedPos.X + subpgm.Offset.X, savedPos.Y + subpgm.Offset.Y);
curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y);
AddProgram(path, subpgm.Program, mode, ref curpos);
curpos = savedPos;
}
mode = tmpmode;