mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-07 14:15:44 -05:00
fix(geometry): check every hull edge for the minimum bounding rectangle
MinimumBoundingRectangle(Polygon) evaluated only the first (n + 1) / 2
edges, assuming opposite edges give the same rectangle. That holds only
for centrally symmetric hulls: for the triangle (0,0), (10,0), (12,1) it
returned area 12 instead of 10, depending on vertex order.
Every edge is now a candidate. A second-half edge replaces the best only
when smaller beyond rounding, so symmetric hulls (rectangles, best-fit
pairs, whose MBR angle PairEvaluator uses directly) keep their previous
orientation instead of flipping 180 degrees on floating-point noise.
On the Desktop problem nests, 8 of 160 drawing entries now get a smaller
MBR (0.004% to 3.2%) and a turned canonical frame; none changed without a
real area gain. The problem-nest corpus is unchanged against f51e5aef.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0922e41401
commit
059a873d32
2 files changed
+66
-6
No files matched your search
@@ -47,14 +47,16 @@ namespace OpenNest.Geometry
|
||||
return new BoundingRectangleResult(angle, length, 0);
|
||||
}
|
||||
|
||||
// Only need half the edges — opposite edges produce the same
|
||||
// bounding rectangle (width/height swapped), so the minimum
|
||||
// area repeats every 180°.
|
||||
var edgeCount = (n + 1) / 2;
|
||||
// Every edge is a candidate: opposite edges only give the same rectangle
|
||||
// when the hull is centrally symmetric. For those hulls (rectangles,
|
||||
// best-fit pairs) the second half repeats the first turned 180°, so a
|
||||
// second-half edge wins only when smaller beyond rounding; that keeps
|
||||
// their orientation from flipping on floating-point noise.
|
||||
var half = (n + 1) / 2;
|
||||
|
||||
BoundingRectangleResult best = null;
|
||||
|
||||
for (int i = 0; i < edgeCount; i++)
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int next = (i + 1) % n;
|
||||
var edgeX = vertices[next].X - vertices[i].X;
|
||||
@@ -94,8 +96,9 @@ namespace OpenNest.Geometry
|
||||
var width = maxU - minU;
|
||||
var height = maxV - minV;
|
||||
var area = width * height;
|
||||
var margin = i < half || best == null ? 0 : Tolerance.Epsilon * best.Area;
|
||||
|
||||
if (best == null || area < best.Area)
|
||||
if (best == null || area < best.Area - margin)
|
||||
{
|
||||
var edgeAngle = System.Math.Atan2(edgeY, edgeX);
|
||||
best = new BoundingRectangleResult(edgeAngle, width, height);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Geometry;
|
||||
|
||||
public class RotatingCalipersTests
|
||||
{
|
||||
private static readonly Vector[] Triangle = { new(0, 0), new(10, 0), new(12, 1) };
|
||||
|
||||
private static Polygon HullStartingAt(Vector[] vertices, int start)
|
||||
{
|
||||
var hull = new Polygon();
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
hull.Vertices.Add(vertices[(start + i) % vertices.Length]);
|
||||
return hull;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void AsymmetricHull_FindsTheBestEdgeInAnyVertexOrder(int start)
|
||||
{
|
||||
// The edge (12,1)-(0,0) gives area 10; the others give 12 and 50. Starting at
|
||||
// (0,0), that edge is last, in the half the opposite-edge shortcut skipped.
|
||||
var result = RotatingCalipers.MinimumBoundingRectangle(HullStartingAt(Triangle, start));
|
||||
|
||||
Assert.Equal(10, result.Area, 9);
|
||||
Assert.Equal(System.Math.Sqrt(145), result.Width, 9);
|
||||
Assert.Equal(System.Math.Atan2(-1, -12), result.Angle, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointList_FindsTheBestEdgeOfItsHull()
|
||||
{
|
||||
var result = RotatingCalipers.MinimumBoundingRectangle(Triangle);
|
||||
|
||||
Assert.Equal(10, result.Area, 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymmetricHull_KeepsAFirstHalfEdgeOrientation()
|
||||
{
|
||||
// A rotated rectangle's opposite edges tie up to rounding; the result must stay
|
||||
// on the first two edges rather than flip 180° onto the last two.
|
||||
var angle = 0.4;
|
||||
var corners = new[] { new Vector(0, 0), new Vector(30, 0), new Vector(30, 7), new Vector(0, 7) };
|
||||
var hull = new Polygon();
|
||||
foreach (var corner in corners)
|
||||
hull.Vertices.Add(corner.Rotate(angle));
|
||||
|
||||
var result = RotatingCalipers.MinimumBoundingRectangle(hull);
|
||||
|
||||
Assert.Equal(210, result.Area, 6);
|
||||
var firstHalfAngles = new[] { angle, angle + System.Math.PI / 2 }.Select(a => System.Math.Round(a, 9));
|
||||
Assert.Contains(System.Math.Round(result.Angle, 9), firstHalfAngles);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user