Skip to content

feat(editor): scrub-editing and drag-target slots in the Attributes pane (#212)#259

Merged
JArmandoAnaya merged 1 commit into
mainfrom
p1-s3-attributes-scrub
Jul 15, 2026
Merged

feat(editor): scrub-editing and drag-target slots in the Attributes pane (#212)#259
JArmandoAnaya merged 1 commit into
mainfrom
p1-s3-attributes-scrub

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

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), /Ctrl coarse (×10), Esc cancels. 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

  • ScrubLabel is 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.
  • The multiplier applies to the motion made while held, not to the running total. Scaling the total would make the value jump the instant goes down. Tapping mid-drag refines from where the value has actually reached.
  • A net-zero drag is not an edit — it cancels rather than committing, so a scrub that wanders out and back leaves no undo entry.
  • SlotWidget resolves 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.
  • The Model slot commits a kernel factory, not an editor macro (approved decision). edit::set_object_model re-points Object::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, so bindings.cpp + place_objects.py move with it (repo policy).

Unplanned but needed

  • Selecting a prop showed its owning ROAD's fields. There was no prop section at all, so the Model slot had nowhere to live. Added one, naming the object's OpenDRIVE @type (§13.1).
  • LibraryPanel::focus_category clears the search box first — a stale filter would otherwise hide the very category the slot is sending you to.
  • Extracted 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 a panel.grab().save(...) in an offscreen gtest:

Prop selected (new section):

OpenDRIVE id  object s1
Position      s 12.00 m, t 5.00 m
┌─ Prop ───────────────────────┐
│ Tree                         │
│ Model  [ tree_pine        ]  │   ← drop target, click to browse
└──────────────────────────────┘

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

  • NEW test_scrub_label.cpp (8): slop, running delta, direction, both multipliers, the while-held rule, Esc, right-button.
  • NEW 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 on widths.front(). The fixture now picks a width-bearing lane.

Verification

  • 796/796 green on ci-macos (-Werror); whole-tree clang-format --dry-run --Werror clean.
  • ASan + UBSan: the new editor and core suites clean.
  • Python: 79 passed / 1 skipped; place_objects.py runs 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).

…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
@JArmandoAnaya
JArmandoAnaya merged commit 1bb088c into main Jul 15, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the p1-s3-attributes-scrub branch July 15, 2026 20:57
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.

p1-s3: Attributes pane — scrub-editing + drag-target slot infrastructure

1 participant