Commit b2dbf9f
authored
Support mid-drag modifier changes (#733)
* Support live modifier switching mid-drag
Previously a scene-node drag froze its (button, modifier) combo at
drag-start: changing the held modifier mid-gesture had no effect. This
partitions a physical drag into one logical segment per combo. When the
held modifier changes, the client ends the current segment and -- if the
new combo is bound -- starts a fresh one under it, preserving the grab
geometry (plane, grab point, instance) so the drag continues without a
visual jump; only the addressed callback set changes.
Modifier changes are picked up from both pointermove and (for stationary
switches) window keydown/keyup during the drag. A switch to an unbound
combo drops the gesture into a dormant gap (no messages) so callbacks see
properly paired start/end per bound combo; re-entering a bound combo
starts a new segment.
The wire message already carries `modifier`, so this is client-side
segmentation plus docs -- no message-schema change. `SceneNodeDragEvent`
keeps a single `modifier` field that now identifies the active segment.
Tests: planModifierTransition unit cases (vitest), a server-side
multi-segment dispatch/bookkeeping test, and an e2e test that adds Shift
mid-drag and asserts the cmd/ctrl segment ends before release.
* Add e2e coverage for dormant-then-resume drag
Covers the gap surfaced in review: switching to an unbound modifier combo
mid-drag must suspend the gesture (dormant), not end it. Asserts the
active segment ends on the switch (before release), and that re-entering
a bound combo resumes with a fresh segment within the same button press --
which only holds if dormant is a suspend rather than a teardown. Both
transitions are driven by key events with the pointer stationary, so this
also exercises the keydown/keyup path.
* Add multi-stage drag coverage (many switches + dormant cycles)
Existing tests each exercised a single modifier switch. These add
sequences within one drag:
- server: one drag cycling cmd/ctrl -> cmd/ctrl+shift -> cmd/ctrl ->
cmd/ctrl+shift (re-entering both combos), asserting each callback sees
clean ordered start...end pairs and the active-drag map fully releases.
- e2e: one continuous press cycling cmd/ctrl -> cmd/ctrl+shift ->
dormant -> cmd/ctrl+shift -> cmd/ctrl, asserting exactly two segments
per combo across repeated switching, re-entry, and resume-after-dormant.
Uses async callbacks so the counters settle deterministically.
* Prettier-format dragUtils.test.ts (CI eslint gate)
* Honor pre-promotion modifier changes; read drag bindings live at switches
Two review findings on the mid-drag modifier-switch mechanism:
1. The opening segment is now attributed to the modifier held on the
promoting pointermove, not the one sampled at pointerdown. The drag
layer's key listeners only install at promotion, so a change inside
the pointerdown->threshold window was invisible: a promote-and-release
gesture opened (and closed) a segment under a stale modifier. When
the promotion-time combo is unbound the drag now begins dormant and
resumes on the next bound combo (new planDragStart, unit-pinned).
2. transitionDragModifier reads bindings live from the scene-tree store
instead of a drag-start snapshot, so callbacks registered or removed
mid-drag take effect at the next switch. The snapshot field is gone
from ActiveDragState.
E2E pins for both: a pre-promotion Ctrl release must not fire a stale
segment (and must resume on re-press), and a binding registered
mid-drag must own the post-switch segment.
* Plan the opening drag segment against live bindings too
The pointerdown->promotion window fix attributed the opening segment to
the promotion-time modifier but still planned it against the bindings
array captured at pointerdown. A binding added or removed inside that
window was invisible to the opening plan (e.g. a combo bound mid-window
began dormant instead of owning the segment). beginDrag now reads
dragBindings live from the scene-tree store for the opening plan; the
pointerdown snapshot remains only as the candidacy gate.
* Defer switch-created segment starts until confirmed (100ms or motion)
Ending a modifier-drag the natural way -- modifier up a beat before the
button -- previously fired a degenerate start/end pair on whatever combo
remained bound (e.g. a bare on_drag registered alongside a modifier
binding), since humans can't release both within the same frame. This
reintroduced exactly the degenerate pair the promotion motion-threshold
eliminated, one boundary later.
A mid-drag switch now ends the old segment immediately but holds the new
segment's start PENDING until confirmed: a 100ms timer
(SWITCH_START_DELAY_MS), or sooner by pointer motion past the promotion
threshold from the switch point. Release/cancel/blur inside the window
discards the pending start outright -- no message was sent, none is owed
-- and a further modifier change abandons it silently, which also
swallows key-rollover pass-through combos when releasing multi-key
combos one key at a time.
Stationary switches still fire (timer path, imperceptibly later); the
opening segment is never deferred (promotion's motion threshold is its
confirmation). E2E pins the release order: bare + cmd/ctrl both bound,
ctrl-drag ended modifier-first -> bare sees nothing, ctrl sees one clean
pair, and a follow-up bare drag proves the binding was live.
* Strengthen the release-order e2e; fix start-at-press docstrings
The discard pin alone was satisfiable with the keyup path broken
entirely (the pointerup would end the ctrl segment and bare would still
read zero), and its keyup->mouseup gap raced the 100ms deferral timer on
slow CI. Split into two gestures: A holds the button through the window
so the keyup's pending bare start CONFIRMS on the timer (proving the
keyup creates a pending segment, and doubling as the bare-binding
positive control); B dispatches keyup + pointerup in one JS task -- both
listeners are window-level -- making the inside-the-window release
deterministic before asserting bare never budged.
Also correct the on_drag docs: start fires when the press is confirmed
as a drag (motion threshold), not at press.1 parent d04c1cd commit b2dbf9f
9 files changed
Lines changed: 1267 additions & 41 deletions
File tree
- src/viser
- client/src
- pointer
- tests
- e2e
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
474 | 474 | | |
475 | 475 | | |
476 | 476 | | |
477 | | - | |
478 | | - | |
479 | | - | |
480 | | - | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
481 | 490 | | |
482 | 491 | | |
483 | 492 | | |
| |||
498 | 507 | | |
499 | 508 | | |
500 | 509 | | |
501 | | - | |
502 | | - | |
503 | | - | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
504 | 516 | | |
505 | 517 | | |
506 | 518 | | |
| |||
611 | 623 | | |
612 | 624 | | |
613 | 625 | | |
614 | | - | |
615 | | - | |
616 | | - | |
617 | | - | |
618 | | - | |
619 | | - | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
620 | 652 | | |
621 | 653 | | |
622 | 654 | | |
| |||
629 | 661 | | |
630 | 662 | | |
631 | 663 | | |
632 | | - | |
633 | | - | |
634 | | - | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
635 | 670 | | |
636 | 671 | | |
637 | 672 | | |
| |||
0 commit comments