fix(engine): the visibility graph dropped every edge running along a wall - #7691
Merged
Conversation
…wall visible_vertices() decides whether a candidate segment runs through the interior by testing its midpoint with inside_area(), which is strict about the boundary. Ray casting on a point lying exactly on an edge answers arbitrarily, and the midpoint of two adjacent ring vertices is always on the ring between them, so the commonest pair there is was read as leaving the area. The walls were therefore missing from the graph, and a planner with no edge along a wall cannot route past an obstacle, only around it. On a random corpus of plazas, 1096 of 9114 clear vertex pairs were absent and one portal to portal path came out 2.65 times the straight line, bending at two plaza corners, where the correct path is 1.28 times and hugs the obstacle. This affects the geodesic and area snapping at query time. The midpoint test now accepts a point on the boundary. The extractor's own visibility sweep is a different algorithm and does not share the defect: it omits ring edges deliberately and AreaMesher puts them back with with_ring_edges(), so the stored mesh has its walls. Adds the tests that would have caught it. Every existing test asked from a point in the interior, and the callers ask from vertices. Two now ask along a wall, and one checks the converse property the suite was missing: the tests here said no reported line crosses an obstacle, which bounds the graph from one side only and is satisfied by an oracle that reports nothing at all. Its clearance oracle uses orientation predicates that share no code with intersect(), since an oracle built on the code under test agrees with it whatever it says. Also corrects geodesic_threads_a_one_metre_gap, whose assertion encoded the defect. It required the path to be at least half as long again as the straight line, which is what the solver produced when it could not turn at the two corners of the wall's western face in succession and had to leave by a longer route: 894 m where the true geodesic is 630 m. The number was measured from the behaviour rather than derived. It now names the two corners the path has to turn at, which is what threading the gap means.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a correctness bug in the engine-side area visibility graph where edges that run exactly along polygon boundaries (“walls”) could be incorrectly dropped due to strict midpoint containment checks, which in turn caused geodesic routing to take unnecessarily long detours.
Changes:
- Treat midpoints that lie on any ring boundary as valid (not “outside”) when determining segment visibility in
visible_vertices(). - Add targeted unit tests for wall-adjacent visibility and a stronger “completeness” property test to catch missing clear edges.
- Update one geodesic unit test assertion to validate the intended corner-turning behavior rather than relying on a length bound that encoded the old defect.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/engine/area_visibility.cpp | Adjusts visibility blocking logic to tolerate boundary midpoints by detecting “on ring” cases with a small tolerance. |
| unit_tests/engine/area_visibility.cpp | Adds new regression/property tests covering vertex-on-wall visibility and completeness of reported clear lines. |
| unit_tests/engine/area_geodesic.cpp | Replaces a defect-preserving path-length assertion with checks that the path turns at the expected wall corners and remains near-optimal. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #7691 +/- ##
==========================================
- Coverage 94.73% 91.83% -2.91%
==========================================
Files 519 519
Lines 41435 41534 +99
==========================================
- Hits 39252 38141 -1111
- Misses 2183 3393 +1210 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
visible_vertices()decides whether a candidate segment runs through the interior bytesting its midpoint with
inside_area(), which is strict about the boundary. Ray castingon a point lying exactly on an edge answers arbitrarily, and the midpoint of two adjacent
ring vertices is always on the ring between them, so the commonest pair there is was read
as leaving the area.
The walls were therefore missing from the graph, and a planner with no edge along a wall
cannot route past an obstacle, only around it.
What it cost
Measured over a random corpus of plazas:
corners, where the correct path is 1.28 times and hugs the obstacle.
This affects the geodesic and area snapping at query time.
The extractor is not affected
VisibilityGraph::visible_verticesin the extractor is a rotational sweep, a differentalgorithm with no midpoint test. It omits ring edges deliberately and
AreaMesherputsthem back with
with_ring_edges(), so the stored mesh has its walls.Tests
Every existing test in
area_visibility.cppasks from a point in the interior. The callersask from vertices, which is where the defect lives, so two new tests ask along a wall.
The third is the property the suite was missing. The existing tests say that no reported
line crosses an obstacle, which bounds the graph from one side only and is satisfied by an
oracle that reports nothing at all. The converse, that every clear line is reported, is what
finds a missing edge. Its clearance oracle uses orientation predicates that share no code
with
intersect(), because an oracle built on the code under test agrees with it whateverit says.
All three fail on master and pass here.
Cost
The geodesic gets slower, by about 24% on
area-geodesic-bench: at 104 vertices, 6485 usto 8043 us for graph plus Dijkstra, and 40 vertices goes from 567 us to 700 us, still
inside the roughly 1 ms budget.
GEODESIC_MAX_VERTICESis unchanged.That is the cost of doing work that was being skipped rather than an inefficiency in the
fix. Wall-adjacent pairs used to be rejected by the midpoint test before the crossing test
ran on them at all, and there are now 12% more edges for the search to relax.
One existing assertion changed
geodesic_threads_a_one_metre_gaprequired the path to be at least half as long again asthe 600 m straight line. That was the length the solver produced when it could not turn at
the two corners of the wall's western face in succession and had to leave by a longer
route: 894 m, where the true geodesic is 630 m. The number was measured from the behaviour
rather than derived, so it held the defect in place.
It now names the two corners the path has to turn at, which is what threading the gap
means.
🤖 Claude Code, Claude Opus 5