Skip to content

feat(core): junction regeneration learns turn-set changes (p2-s2) - #270

Merged
JArmandoAnaya merged 4 commits into
mainfrom
p2-s2-junction-regen-turn-sets
Jul 16, 2026
Merged

feat(core): junction regeneration learns turn-set changes (p2-s2)#270
JArmandoAnaya merged 4 commits into
mainfrom
p2-s2-junction-regen-turn-sets

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Closes #263. Highest-risk sprint in P2; gates #218 (Lane Carve).

What this does

regenerate_junction refused any turn-set change (twice — connection count, then per-turn key) and the editor swallowed the failure into a warning toast, so adding a lane to a road feeding a junction left it stale until a manual delete-and-recreate. That is exactly the case GW-2 step 12's turn lane needs. This teaches regeneration to add, drop, and rewrite connections in place.

The dirty-set contract (junctions_are_current)

Discovery found the M2 restriction was not where the issue assumed: add_lane set topology = true but never populated .junctions, so the editor's regen loop iterated an empty list. And DirtySet::topology was doing double duty — "roads/junctions added or removed" (a view concern) and "this command already handled its own junctions, don't regenerate" (a correctness concern). Those coincided only because every topology command so far also built junctions; a lane appearing is topology and needs regeneration, which one flag cannot say.

So DirtySet gains junctions_are_current, set by the commands that build or tear down junction structure (create/delete junction, split_road, delete_road). The editor's regen loop keys off it instead of topology. Default false fails safe: a command that forgets it gets a redundant regeneration (slow, correct), never a stale junction (fast, wrong).

The regeneration restructure

regenerate_junction is rebuilt around a creator. It partitions the freshly planned turns against the existing connection table into matched / new / dropped (keyed by the incoming/outgoing road+contact+lane, so a re-ordered plan is not a changed turn set). Matched turns keep their connecting-road ids; new turns get fresh connecting roads via a factored-out materialize_connection; dropped turns are erased with everything they own (sections, lanes, and any objects/signals anchored to them — erase_road_exact does not cascade).

A TurnSetPolicy parameter keeps the per-frame node-drag preview in-place-only: a preview command is reverted and destroyed every frame, and Arena::erase_exact reserves a slot rather than recycling it, so creating connecting roads there would leak arena slots for the rest of the session. The drag keeps today's behaviour (stale junction + toast); lane edits, which are not previewed, regenerate normally. (The general preview-session leak is filed as a separate issue gating #218.)

insert_lane

Interior lane insert with renumbering + link remap, moved here from #262 — Lane Form and Lane Carve need a lane that starts partway across the carriageway, which add_lane (outermost-only) cannot author. Since odr id is the lateral position, an interior insert renumbers the outer block and remaps everything that named those lanes by id (adjacent-section links, junction lane_links); the inserted lane is unlinked from its neighbours (asam new_lane_appear).

Verification

  • 864 ctest (was 854) + 99 pytest green; ASan+UBSan clean on the edit/lane/junction/network suite; soak PASS on seeds 1/2/7 with the new insert_lane op exercised.
  • The no-op guard RegenerateJunctionIsByteEqualWhenNothingChanged stays green and was strengthened to also assert a clean revert.
  • New tests cover: turn set grows / shrinks (undo resurrects the exact ids), survivors keep their ids, InPlaceOnly still refuses, add_lane names its junctions without claiming they are current, insert_lane renumber / unlinked / neighbour-remap / junction-lane_link-remap / rejections, and the editor path (add_lane on an arm regenerates with no regeneration_skipped toast). The grow and shrink tests were mutation-checked to confirm they bite.

🤖 Generated with Claude Code

regenerate_junction refused any turn-set change and the editor swallowed
the failure into a warning toast, so adding a lane to a road feeding a
junction left it stale until a manual delete-and-recreate. Teach it to
add, drop, and rewrite connections instead — the case GW-2 step 12's turn
lane needs.

- DirtySet gains junctions_are_current: the flag that says a command
  already brought its junctions up to date, split out from topology so a
  lane appearing can be topology AND need regeneration. The editor's regen
  loop keys off it instead of topology.
- add_lane, remove_lane, set_lane_type declare junctions_touching so the
  editor knows which junctions to regenerate.
- regenerate_junction is restructured around a creator: it partitions the
  plan into matched/new/dropped turns, keeps the ids of the turns that
  survive (keyed matching), materializes the ones that appeared, and
  erases the ones that vanished with everything they own. A TurnSetPolicy
  parameter keeps the per-frame drag path in-place-only, where creating
  connecting roads would leak arena slots on every discarded frame.
- The regen loop now merges a regeneration's own topology into the dirty
  set, so a dropped connecting road prunes stale selections.

Author: Armando Anaya <armando@robomous.ai>
Lane Form and Lane Carve need a lane that starts partway across the
carriageway, which add_lane (outermost-only) cannot author. insert_lane
places a lane at a given odr id and renumbers every lane already at or
outside it one step further out.

Because odr id IS the lateral position, an interior insert necessarily
renumbers, and everything that named the shifted lanes by id is remapped
rather than cleared: adjacent-section predecessor/successor links (the
writer refuses a dangling one in either direction) and junction
lane_links. The inserted lane is a new lane, so it does not link back to
the neighbouring sections (asam new_lane_appear). All of it runs inside
the command creator, so apply/revert stays byte-identical.

Also exposes it through the Python bindings and the soak driver, adds the
TurnSetPolicy enum and junctions_are_current to the bindings, and amends
the frozen M2 editing-tools doc with the turn-set-change decision.

Author: Armando Anaya <armando@robomous.ai>
…y scope

GCC -Werror=shadow (Linux only; the macOS clang preset does not flag it)
rejected the creator's road/here locals shadowing the factory-scope ones.
@JArmandoAnaya JArmandoAnaya mentioned this pull request Jul 16, 2026
34 tasks
@JArmandoAnaya
JArmandoAnaya merged commit ac46d29 into main Jul 16, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the p2-s2-junction-regen-turn-sets branch July 16, 2026 08:44
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.

p2-s2: junction regeneration learns turn-set changes

1 participant