Files
OpenNest/OpenNest.Tests/Geometry/RotatingCalipersTests.cs
T
ajandClaude Opus 5.5 059a873d32 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>
2026-10-20 21:41:27 -04:00

58 lines
2.0 KiB
C#

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);
}
}