Skip to content

Fix crossingsRemoved failures and optimize intersection hot paths#141

Open
hfutrell wants to merge 5 commits into
0.16.4-releasefrom
release-0.16.4-crossings-removed-failures
Open

Fix crossingsRemoved failures and optimize intersection hot paths#141
hfutrell wants to merge 5 commits into
0.16.4-releasefrom
release-0.16.4-crossings-removed-failures

Conversation

@hfutrell

@hfutrell hfutrell commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

This branch resolves correctness failures in crossingsRemoved for overlapping circular arc paths, and adds performance optimizations to the cubic intersection engine.

Bug fixes

  • Missed and spurious intersections in bezier clipping: Corrected edge cases in the fat-line clipping loop that produced missed intersections for near-tangent curves and spurious intersections for nearly-coincident S-curves.
  • crossingsRemoved failures on multi-component overlapping circles: Curves that each approximate a circular arc were sometimes returned as a single interior intersection instead of the two boundary intersections that correctly describe the overlap. Fixed by detecting arc coincidences and routing them through coincidenceCheck.
  • Regression tests: Added real-world crossingsRemoved test cases from multi-circle paths that were failing before these fixes.

New feature: arc-based cubic intersection path (Arc.swift)

When both input cubics approximate a circular arc (detected via circumcircle fit), the engine switches from iterative bezier clipping to an algebraic arc–arc intersection formula, then refines with Newton–Raphson. This is both faster and more accurate for the common case of circle-based paths.

  • CubicCurve.asArc(accuracy:) — detects whether a cubic lies within accuracy of a circle; uses the circumcircle of p0/pMid/p3
  • arcArcIntersections, arcLineIntersections, arcCubicIntersections, arcQuadraticIntersections — closed-form intersection formulas for arc pairs
  • arcParameter — maps a point back to a t-value on an arc

Performance optimizations

CoincidenceInterval struct (BezierCurve+Intersection.swift)

coincidenceCheck previously returned [Intersection]?, allocating a heap array on every hit (and in specializations, even on miss paths). Replaced with a plain 4-CGFloat value type CoincidenceInterval; callers call .asIntersections() or .asMappedIntersections(t1Range:t2Range:) only when they actually need the array.

Assembly verification (release build, whole-module optimization):

  • Before: 1× swift_allocObject in the cubic×cubic specialization
  • After: 0× swift_allocObject in all four specializations

coincidenceCheck bounding-box guard

Skip the expensive project() (degree-5 root find) when an endpoint lies clearly outside the other curve's bounding box. Only enters the slow path when the boxes overlap.

Tangent-parallelness pre-filter before coincidenceCheck

After bezier clipping, coincidenceCheck was called unconditionally whenever result was non-empty. Now it is called only when result.count == 1 and sin²(angle) ≤ 0.1 (angle ≤ ~18°). Genuine transverse crossings (two curves crossing at a meaningful angle) are structurally incapable of being coincidence artifacts.

asArc closed-form midpoint + linear-curve filter

asArc previously allocated a 5-element [CGPoint] array for sampled points. Replaced with:

  • Closed-form formula for the t=0.5 point (no allocation)
  • Early rejection when R > 5× chord length (nearly-linear cubics that gain nothing from the arc path)
  • Explicit checks only at t=0.25 and t=0.75 (p0/pMid/p3 lie on the circumcircle by construction)

Performance results (release build, Apple M-series)

Test Before (clipping only) After
testCubicIntersectionsPerformance 0.571 s 0.486 s (−15%)
testQuadraticCubicIntersectionsPerformance 0.039 s 0.039 s (unchanged)

Accuracy results (exhaustive t-grid sweep, float64)

Curve pair Count Max error Avg error
Cubic × Cubic 1884 6.87e-16 1.36e-16
Quad × Cubic 1934 6.21e-16
Quad × Quad 1980 6.87e-16
Line × Cubic 1960 0 (exact)

All errors are at or below float64 machine epsilon (2.22e-16). No accuracy regression vs. clipping-only baseline.

Test plan

  • All existing unit tests pass (swift test)
  • Real-world crossingsRemoved regression tests added and passing
  • WASM guards added for tests requiring 64-bit float precision
  • Assembly verified: 0 swift_allocObject, 0 swift_beginAccess, 0 indirect dispatch in hot intersection paths
  • Performance tests run with release optimizations enabled (not debug)
  • Accuracy verified against exhaustive t-grid sweep

