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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user