feat(editor): junctions follow an arm live during a node drag#208
Merged
Conversation
Dragging a junction arm's node moved the arm while its connecting roads stayed put, snapping into place only on release. They now follow the arm continuously. move_waypoint_following_junctions composes the move and each touched junction's regeneration into ONE command via the existing CompositeCommand, whose lazy stages already see the network with earlier stages applied — exactly what regen-after-move needs. That keeps the preview session's one-command contract and its snapshot-at-creation semantics intact instead of reaching around them: regeneration was previously reachable only from push_applied_with_regeneration, which update_preview never calls and push_command refuses outright during a session. That, not the regeneration_skipped signal named in the issue, is why this never worked — that signal reports a changed turn set and has nothing to do with drags. No throttle. The issue and my own plan assumed 12 clothoid fits per frame would be too expensive to do live; measured on the 4-arm scene it is 0.09 ms for the regeneration and 3.85 ms mean / 5.74 ms worst for a whole frame including re-mesh, against a 16 ms budget. A debounce would have been complexity bought with a guess. Foreign junctions (no recorded arms) are skipped rather than refused, so an unrelated imported junction cannot fail a drag. The composite is atomic, so a failed regeneration rejects the frame and the last good preview stands — close to unreachable anyway, since regeneration only fails on a changed turn set and a node drag moves geometry, not lanes. Document::commit_preview takes already_regenerated, so committing a drag no longer regenerates a second time: byte-identical output, but a wasted re-plan and an extra child in the undo entry. Closes #156
…ld path The mid-drag assertion is by contrast: a plain move_waypoint previewed on the same drag BREACHES the weld (the connector keeps its old heading while the arm swings), and the following move does not. Asserting only that the welds are clean would have passed even if nothing regenerated. Dragging node 0 swings the arm's far end, so its contact point barely moves and its heading changes — my first attempt asserted the connector tip moved and failed at 1.8e-15, because the tip is welded to a contact point that stays put. The gap is angular, not positional.
CI runs clang-format over every tracked file; `git clang-format` only checks what is uncommitted, so it reported clean after the commit and the violations only surfaced in CI. node_drag's preview call is an if-style lambda rather than the formatter's wrapped ternary, which was unreadable.
Contributor
Author
|
14/14 green on Held for you rather than self-merged: every other PR in this sprint landed on green CI under the merge-as-you-go directive, but this one changes drag behaviour in both the Select and Edit Nodes tools, and #156 was explicitly scoped out of the release gate on 2026-07-13 — so it's the one item where "green" isn't the whole question. It's yours to take or leave; nothing else depends on it. Two things worth a second look if you do review it:
|
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 #156. Dragging a junction arm's node moved the arm while its connecting roads stayed put, snapping into place only on release. They now follow continuously.
The issue named the wrong blocker
regeneration_skippedhas nothing to do with drags — it is a semantic signal, raised when a turn set or connection count changes (operations.cpp:2504,:2543).The real blocker was structural: regeneration lived only inside
push_applied_with_regeneration, reachable frompush_commandandcommit_preview(document.cpp:159,:305) but never fromupdate_preview— andpush_commandis hard-refused during a session (document.cpp:145-148). Mid-drag frames only ever re-meshed. Nothing was broken; the code path simply did not exist.No throttle — the cost premise was wrong
My plan (and the issue's framing) assumed 12 clothoid fits per frame was too expensive to do live, and prescribed an
AutosaveManager-style debounce. I measured it on the 4-arm scene before writing any of it:~3× headroom. The throttle would have been complexity bought with a guess, plus a fake-clock test to maintain. Dropped.
Design
move_waypoint_following_junctionscomposes the move and each touched junction's regeneration into one command using the existing file-localCompositeCommand, whose lazy stages already see the network with earlier stages applied — precisely what regen-after-move needs, and howattach_t_junctionalready works. So the preview session keeps its one-command contract and its snapshot-at-creation semantics rather than being worked around.Two judgement calls worth flagging:
regenerate_junctiontreats an empty arm list as an error. Letting that abort a composite would mean an unrelated imported junction could fail your drag; skipping matches what the commit path already does (leaves it stale).Document::commit_previewgainsalready_regeneratedso committing a drag does not regenerate a second time. The second pass was byte-identical, but it cost a re-plan and landed an extra child in the undo entry.Tests
JunctionRegen.ConnectingRoadsFollowTheArmMidDragNotOnlyOnReleaseproves it by contrast: on the same drag, a plainmove_waypointpreview leaves the weld breached (connector holds its old heading while the arm swings); the following move leaves it clean, with the session still open. Asserting "welds are clean mid-drag" alone would have passed even if nothing regenerated — so the test asserts the old path fails first.Worth recording: my first version asserted the connector's tip moved, and failed at
1.8e-15. Dragging node 0 swings the arm's far end — the junction-facing contact point barely moves and the heading changes. The tip is welded to a point that stays put; the gap is angular, not positional. The test was wrong, not the code.JunctionRegen.LiveFollowCommitsExactlyOneUndoEntrycovers the flag: one undo entry, byte-identical undo.Checks
ci-macosclean (-Werror); 750/750 ctest green.PreviewSessioninvariants still pass —CancelledSessionIsByteIdentical,CommitPushesExactlyOneEntryAndUndoRestoresBase,PushCommandIsRefusedWhileActive,FailedUpdateKeepsLastGoodStateAndSession.Known, deliberately not fixed here
An objects/junction-dirty frame emits
mesh_changed({})— a wholesale viewport rebuild — becausepartialrequiresdirty.junctionsto be empty (document.cpp:330-333). Live follow now hits that every frame. It fits the budget on GS-1-sized scenes, but making it partial needsmesh_changedto carry junction ids, a signal-signature change that does not belong in a polish PR. Happy to file it.Closes #156