From c122e64eddcbbbbc866bdcff5de8d5ca46061310 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 2 Nov 2026 12:34:14 -0500 Subject: [PATCH] fix(verify): resolve adjacent curves before any native query runs A fillet whose circle is tangent inside the arc it continues, as a keyhole's slot ends are, gives the supporting-circle query of the pair no real root: the circles' distance is the radii's difference up to rounding, so the intersection comes back NaN, and the unfiltered check that runs before any adjacency handling refused the whole part as "nonfinite program coordinates". On the production corpus 17 plates failed this way. Adjacent curves are now resolved from their shared vertex before the native query would run, so no query runs for them. Co-Authored-By: Claude Fable 5.1 --- .../Diagnostics/PostVerificationGeometry.cs | 90 ++++++++++--------- .../Diagnostics/AdjacentCurveContactTests.cs | 21 +++++ docs/cutting-planner.md | 4 +- 3 files changed, 71 insertions(+), 44 deletions(-) diff --git a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs index 81855229..acde64d9 100644 --- a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs +++ b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs @@ -351,33 +351,53 @@ internal static class PostVerificationGeometry internal IReadOnlyList Contacts(Curve other, out bool overlap) { - var entity = nativeEntity.Value; - var candidate = other.nativeEntity.Value; - // Native arc filters discard NaN supporting-circle intersections. Inspect - // both unfiltered queries first: roundoff can differ by operand direction. - // Coincident supports have separate overlap handling below. - if (Center is { } center && other.Center is { } otherCenter && !SameSupport(other)) - { - var support = new Circle(center, Radius); - var otherSupport = new Circle(otherCenter, other.Radius); - support.Intersects(otherSupport, out var forward); - otherSupport.Intersects(support, out var reverse); - foreach (var point in forward) - Validate(point); - foreach (var point in reverse) - Validate(point); - } + var same = SameSupport(other); + // Two curves that meet end to end share that vertex, and the native query is + // ill-conditioned there: circles meeting at a shallow angle, or a line meeting its own + // fillet, put their root up to 1e-5 off the joint, which no joint tolerance can tell + // from a cut that touches beside it, and a fillet's circle tangent inside the arc it + // continues gives the supporting-circle query no root at all. Both shapes are known, + // so their other intersection, if there is one, follows from the shared vertex + // exactly instead, and no native query runs for the pair. + var joint = same ? null : SharedVertex(other); List points; - bool intersects; - switch (candidate) + if (joint is { } vertex) { - case Line line: intersects = entity.Intersects(line, out points); break; - case Arc arc: intersects = entity.Intersects(arc, out points); break; - case Circle circle: intersects = entity.Intersects(circle, out points); break; - default: throw new NotSupportedException("Unsupported native boundary."); + points = [vertex]; + if (SecondIntersection(other, vertex) is { } second && second.DistanceTo(vertex) > Epsilon + && Contains(second) && other.Contains(second) + && !IsOutsideArcSpan(second) && !other.IsOutsideArcSpan(second)) + points.Add(second); + } + else + { + var entity = nativeEntity.Value; + var candidate = other.nativeEntity.Value; + // Native arc filters discard NaN supporting-circle intersections. Inspect + // both unfiltered queries first: roundoff can differ by operand direction. + // Coincident supports have separate overlap handling below. + if (Center is { } center && other.Center is { } otherCenter && !same) + { + var support = new Circle(center, Radius); + var otherSupport = new Circle(otherCenter, other.Radius); + support.Intersects(otherSupport, out var forward); + otherSupport.Intersects(support, out var reverse); + foreach (var point in forward) + Validate(point); + foreach (var point in reverse) + Validate(point); + } + bool intersects; + switch (candidate) + { + case Line line: intersects = entity.Intersects(line, out points); break; + case Arc arc: intersects = entity.Intersects(arc, out points); break; + case Circle circle: intersects = entity.Intersects(circle, out points); break; + default: throw new NotSupportedException("Unsupported native boundary."); + } + if (!intersects) + points.Clear(); } - if (!intersects) - points.Clear(); // Coincident circles produce NaNs in the native discrete-contact query. // Their support overlap is handled separately, without inventing crossings. // Supports that agree only within Epsilon (arcs of one circle whose centers were @@ -386,32 +406,16 @@ internal static class PostVerificationGeometry // still refuse, while arcs that only meet at a joint or lie apart are not overlap. var witness = Center is { } c && other.Center is { } oc && (c.X != oc.X || c.Y != oc.Y || Radius != other.Radius) ? RaySeamTolerance : Epsilon; - overlap = SameSupport(other) && (InteriorWitness(Midpoint, other, witness) + overlap = same && (InteriorWitness(Midpoint, other, witness) || InteriorWitness(other.Midpoint, this, witness) || InteriorWitness(Start, other, witness) || InteriorWitness(End, other, witness) || InteriorWitness(other.Start, this, witness) || InteriorWitness(other.End, this, witness)); - if (SameSupport(other)) + if (same) points.Clear(); - // Two curves that meet end to end share that vertex, and the native query is - // ill-conditioned there: circles meeting at a shallow angle, or a line meeting its own - // fillet, put their root up to 1e-5 off the joint, which no joint tolerance can tell - // from a cut that touches beside it. Both shapes are known, so their other - // intersection, if there is one, follows from the shared vertex exactly instead. - var joint = SameSupport(other) ? null : SharedVertex(other); - var adjacent = joint.HasValue; - if (joint is { } vertex) - { - points.Clear(); - points.Add(vertex); - if (SecondIntersection(other, vertex) is { } second && second.DistanceTo(vertex) > Epsilon - && Contains(second) && other.Contains(second) - && !IsOutsideArcSpan(second) && !other.IsOutsideArcSpan(second)) - points.Add(second); - } foreach (var point in points) Validate(point); // Analytic contacts of adjacent curves are certain; the ray guard below is for native queries. - var nativeContactCount = adjacent ? int.MaxValue : points.Count; + var nativeContactCount = joint.HasValue ? int.MaxValue : points.Count; foreach (var point in new[] { Start, End, other.Start, other.End }) if (Contains(point) && other.Contains(point) && !points.Exists(p => p.DistanceTo(point) <= Epsilon)) diff --git a/OpenNest.Tests/Diagnostics/AdjacentCurveContactTests.cs b/OpenNest.Tests/Diagnostics/AdjacentCurveContactTests.cs index fbacad25..20b19680 100644 --- a/OpenNest.Tests/Diagnostics/AdjacentCurveContactTests.cs +++ b/OpenNest.Tests/Diagnostics/AdjacentCurveContactTests.cs @@ -42,6 +42,27 @@ public class AdjacentCurveContactTests Assert.Equal(vertex, Assert.Single(reversed)); } + // A fillet whose circle is tangent inside the arc it continues, as a keyhole's slot ends + // are: the supporting circles' intersection has no real root (their distance is the + // radii's difference up to rounding), which the native query reports as NaN. + [Theory] + [InlineData(1.125, 0.0625, 1.3761, 2.12)] + [InlineData(1.1, 0.0625, 0, 0)] + [InlineData(0.9375, 0.03125, 10.5, -3.25)] + [InlineData(2.3, 0.1, 100.001, 7)] + public void FilletTangentInsideTheArcItContinues_TouchesOnlyAtTheirVertex(double radius, double fillet, double x, double y) + { + var center = new Vector(x, y); + var vertex = center + new Vector(radius, 0); + var filletCenter = center + new Vector(radius - fillet, 0); + var small = Arc(filletCenter + new Vector(0, fillet), vertex, filletCenter, clockwise: true); + var large = Arc(vertex, center + new Vector(0, radius), center); + + Assert.Equal(vertex, Assert.Single(small.Contacts(large, out var overlap))); + Assert.Equal(vertex, Assert.Single(large.Contacts(small, out _))); + Assert.False(overlap); + } + [Fact] public void LineMeetingItsFilletEndToEnd_TouchesOnlyAtTheirVertex() { diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index c986823f..f7eca5e2 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -71,7 +71,9 @@ end (within the check epsilon, 1e-8) are not put to the native intersection quer all: it is ill-conditioned at such a joint (two circles meeting at a shallow angle, as a fillet continuing into a larger arc, or a line meeting its fillet), and lands its root up to 1e-5 off the vertex, which no joint tolerance can tell from a cut that touches beside -the joint. `PostVerificationGeometry.Curve.Contacts` reports the shared vertex itself and +the joint; a fillet whose circle is tangent inside the arc it continues, as a keyhole's slot +ends are, gives the supporting-circle query no real root at all, which read as nonfinite +coordinates and refused the whole part. `PostVerificationGeometry.Curve.Contacts` reports the shared vertex itself and derives the pair's other intersection from it (two circles meet again at the vertex's mirror image across the line through their centers; a line meets a circle again at the far end of its chord), keeping it only when it lies on both finite spans. Every consumer of contacts