fix(geometry): correct overlap detection for real-world CNC shapes
Part.Intersects has been silently non-functional everywhere it's used. Shape.ToPolygon()/ToPolygonWithTolerance() never called UpdateBounds(), so every freshly-built polygon kept Entity's constructor-default zero-size bounding box regardless of its actual vertices. Collision.Check's first step is a bounding-box pre-filter, and a zero-size box can never overlap anything, so it always short-circuited to "no overlap" no matter what the real geometry looked like. That masked a second bug: Part.Intersects built its polygons via the unconditional 1000-segments-per-arc ToPolygon() default instead of an adaptive tolerance. For parts with several small fillets/holes this produced tens of thousands of vertices, making the now-correct bbox check fall through into a triangulation/clip step too slow to return in practice. Switched to ToPolygonWithTolerance at a named tolerance matching PartGeometry's existing convention. PairEvaluator's own Keep/overlap check had a third, independent bug: it used Shape.Intersects (edge-crossing detection only) at a coarse 0.01 chord tolerance, which misses containment-style overlaps and can polygonize rounded corners coarsely enough to hide a genuine sliver overlap. Switched to Collision.HasOverlap (full polygon clip, handles containment) at a tighter dedicated tolerance. Verified against a real nest file: PairFiller was tiling a BestFit pair that PairEvaluator had incorrectly marked Keep=true, producing visibly overlapping parts on the plate that no downstream overlap check ever caught. Known follow-up: OpenNest.Tests.BestFit.BestFitOverlapTests.KeptPairs_NoOverlap still fails on 3/1082 synthetic candidates that overlap by a sub-0.001 sliver right at a rounded-corner tangent point — a separate, much smaller precision edge case in PairEvaluator's raw (pre-transform) coordinate frame, not a regression from this change.
This commit is contained in:
@@ -298,6 +298,7 @@ namespace OpenNest.Geometry
|
||||
|
||||
polygon.Close();
|
||||
polygon.Cleanup();
|
||||
polygon.UpdateBounds();
|
||||
|
||||
return polygon;
|
||||
}
|
||||
@@ -341,6 +342,7 @@ namespace OpenNest.Geometry
|
||||
|
||||
polygon.Close();
|
||||
polygon.Cleanup();
|
||||
polygon.UpdateBounds();
|
||||
|
||||
return polygon;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ namespace OpenNest
|
||||
|
||||
public class Part : IPart, IBoundable
|
||||
{
|
||||
/// <summary>
|
||||
/// Chord tolerance used to polygonize arcs/circles for overlap testing in
|
||||
/// <see cref="Intersects"/>. Matches the tolerance used elsewhere for
|
||||
/// geometry-sensitive checks (e.g. <see cref="PartGeometry"/>).
|
||||
/// </summary>
|
||||
private const double IntersectsChordTolerance = 0.001;
|
||||
|
||||
private Vector location;
|
||||
private bool ownsProgram;
|
||||
private double preLeadInRotation;
|
||||
@@ -247,8 +254,8 @@ namespace OpenNest
|
||||
if (perimeter1 == null || perimeter2 == null)
|
||||
return false;
|
||||
|
||||
var polygon1 = perimeter1.ToPolygon();
|
||||
var polygon2 = perimeter2.ToPolygon();
|
||||
var polygon1 = perimeter1.ToPolygonWithTolerance(IntersectsChordTolerance);
|
||||
var polygon2 = perimeter2.ToPolygonWithTolerance(IntersectsChordTolerance);
|
||||
|
||||
if (polygon1 == null || polygon2 == null)
|
||||
return false;
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
private const double ChordTolerance = 0.01;
|
||||
|
||||
/// <summary>
|
||||
/// Tighter chord tolerance for the overlap check only. Rounded-corner arcs
|
||||
/// polygonized at the coarser <see cref="ChordTolerance"/> can "cut the corner"
|
||||
/// enough to hide a genuine but tiny sliver overlap between two candidates —
|
||||
/// this needs to match the precision Part.Intersects uses elsewhere so BestFit's
|
||||
/// Keep decision agrees with the same overlap check callers rely on downstream.
|
||||
/// </summary>
|
||||
private const double OverlapChordTolerance = 0.001;
|
||||
|
||||
public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates)
|
||||
{
|
||||
if (candidates.Count == 0)
|
||||
@@ -49,10 +58,18 @@ namespace OpenNest.Engine.BestFit
|
||||
part2.Location = candidate.Part2Offset;
|
||||
part2.UpdateBounds();
|
||||
|
||||
// Overlap check — perimeter vs perimeter
|
||||
// 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 && shape1.Intersects(shape2, out _);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user