fix(mesh): pave the arc a road curving through a junction actually describes (#356) - #559
Merged
Merged
Conversation
…scribes (#356) A junction splits its through road into two arms, and the corner solver extrapolated each arm's facing edge as an infinite straight ray from a single end station. Two collinear edges never meet, so `parallel_edges` caught the straight case and paved a corridor; on a curve the rays met at a fabricated apex metres outside the pavement and the fillet built there paved out to it. Measured at r = 30 m: the apex sat 7.18 m past the true edge, and the 15 m fillet dipped 4.08 m past it. The curved case is now detected the way the straight one always was. When two arms' facing edges share an osculating circle they are one pavement edge running through the junction, so there is no corner to fillet and the mesher paves the arc corridor the edge describes. The arc is anchored through BOTH face corners rather than to either arm's osculating circle: the arms are clothoid-fitted, so those circles differ by centimetres and anchoring to one leaves a sliver at the other. The corridor's inward offset is signed, because "inward" is toward the arc centre only for a convex through-arm. A straight arm has zero edge curvature, so the solve stays exactly the line-line one it has always been. All seven straight-approach fixtures mesh bit-identically, verified by a before/after differential of every fixture and pinned from here on by a golden generated from the pre-fix solver. `junction_corners()` already drops solves it cannot fillet, so a through pair simply reports no corner — as a straight through-road always did — and the Corner tool needs no change. An authored radius on such a pair is ignored, exactly as it always was for a straight one. The reproduction is no longer disabled: `r30_sidewalk_inside` joins the quality matrix, and new gates assert the floor lands on the pavement edge the curve describes and that no corner is reported between a road and itself. Worth recording: without sidewalks the spike was there too and every gate passed it, because a spike is a convex corner and the fillet gate exempts those. Only the sidewalk band wrapped it into a concave needle the matrix could see — which is why the new gates are geometric rather than quality metrics. Closes #356
…e by line The gate failed on macOS arm64 while passing on x86_64 Linux, Windows and macOS — with the SAME vertices in a different sequence. Mesh vertex order is not stable across architectures and never was a kernel guarantee: the determinism tests pin it only within a single run. A golden that compares the dump line by line is therefore asserting something the mesher does not promise. Resolve each index triple to coordinates, sort the corners within a triangle and the triangles within a case, and compare that. Geometry and connectivity — what actually must not move — stay pinned; vertex numbering no longer is. The committed golden is unchanged, since it already encodes the same triangles. Re-verified by sabotage: kFootprintWeld 0.01 -> 0.012 still fails the gate.
…g them Sorting canonical triangles and comparing the lists pairwise is not robust: floor coordinates carry float32-precision noise (~2e-6 m at these magnitudes, one float ULP), and a single ULP is enough to swap two neighbours in the sort. Every later triangle then reports as a metres-wide mismatch, which is exactly how this read on the Windows runner -- one true 1.9e-6 difference followed by a cascade of bogus 8 m ones. Match the two triangle multisets with a tolerance instead. 1e-4 m is 20x above the float noise and 50x below kBoundarySimplify, so it cannot hide a real change. Sorting still canonicalises WITHIN a triangle, where the corners are metres apart and no rounding can reorder them. Re-verified by sabotage: kFootprintWeld 0.01 -> 0.012 leaves 25 of 68 golden triangles in `asymmetric` with no counterpart.
17 tasks
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.
Closes #356.
Diagnosis first — all four suspected causes were wrong
The issue's standing note predicted the fillet was never emitted and that
solve_cornerreturned early through one of four guards. None of themtrips. Every corner in the failing fixture solves
valid = true:ta/tbare positive and large,phiis mid-range,max_radiushas 34.8 mof headroom, arm ordering is correct. Bound relaxation is not just unnecessary
— relaxing any of these could only make the artifact bigger.
What is actually wrong
Pair 0→1 is the two halves of the same road, split by the junction. Their
facing edges are one continuous pavement edge of radius 34.80 m centred
(0, 30). Extrapolated as straight rays 112° apart, they meet at (0, −11.978)
— while the true edge crosses x = 0 at (0, −4.800). That is 7.18 m of
fabricated pavement, and the 15 m fillet built on it dips to (0, −8.877).
The straight case works only because two collinear edges never meet:
parallel_edgescatches it and paves a corridor instead. The curved case isthat case's exact analogue and was never handled.
Why every gate missed it for so long
The failing boundary marches out along the arm's edge line to (−7.382, −4.827),
turns 88.210° back to the fillet tangency, then follows the 15 m arc. Those
two features are 1.800 m apart — exactly the sidewalk width. The floor mesh is
the carriageway, inset 1.8 m by the #402 bands; the bands wrap the corridor
strips but not the fillet wedge, so the wedge pokes proud and the boundary
doubles back.
Without sidewalks the spike is still there —
r30_insidecarries the apex at(0, −12.699) in its boundary, with
concave = 0.000— and passes everygate, because a spike is a convex corner and the fillet gate exempts convex
corners by design. The sidewalk band is the only reason the matrix could ever
see this. That is why the new gates are geometric rather than quality metrics.
The fix
Detect the curved case the way the straight one is detected — by the two
facing edges being the same curve — and pave the arc corridor instead of a
corner. Two details that are not incidental:
osculating circle. The arms are clothoid-fitted, so their osculating circles
differ by centimetres; anchoring to one left a 5.7 cm miss at the other,
which is exactly the sliver class this removes.
convex through-arm; on the concave side the pavement is on the far side.
junction_corners()already drops solves it cannot fillet, so a through pairnow reports no corner — as a straight through-road always did — and the Corner
tool needs no change. Consequence worth flagging: an authored radius on a
through pair is now ignored, exactly as it always was for a straight one.
Straight approaches are bit-identical
Verified by dumping every junction floor for all 12 fixtures before and after:
A straight arm has zero edge curvature, so the solve provably stays the
line-line one. Pinned from here on by
straight_floor_golden.txt, generatedfrom the pre-fix solver so it proves the fix reproduces
9e9bce2rather thanagreeing with itself. Positions compare to 1e-6 m (one file, three platforms,
and clothoid/Clipper2/CDT need not agree to the last ULP — a micron is three
orders of magnitude below
kBoundarySimplify); indices compare exactly..gitattributespins iteol=lf, the third instance of that byte-gate class.Verification
editor tests, zero findings.
millimetre (−4.8000 against a true −4.800; was −8.877).
green, confirming it pins only straight output
kFootprintWeld0.01 → 0.012 → golden fails on the first vertexOne sabotage (
kEdgeStripWidth1.0 → 1.01) caught nothing. That is a vacuoussabotage rather than a weak test — the strips are interior to the union for
straight junctions, so the constant genuinely cannot move that output; the
kFootprintWeldsabotage above is the one that proves the golden bites.A first draft also carried a concave-orientation gate. Sabotage showed it could
not fail with any available fixture — the concave corridor's overhang stays
inside the mouth — so it was dropped rather than shipped as coverage that isn't
there, and the detection's concave path is covered by running
NoCornerIsReportedForTheRoadRunningThroughover both orientations instead.