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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Fable 5.1 committed 2026-11-02 14:31:45 -05:00
1 parent 60d235c8ce
commit c122e64edd
3 files changed
+71 -44

No files matched your search

@@ -351,33 +351,53 @@ internal static class PostVerificationGeometry
internal IReadOnlyList<Vector> 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<Vector> 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))
@@ -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()
{
+3 -1
View File
@@ -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