fix(cutting): widen arc extents to their native contact band

Review of the extent filter (bfe2e51) found it could skip checks the
native queries would have flagged. ContactAfterStart counts a point as
touching an arc while its squared distance from the centre is within
1e-8 x max(1, 2r) of r^2, which for small arcs reaches well past the
radius (up to 1e-4 as r -> 0), more than the filter's margin. A rapid
0.000002 outside a radius-0.001 circle lost its crossing, the pre-post
review lost the same finding, and a lead grazing a radius-0.003 circle
turned from an uncertain check into a clear one.

An arc's extent now uses that contact reach, computed from the same
slack expression as ContactAfterStart, so every point a native query
can count as contact lies inside it. The three cases above are
regression tests.
This commit is contained in:
aj committed 2026-10-06 00:03:00 -04:00
1 parent 2419a9e2e3
commit c1c8d5fc36
2 files changed
+96 -4

No files matched your search

@@ -106,11 +106,13 @@ internal static class PostVerificationGeometry
: new Line(Start, End);
/// <summary>
/// A conservative axis-aligned extent: an arc's whole supporting circle, a line's endpoints.
/// Nonfinite geometry yields NaN bounds, which no separation test can pass.
/// A conservative axis-aligned extent of everything a native query may count as touching
/// this curve: for an arc, its whole supporting circle widened by the contact band of
/// <see cref="ContactAfterStart"/>; for a line, its endpoints. Nonfinite geometry yields NaN
/// bounds, which no separation test can pass.
/// </summary>
internal Extent Extent => Center is { } center
? new(center.X - Radius, center.Y - Radius, center.X + Radius, center.Y + Radius)
? new(center.X - ContactReach, center.Y - ContactReach, center.X + ContactReach, center.Y + ContactReach)
: new(System.Math.Min(Start.X, End.X), System.Math.Min(Start.Y, End.Y),
System.Math.Max(Start.X, End.X), System.Math.Max(Start.Y, End.Y));
@@ -246,6 +248,13 @@ internal static class PostVerificationGeometry
point.X - Center.Value.X)) <= System.Math.Abs(Sweep) + Epsilon / Radius
|| point.DistanceTo(Start) <= Epsilon || point.DistanceTo(End) <= Epsilon;
// How far past r^2 a squared distance from the centre still counts as touching the arc.
// For small arcs this band reaches well beyond the radius (sqrt(1e-8) = 1e-4 as r -> 0).
private double ContactSlack => Epsilon * System.Math.Max(1, Radius * 2);
// The largest distance from the centre that ContactAfterStart can count as contact.
private double ContactReach => System.Math.Sqrt(Radius * Radius + ContactSlack);
internal bool ContactAfterStart(Vector origin, Vector direction, double length)
{
if (Center is { } center)
@@ -256,7 +265,7 @@ internal static class PostVerificationGeometry
var projection = Dot(relative, direction);
var perpendicular = Cross(relative, direction);
var square = Radius * Radius - perpendicular * perpendicular;
if (square < -Epsilon * System.Math.Max(1, Radius * 2))
if (square < -ContactSlack)
return false;
var offset = System.Math.Sqrt(System.Math.Max(0, square));
return Hit(projection - offset) || Hit(projection + offset);