Merge branch 'fix/bestfit-overlap-test'

Fixes BestFit overlap check to agree with the geometry BuildParts
actually places, resolving BestFitOverlapTests.KeptPairs_NoOverlap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-22 09:58:46 -04:00
co-authored by Claude Sonnet 5
+30 -14
View File
@@ -58,19 +58,6 @@ namespace OpenNest.Engine.BestFit
part2.Location = candidate.Part2Offset;
part2.UpdateBounds();
// Overlap check — perimeter vs perimeter. Uses Collision.HasOverlap (full polygon
// clip) rather than Shape.Intersects (edge-crossing only), which misses containment-
// style overlaps where one perimeter's boundary never crosses the other's.
var shape1 = GetPerimeterShape(part1);
var shape2 = GetPerimeterShape(part2);
var overlaps =
shape1 != null
&& shape2 != null
&& Collision.HasOverlap(
shape1.ToPolygonWithTolerance(OverlapChordTolerance),
shape2.ToPolygonWithTolerance(OverlapChordTolerance)
);
// Convex hull vertices from perimeter polygons only
var allPoints = GetPartVertices(part1);
allPoints.AddRange(GetPartVertices(part2));
@@ -106,7 +93,9 @@ namespace OpenNest.Engine.BestFit
var trueArea = candidate.Drawing.Area * 2;
// Normalize to landscape (width >= height) for consistent display.
// Normalize to landscape (width >= height) for consistent display. Do this before
// the overlap check so bestRotation already matches the final OptimalRotation that
// BuildParts will apply.
if (bestHeight > bestWidth)
{
var tmp = bestWidth;
@@ -115,6 +104,33 @@ namespace OpenNest.Engine.BestFit
bestRotation += Angle.HalfPI;
}
// Overlap check — perimeter vs perimeter, in the same final orientation BuildParts
// uses downstream. Uses Collision.HasOverlap (full polygon clip) rather than
// Shape.Intersects (edge-crossing only), which misses containment-style overlaps
// where one perimeter's boundary never crosses the other's. Checking pre-rotation
// geometry here (rather than rotating part1/part2 first, matching BuildParts) would
// tessellate arcs at a different orientation than the geometry actually gets placed
// with, letting tangent-corner slivers slip through in one frame but not the other.
if (!bestRotation.IsEqualTo(0))
{
var pairBounds = (
(IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }
).GetBoundingBox();
var center = pairBounds.Center;
part1.Rotate(-bestRotation, center);
part2.Rotate(-bestRotation, center);
}
var shape1 = GetPerimeterShape(part1);
var shape2 = GetPerimeterShape(part2);
var overlaps =
shape1 != null
&& shape2 != null
&& Collision.HasOverlap(
shape1.ToPolygonWithTolerance(OverlapChordTolerance),
shape2.ToPolygonWithTolerance(OverlapChordTolerance)
);
return new BestFitResult
{
Candidate = candidate,