feat(editor): scrub-editing and drag-target slots in the Attributes pane (#212)#259
Merged
Conversation
…ane (#212) Numeric attributes can now be edited by dragging their NAME, and asset-reference attributes by dropping a Library item on a slot. Discovery found the transaction model already in place: the panel's edits push edit::Command, and the preview-session pattern already delivers one-undo-entry gestures. So this adds the two widgets and binds them to that machinery rather than inventing anything. ScrubLabel is a pure gesture source: it knows pixels and modifiers, never units, values, or commands. PropertiesPanel maps its delta onto each attribute's units through a LOCAL descriptor table — local because it is the only consumer, and a shared registry would be speculative until a second one appears. One gesture = one preview session = one undo entry; a drag that ends where it started, or is cancelled with Esc, leaves the undo stack untouched. The multiplier applies to the motion made WHILE it is held rather than to the running total, so tapping Shift mid-drag refines from where the value has reached instead of jumping. SlotWidget resolves and commits nothing — it republishes the dropped key and lets its consumer decide what the key means. The first consumer is the prop Model slot, which commits a new kernel factory edit::set_object_model: it re-points Object::name (the prop_library id the mesher renders) and refreshes radius/height from the new model, so the declared volume never describes the model the prop used to be. That is public kernel API, so the nanobind binding and place_objects.py move with it. Selecting a prop used to show its owning ROAD's fields; it now shows the prop's own, with its OpenDRIVE @type named. Also extracts active_node_height(), which the Height spin box and its scrub binding both need, and adds LibraryPanel::focus_category so an engaged slot can send the user to the right category — clearing the search box first, since a stale filter would hide the target. Refs #212
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.
Third sprint of Pillar P1 — Interaction & Navigation (epic #250). Independent of #210/#211, so it branches from
main(rebased on the merged p1-s1).Closes #212
What this adds
Scrub-editing — drag a numeric attribute's name and the value follows the cursor, live.
⇧fine (×0.1),⌘/Ctrlcoarse (×10),Esccancels. On lane Width, Mark width, elevation Height, and signal s / t / Heading offset.Slots — a framed drop target for asset references: takes a Library drag with a hover affordance, click to jump to its Library category. First consumer: the prop Model slot.
Discovery is why this is small
The transaction model was already there: the panel's edits push
edit::Command, and the preview-session pattern (begin_preview/update_preview/commit_preview/cancel_preview) already gives one-undo-entry gestures. So this binds two new widgets to existing machinery instead of inventing a mechanism.Design calls worth reviewing
ScrubLabelis a pure gesture source — pixels and modifiers, never units, values, or commands. The panel maps its delta onto each attribute's units through a local descriptor table. Local on purpose: it is the only consumer, and a shared registry is speculative until a second one appears.⇧goes down. Tapping⇧mid-drag refines from where the value has actually reached.SlotWidgetresolves and commits nothing. It republishes the dropped key; the consumer decides what a key means. That is what lets the same widget serve props now and materials in P6 without changing.edit::set_object_modelre-pointsObject::name— the prop_library id the mesher renders — and refreshes radius/height from the new model, so a pine's bounding volume can't survive on a shrub. Public kernel API, sobindings.cpp+place_objects.pymove with it (repo policy).Unplanned but needed
@type(§13.1).LibraryPanel::focus_categoryclears the search box first — a stale filter would otherwise hide the very category the slot is sending you to.active_node_height()— the Height spin box and its scrub binding must agree on what "current" means; it was inline in the spin box's commit path.Visual change
Panel renders (offscreen
QWidget::grab, macOS, graphite theme). GitHub attachments aren't reachable from the CLI, so they're described rather than embedded — reproduce with apanel.grab().save(...)in an offscreen gtest:Prop selected (new section):
Lane selected — unchanged in appearance: Width and Mark width read exactly as before; the only difference is a horizontal-resize cursor over their names. The empty 48 px preview box was dropped from the slot after the first render — with no thumbnail it read as a broken image.
Tests
test_scrub_label.cpp(8): slop, running delta, direction, both multipliers, the while-held rule, Esc, right-button.test_slot_widget.cpp(7): the MIME contract, non-library and empty payloads refused, hover affordance appears/clears, engage, placeholder.test_panels.cpp(+5): one-undo-entry per gesture, Esc restores the baseline byte-identically, net-zero pushes nothing, the Model slot commits/undoes, an unknown model is refused without an undo entry.test_library_panel.cpp(+3),test_object_ops.cpp(+3: retarget/revert byte-identical, dirty set, bad input leaves the network untouched),test_edit.py(+2).A test caught a real thing: the first version picked
section->lanes.front()— the centre lane, which carries no<width>records. The panel correctly refuses to scrub it (baseline()returns nullopt); the test was wrong and segfaulted onwidths.front(). The fixture now picks a width-bearing lane.Verification
ci-macos(-Werror); whole-treeclang-format --dry-run --Werrorclean.place_objects.pyruns and prints the swap (command-path tree is now a shrub (r=1.10 m)), with the undo restoring the oak.Acceptance
GW-2 step 9 (scrub path); GW-3 steps 6–7 and 10 slot mechanics (their real consumers land in P4/P6).