fix(engine): check BestFit overlap in the same frame BuildParts places

PairEvaluator checked overlap on the raw candidate geometry before
applying the pair's OptimalRotation, but BestFitResult.BuildParts (and
everything downstream) rotates both parts by -OptimalRotation before
placing them. Re-tessellating a rotated arc at the overlap chord
tolerance samples different chord points than rotating an
already-tessellated polygon, so a few tangent-corner candidates came
out overlap-free in the raw frame but overlapping once actually
placed.

Move the landscape-normalization step before the overlap check and
rotate part1/part2 the same way BuildParts does before tessellating
and running Collision.HasOverlap, so Keep agrees with the geometry
that's actually placed. Fixes
OpenNest.Tests.BestFit.BestFitOverlapTests.KeptPairs_NoOverlap (was
failing 3/1082 candidates).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-22 09:57:40 -04:00
co-authored by Claude Sonnet 5
parent b14cb9d10a
commit 0512f3f84a
+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,