Skip to content

fix(area): decide segment touching by cross product, not by division - #7692

Merged
DennisOSRM merged 1 commit into
masterfrom
fix-area-intersect-touching
Aug 15, 2026
Merged

fix(area): decide segment touching by cross product, not by division#7692
DennisOSRM merged 1 commit into
masterfrom
fix-area-intersect-touching

Conversation

@DennisOSRM

Copy link
Copy Markdown
Collaborator

The problem

intersect() already means to treat a touch as not a crossing. With std::less_equal it
rejects the parameters at 0 and at 1, so a segment that ends on another does not cross it.
It says this by dividing, and the division does not land on 1.

Whether it does depends on the compiler. For a segment ending exactly on the endpoint of
another, at the scale we store coordinates, the parameter is exactly 1 when multiply-add is
contracted into fma, and 0.99999999999985489385 when it is not:

-ffp-contract=on    t = 1                      touch rejected, correct
-ffp-contract=off   t = 0.99999999999985489385  counted as a crossing

So the same predicate on the same points can answer differently in two translation units of
one build. That is how a unit test on these coordinates passed while the fuzz harness saw
the same line blocked.

The fix

A parameter is exactly 0 or 1 exactly when the matching endpoint lies on the other segment's
line. That is a cross product, not a quotient. It is exactly zero whenever the two
differences it multiplies are zero, which is this whole case, and no rounding or contraction
can move it off zero.

Degenerate cases are now decided by the cross products. Only genuinely interior crossings
are decided by the division. The change is additive: four cross products and a snap.

Why it matters

A wrong answer here deletes a sight line from a visibility graph. Walking along a wall is
allowed, so a line that grazes an obstacle's corner and then runs along its face is real,
and dropping it sends the planner the long way round.

Two places in this repository already work around the same weakness by hand. One of them
says so directly:

intersect() is meant to ignore endpoint hits by itself, but it cannot be relied on

Testing

At the scale plaza coordinates live at, near 1.0 with differences around 1e-6, 543 of 2400
touching configurations were reported as crossings before this change and none are after.
That test fails without the fix, checked by reverting it.

A single hand written case is also included but is deliberately not the guard. Under
contraction it passes either way, so on its own it would keep passing while the predicate
was wrong everywhere else.

Full cucumber suite: 1478 scenarios, 1463 passed, 15 skipped, 0 failed, the same as before
the change. Unit suites for engine, extractor and util all pass.

🤖 Claude Code, Claude Opus 5

intersect() already means to treat a touch as not a crossing: with
std::less_equal it rejects the parameters at 0 and at 1, so a segment ending
on another does not cross it. It says so by dividing, and division does not
land on 1.

Worse, whether it does depends on the compiler. For a segment ending exactly
on the endpoint of another, at the scale the engine stores coordinates, the
parameter comes out as exactly 1 when multiply-add is contracted into fma and
as 0.99999999999985489385 when it is not. The same predicate on the same
points answers differently in two translation units of one build.

A parameter is exactly 0 or 1 precisely when the corresponding endpoint lies
on the other segment's line, and that is a cross product rather than a
quotient: it is exactly zero whenever the two differences it multiplies are,
which is the whole of this case, and no rounding or contraction can move it
off zero. So the degenerate cases are decided by the cross products and only
the genuinely interior ones by the division.

The cost of getting this wrong is a sight line deleted from a visibility
graph. Walking along a wall is allowed, so a line that grazes an obstacle's
corner and continues along its face is real, and dropping it sends the
planner the long way round. Two places in this repository already work around
the same weakness by hand, one of them noting that intersect() "is meant to
ignore endpoint hits by itself, but it cannot be relied on".

Measured near 1.0 with differences around 1e-6, which is where plaza
coordinates live, 543 of 2400 touching configurations were reported as
crossings before this and none are after. A single hand written case is kept
but is deliberately not the guard, because under contraction it passes either
way and would go on passing while the predicate was wrong everywhere else.
Copilot AI lite review requested due to automatic review settings August 15, 2026 21:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes the extractor::area::intersect() predicate deterministic for “touching at endpoints” cases by avoiding reliance on floating-point division landing exactly on 0/1, preventing compiler-dependent endpoint touches from being misclassified as proper crossings (which can incorrectly remove visibility edges in area routing).

Changes:

  • Snap the intersection parameters to 0/1 when an endpoint lies exactly on the other segment’s supporting line (via cross products), and only rely on the quotient for non-degenerate cases.
  • Add regression tests covering both a concrete real-world coordinate case and a larger deterministic grid of lon/lat-scale “T-touch” configurations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
include/extractor/area/util.hpp Makes intersect() robust to FMA/contraction differences by snapping endpoint-touch parameters using cross products before endpoint-exclusive comparisons.
unit_tests/extractor/area/util.cpp Adds targeted and grid-based unit tests ensuring endpoint touches are never reported as crossings at relevant coordinate scales.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.74%. Comparing base (551f3f0) to head (f0d440a).

Files with missing lines Patch % Lines
unit_tests/extractor/area/util.cpp 94.28% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7692      +/-   ##
==========================================
- Coverage   94.74%   94.74%   -0.01%     
==========================================
  Files         519      519              
  Lines       41534    41582      +48     
==========================================
+ Hits        39353    39398      +45     
- Misses       2181     2184       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DennisOSRM
DennisOSRM enabled auto-merge (squash) August 15, 2026 21:43
@DennisOSRM
DennisOSRM disabled auto-merge August 15, 2026 21:43
@DennisOSRM
DennisOSRM merged commit 9229f17 into master Aug 15, 2026
24 checks passed
@DennisOSRM
DennisOSRM deleted the fix-area-intersect-touching branch August 15, 2026 21:43
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.

2 participants