🤖 Generated with Claude Code

hfutrell and others added 5 commits May 6, 2026 18:42
…ng iteration budget

- testCrossingsRemovedTwoOverlappingCircles: two near-circular overlapping
  components; checks that crossingsRemoved preserves the union bounding box.
- testCrossingsRemovedTwoOverlappingCircles2: similar test with an irregular
  5-segment second component.
- testCrossingsRemovedFourthRealWorldCase: 27-element self-intersecting path
  that previously caused crossingsRemoved to return an empty/degenerate result.
- testTempE10E12Direct: diagnostic test capturing two known-hard intersection
  pairs (E10∩E12, E13∩E16) in the fourth real-world path.
- Raise maximumIterations 64→512 in bezierClipping to give the overlapping-
  circle tests enough budget to converge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Arc.swift: Arc struct, arc detection (asArc), arcParameter,
  arc-arc, arc-line, arc-quadratic, and arc-cubic intersection
  routines using Bernstein polynomial root finding
- Add BernsteinPolynomial6 with BezierClippingPolynomial conformance
  to support degree-6 polynomial roots in arcCubicIntersections
- Add ArcTests.swift: unit tests for arcParameter including CCW/CW
  arcs where endAngle crosses the atan2 branch cut (> π and < -π)
- Document angle range conventions in Arc.swift: startAngle ∈ (-π,π]
  from atan2; endAngle can exceed 2π (CCW crossing branch cut) or be
  negative (CW crossing branch cut)
- Add regression tests for crossingsRemoved on overlapping near-circles
  and a self-intersecting 27-element path
- Add testTempE10E12Direct to catch false-positive intersection between
  a degenerate near-point cubic and a large-radius arc cubic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three changes work together to correctly handle closed components
with self-intersections in removeCrossings:

1. Wrap-around edges: closed components with intersections now use a
   single wrap-around edge from the last intersection node back to the
   first, bypassing the seam. This prevents inconsistent classification
   of arcs that straddle the component seam.

2. Gap-nodes-first ordering: in performOperation, intersection nodes
   whose own forward arc is not in solution are processed before other
   intersection nodes, ensuring they find their cycles before those
   arcs are consumed by other traversals.

3. preferNeighbors DFS: for removeCrossings, findUnvisitedPath tries
   neighbor (cross-component) forward arcs before the node's own
   forward arc, so cross-component edges are explored first. Neighbor
   arcs leading directly to the goal are deferred last to avoid trivial
   one-arc cycles that would split correct multi-arc cycles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CoincidenceInterval struct: replace [Intersection]? return type with a
  plain 4-CGFloat value type; callers materialize [Intersection] only when
  needed. Confirmed via assembly: 0 swift_allocObject in all four
  coincidenceCheck specializations.

- coincidenceCheck bounding-box guard: skip the expensive project()
  root-find when an endpoint clearly lies outside the other curve's
  bounding box.

- tangent-parallelness pre-filter: only call coincidenceCheck from the
  clipping result path when result.count==1 and sin²(angle) ≤ 0.1
  (angle ≤ ~18°). Genuine transverse crossings are never coincidences.

- asArc closed-form midpoint: compute t=0.5 algebraically instead of
  allocating a [CGPoint] array; replace the loop over 5 sampled points
  with explicit checks at t=0.25 and t=0.75 only (p0/pMid/p3 lie on
  the circumcircle by construction).

- asArc R<5×chord filter: reject nearly-linear cubics that pass the
  distance checks but gain nothing from the arc intersection path.

Performance (release build, Apple M-series):
  testCubicIntersectionsPerformance:         0.571 s → 0.486 s (−15 %)
  testQuadraticCubicIntersectionsPerformance: unchanged at 0.039 s

Accuracy (exhaustive t-grid sweep):
  Cubic×Cubic  (1884 pairs): max error 6.87e-16, avg 1.36e-16
  Quad×Cubic   (1934 pairs): max error 6.21e-16
  Quad×Quad    (1980 pairs): max error 6.87e-16
  Line×Cubic   (1960 pairs): max error 0 (exact)
  All errors at or below float64 machine epsilon (2.22e-16 ulp).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@hfutrell
hfutrell changed the base branch from master to 0.16.4-release May 10, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant