Skip to content

OpenDRIVE round-trip: keep left lanes on the left, regenerate lateral edits surgically - #105

Merged
kosuke55 merged 5 commits into
mainfrom
fix/odr-roundtrip-fidelity
Aug 22, 2026
Merged

OpenDRIVE round-trip: keep left lanes on the left, regenerate lateral edits surgically#105
kosuke55 merged 5 commits into
mainfrom
fix/odr-roundtrip-fidelity

Conversation

@kosuke55

@kosuke55 kosuke55 commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two fixes for how edited roads regenerate on OpenDRIVE export.

Left-side lanes — a bundle made entirely of imported left lanes (positive lane ids) used to come back as <right> lanes of a reversed reference line: lane-id signs flipped (+2 → -2) and the road's s coordinate ran backwards. The exporter now keeps these bundles on the <left> side with the original reference direction. A sign bug in the closest-point width fallback (unsigned distance rejected left-side intersections) is fixed along the way.

Surgical (lateral-only) regeneration — editing one boundary point used to regenerate the whole road: reference line refit, laneOffset dropped, elevation coarsened, s drifting over the full length. When every moved point's displacement is lateral (no s-direction component), the exporter now keeps the original planView / laneOffset / elevationProfile / signals / objects verbatim and rewrites only the <width> records, recomputed against the original reference line. Anything else — longitudinal drags, structural changes, ambiguous cases — falls back to full regeneration. A geometry-free semantic hash per road distinguishes the two.

Verification

  • 335 tests passing (7 new regression pins: lane-id sign, reference direction, links, surgical keep/fallback)
  • Unedited round trips stay 100% verbatim on all bundled fixtures
  • On the soderleden fixture, a lateral edit keeps width, length, laneOffset and elevation; a longitudinal drag falls back to full regeneration

…l reference direction

Bundles made entirely of imported left lanes (positive odr_lane_id) used to
regenerate as <right> lanes of a reversed reference line: lane-id signs
flipped (+2 -> -2) and s ran backwards (s_new = L - s_orig). Such bundles now
keep the boundary polylines in original reference-line order, emit their
lanes on the <left> side (+1..+n outward), and orient the unsigned
closest-point fallback of normalOffsets to the negative-t side so widths
survive. Road/lane links and junction connecting-road contact points are
derived from the lane-id signs (a left road's travel exit is its geometric
start), matching the importer's travel-direction semantics.
Add hashRoadSemantics() and store a per-road semanticHash in the sidecar
record alongside the existing full stateHash. It hashes only the road's
non-geometric state (lane attributes, connectivity, right-of-way and the
regulatory shapes, with all boundary / stop-line point sequences dropped).

When the full hash changes but the semantic hash still matches, the edit
touched only boundary geometry -- the precondition the exporter needs to
decide an edit is a candidate for lateral-only (surgical) regeneration.
A carry-through edit that moves boundary points only across the travel
direction changes nothing but the lane widths: the road's plan view,
laneOffset, elevation profile, signals, objects and links are all still
valid. The fitting exporter regenerated the whole road anyway, rebuilding
the reference line from the leftmost boundary -- dropping the original
laneOffset, shifting s over the road length, coarsening the elevation
profile and dropping signals / objects.

buildSurgicalRoad() instead keeps the original <road> element verbatim and
rewrites only the lane <width> records, recomputed from the edited
boundaries measured along the original reference line + laneOffset. It runs
only when (a) the semantic hash still matches (no attribute / connectivity /
regulatory change) and (b) every edited boundary point still projects onto
the reference line at its original station (no longitudinal drift) with its
inner boundary on the accumulated datum. Anything else -- a point dragged
along the road, a moved reference/center edge, a skipped micro section, a
changed vertex count -- fails the check and the road regenerates fully, so
the surgical path can never emit wrong widths.

planCarryThrough attempts the surgical rebuild for each edited road; a
success keeps the road clean (its lanes stay out of full regeneration and
join the connectivity maps as external endpoints) and emits the
width-rewritten text through the same verbatim path (link rewriting intact).
- lateral edit on a real curved road with a laneOffset (soderleden road 0)
  keeps its length / plan-view primitive count / laneOffset / elevation
  records, leaves every other road byte-verbatim, and reproduces the moved
  point on re-import
- longitudinal edit on the same road falls back to full regeneration
- unedited round trip stays fully verbatim (surgical never fires)

Also switch the existing junction-regeneration test's edit from a lateral
nudge to a longitudinal drag: a lateral nudge of an outgoing road is now
handled surgically (road + junction stay verbatim), so forcing full
regeneration -- the behaviour that test pins -- needs a non-lateral edit.
@kosuke55 kosuke55 added the run-tests Trigger CI test execution label Aug 21, 2026
@kosuke55
kosuke55 force-pushed the fix/odr-roundtrip-fidelity branch from 90b3084 to 379dac6 Compare August 22, 2026 00:13
@kosuke55
kosuke55 merged commit 4a0e136 into main Aug 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-tests Trigger CI test execution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant