Skip to content

Treat a line as tangent to a circle when its closest point is on the circle - #71

Merged
fontanf merged 1 commit into
mainfrom
fix-line-circle-tangency-rounding
Sep 23, 2026
Merged

fontanf merged 1 commit into
mainfrom
fix-line-circle-tangency-rounding

Conversation

@fontanf

@fontanf fontanf commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Fixes the remaining cause of fontanf/packingsolver#595 ("face area is not positive").

Problem

compute_line_circle_intersections collapsed its two computed intersections into one only when they were equal.

At a tangency, they are a double root. The rounding error of the coordinates alone (a few epsilon times their magnitude) moves the line by enough to separate them by about 2 * sqrt(2 * radius * delta). That is several times the point-equality tolerance once the radius exceeds ~30: e.g. ~2e-6 for a radius of ~46 at coordinates of ~100. A tangency was then reported as two proper intersections a few micro-units apart.

Boolean operations split both elements at these two points. Between them, the two pieces differ by ~1e-14, so they are merged, and the arc and the line end up sharing a sub-tolerance edge instead of touching at a point. For a union of two shapes touching at a point, this merges them into a single shape joined through a sub-tolerance neck. The final fix_self_intersections pass then fails on it:

  • with "face area is not positive" (e.g. union 036.json: the outer loop goes around the rectangle the wrong way, and the true outer boundary is left over as a face of area -(total area));
  • or by recursing between fix_self_intersections and compute_union until the stack overflows (union 037.json, see Fix fix_self_intersections/compute_union infinite mutual recursion #68).

In packingsolver#595, the tangent segments are those of the circular arc extras of approximate_by_line_segments's fallback union. With #69 and #70 merged, that fallback is no longer used for the issue's shape, but it is still reachable, and tangencies at an interior point of a segment were mishandled in 36-44% of random cases.

Fix

Each branch of compute_line_circle_intersections now first computes the foot of the perpendicular from the circle's center to the line: the midpoint of the two intersections, whenever there are two. If it is on the circle (point_on_circle, i.e. at the library's own tolerance), the line stays within that tolerance of the circle everywhere between the two intersections, and the foot is returned as the single tangency point. This also covers a tangent line that rounding makes miss the circle by a sub-tolerance amount. The final equal-based collapse is removed.

Behavior change (fontanf/packingsolver#574)

Two genuine crossings between which the line stays within the tolerance of the circle are now reported as a tangency at the foot.

In #574's first intersection test, a line and a radius-4 arc share an endpoint and cross again 0.0023 further on, never more than ~1.6e-7 apart in between. The crossing is now reported as a tangency at the foot, 0.0011 from the shared endpoint, besides the endpoint itself. The expected outputs of that test and of union 033.json are updated. The new union output is simple, non-self-intersecting, and agrees with the previous one within the tolerance (same area up to ~1e-4 out of ~3.57e6).

Reporting only the shared endpoint instead (treating the foot as the same contact as the endpoint) was also tried: it makes 033.json throw "outline area is not positive" again, as it originally did in #574.

Testing

  • New intersection test cases:
    • two tangencies at an endpoint of the line segment (radius ~46, from packingsolver#595);
    • one at an interior point of the segment (radius ~93).
  • New union cases:
    • 034.json and 035.json: the issue's item with its arc's tangent extras;
    • 036.json and 037.json: a disk and a rectangle touching at an interior point of one of its edges.
  • All 7 new cases fail on main; 037.json crashes it (stack overflow).
  • All 491 tests pass.
  • On 120,000 random interior tangencies (coordinates up to 10, 100 and 1,000), none is mishandled, against 36-44% on main.

…circle

compute_line_circle_intersections used to collapse its two computed
intersections into one only when they were equal. At a tangency, they
are a double root, and the rounding error of the coordinates alone is
enough to separate them by several times the point-equality tolerance
once the radius exceeds ~30 (e.g. ~2e-6 for a radius of ~46 at
coordinates of ~100): a tangency was then reported as two proper
intersections a few micro-units apart. Boolean operations split both
elements there, which made them share a sub-tolerance edge instead of
touching at a point, and compute_union failed with "face area is not
positive" or recursed until the stack overflowed
(fontanf/packingsolver#595).

Each branch now first computes the foot of the perpendicular from the
circle's center to the line, the midpoint of the two intersections
whenever there are two. If it is on the circle (point_on_circle), the
line stays within the tolerance of the circle everywhere between the
two intersections, and the foot is returned as the single tangency
point. This also covers a tangent line whose rounding makes it miss the
circle by a sub-tolerance amount.

As a consequence, two genuine crossings between which the line stays
within the tolerance of the circle are reported as a tangency at the
foot, e.g. the crossing 0.0023 from the shared endpoint of a radius-4
arc and a line in fontanf/packingsolver#574: the expected outputs of
that intersection test and of union 033.json are updated accordingly
(the new union output agrees with the previous one within the
tolerance).

New regression tests: two line/arc tangencies at an endpoint of the
line segment and one at an interior point, and the unions 034.json to
037.json.
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