fix(core): stop Program.BoundingBox including the origin

Min/max were seeded at 0, so any geometry not touching the origin got an
inflated box, and the first move only updated max (else-if). Rotated
canonical drawings are the common trigger: their origin ends up outside
the shape, which skewed Part bounds and bbox-based alignment.

Track the real extents and keep returning a zero box for empty programs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-21 11:07:44 -04:00
co-authored by Claude Sonnet 5
parent f23f0fa566
commit 2a855139d2
2 changed files with 75 additions and 12 deletions
+25 -12
View File
@@ -305,22 +305,27 @@ namespace OpenNest.CNC
return new Vector(0, 0);
}
/// <summary>
/// Bounding box of the geometry the program visits. The tool's starting position is not
/// part of the geometry, so the origin only contributes when the program reaches it.
/// An empty program returns a zero-size box at the origin.
/// </summary>
public Box BoundingBox()
{
var origin = new Vector(0, 0);
return BoundingBox(ref origin);
return BoundingBox(ref origin, out var box) ? box : new Box(0, 0, 0, 0);
}
private Box BoundingBox(ref Vector pos)
private bool BoundingBox(ref Vector pos, out Box result)
{
// 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 minY = 0.0;
double maxX = 0.0;
double maxY = 0.0;
var minX = double.PositiveInfinity;
var minY = double.PositiveInfinity;
var maxX = double.NegativeInfinity;
var maxY = double.NegativeInfinity;
for (int i = 0; i < Codes.Count; ++i)
{
@@ -338,12 +343,12 @@ namespace OpenNest.CNC
if (pt.X > maxX)
maxX = pt.X;
else if (pt.X < minX)
if (pt.X < minX)
minX = pt.X;
if (pt.Y > maxY)
maxY = pt.Y;
else if (pt.Y < minY)
if (pt.Y < minY)
minY = pt.Y;
pos = pt;
@@ -361,12 +366,12 @@ namespace OpenNest.CNC
if (pt.X > maxX)
maxX = pt.X;
else if (pt.X < minX)
if (pt.X < minX)
minX = pt.X;
if (pt.Y > maxY)
maxY = pt.Y;
else if (pt.Y < minY)
if (pt.Y < minY)
minY = pt.Y;
pos = pt;
@@ -470,7 +475,8 @@ namespace OpenNest.CNC
// 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 (!subpgm.Program.BoundingBox(ref pos, out var box))
break;
if (box.Left < minX)
minX = box.Left;
@@ -489,7 +495,14 @@ namespace OpenNest.CNC
}
}
return new Box(minX, minY, maxX - minX, maxY - minY);
if (minX > maxX || minY > maxY)
{
result = new Box(0, 0, 0, 0);
return false;
}
result = new Box(minX, minY, maxX - minX, maxY - minY);
return true;
}
public object Clone()
@@ -0,0 +1,50 @@
using OpenNest.CNC;
using OpenNest.Geometry;
namespace OpenNest.Tests.CNC;
public class ProgramBoundingBoxTests
{
[Fact]
public void GeometryAwayFromOrigin_DoesNotIncludeOrigin()
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new RapidMove(new Vector(10, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 10)));
pgm.Codes.Add(new LinearMove(new Vector(20, 30)));
pgm.Codes.Add(new LinearMove(new Vector(10, 30)));
pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
var box = pgm.BoundingBox();
Assert.Equal(10, box.Left, precision: 6);
Assert.Equal(10, box.Bottom, precision: 6);
Assert.Equal(20, box.Right, precision: 6);
Assert.Equal(30, box.Top, precision: 6);
}
[Fact]
public void GeometryBelowAndLeftOfOrigin_IsTrackedExactly()
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new RapidMove(new Vector(-30, -20)));
pgm.Codes.Add(new LinearMove(new Vector(-10, -20)));
pgm.Codes.Add(new LinearMove(new Vector(-10, -5)));
var box = pgm.BoundingBox();
Assert.Equal(-30, box.Left, precision: 6);
Assert.Equal(-20, box.Bottom, precision: 6);
Assert.Equal(-10, box.Right, precision: 6);
Assert.Equal(-5, box.Top, precision: 6);
}
[Fact]
public void EmptyProgram_ReturnsZeroBox()
{
var box = new OpenNest.CNC.Program().BoundingBox();
Assert.Equal(0, box.Width, precision: 6);
Assert.Equal(0, box.Length, precision: 6);
}
}