Skip to content

feat(studio): alt marquee removes the covered items from the selection - #5680

Merged
jrusso1020 merged 2 commits into
heygen-com:mainfrom
vincent65:feat/marquee-alt-subtract
Nov 5, 2026
Merged

jrusso1020 merged 2 commits into
heygen-com:mainfrom
vincent65:feat/marquee-alt-subtract

Conversation

@vincent65

@vincent65 vincent65 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Closes #5586

What

Holding Alt while dragging a marquee now removes the covered items from the current selection, on both the canvas and the timeline. Shift still adds, no modifier still replaces, and Alt wins when both are held so you can flip from adding to removing without releasing Shift.

  • utils/marqueeGeometry.ts gains MarqueeSelectionMode (replace | add | subtract) and resolveMarqueeMode(modifiers, base), the one place the modifiers are read. Both marquee paths thread this mode instead of the old additive: boolean.
  • Canvas: useMarqueeGestures commits with the mode its modifiers select and tracks it live for the band; applyMarqueeSelection in useDomSelection filters the covered selections out of the group in subtract mode, re-anchors the primary on a survivor if the primary was removed, and clears the selection when nothing is left.
  • Timeline: computeMarqueeSelection takes a mode and removes hits from baseSelection when subtracting; useTimelineRangeSelection picks Alt up at pointerdown and mid-drag, the way Shift already is, and resolves the primary through survivingPrimary. Escape still restores the pre-drag selection.
  • Visual: while subtracting, the band and the candidate outlines use the danger colour (border-danger on the canvas, a new --timeline-danger-fill token next to --timeline-danger on the timeline). Both bands expose data-marquee-mode for tests.

Release note for hosts

  • onMarqueeSelect / applyMarqueeSelection / onMarqueeSelectRef receive mode: MarqueeSelectionMode ("replace" | "add" | "subtract") instead of additive: boolean; marqueeRect is a MarqueeBand (a Rect plus mode).
  • A host that reuses MarqueeOverlay with useMarqueeGestures but decides add or replace on its own will now see the red "remove" band when the user holds Alt. Either handle mode in the host's commit callback, or keep Alt out of the band by passing a marqueeRect whose mode you override.
  • On both surfaces the modifiers held at release decide: letting go of Alt before the pointer lifts turns the gesture back into a replace. On the timeline, Cmd/Ctrl at pointerdown still sets add as the base, as before.

API note

onMarqueeSelect on DomEditOverlay, onMarqueeSelectRef on useMarqueeGestures, and applyMarqueeSelection now receive mode: MarqueeSelectionMode instead of additive: boolean, and marqueeRect is a MarqueeBand (a Rect plus mode). MarqueeBand, MarqueeSelectionMode and resolveMarqueeMode are exported from the package root for hosts such as the Desktop app.

Before

Same project and gesture on main: all four clips selected, then an Alt+drag across Logo on the timeline. Alt is ignored, so the marquee replaces the selection and only Logo is left selected.

Starting selection (same on both branches):

Four clips selected

After the Alt+drag on main:

main: Alt+drag replaces the selection with Logo

After

With this PR, the same Alt+drag shows the red subtract band, and Logo leaves the selection live while the drag is still going (3 elements selected):

This PR: Alt held mid-drag, Logo removed live

After release with Alt still held, Intro, Tagline and Outro stay selected:

This PR: after release, three clips remain selected

Walkthrough video

A 50-second explainer of this change, made with HyperFrames itself (the pr-to-video workflow, built from this PR's diff): watch pr-5680-alt-marquee.mp4 (6 MB, 1080p).

Contact sheet of the eight frames: the stray-clips hook, the Alt marquee removing them, the three modes, the resolveMarqueeMode helper typing on, the two-lane subtract diagram, the subtract branch diff, the test grid, and the credits close

How I tested

  • Unit tests for replace, Shift-add and Alt-subtract on both surfaces: marqueeGeometry.test.ts (mode resolution), marqueeCommit.test.tsx (mode committed on release, live band mode), useDomSelection.test.ts (subtract members, re-anchor primary, clear on empty, no-op on outsiders), timelineMarquee.test.ts (subtract and survivingPrimary), useTimelineRangeSelection.test.tsx (Shift add, Alt subtract at pointerdown and mid-drag, cancel restores).
  • bun run --cwd packages/studio typecheck, bun run lint, bun run format:check, node scripts/check-comment-citations.mjs and node scripts/comment-ratchet.mjs pass.
  • bun run --cwd packages/studio test: the touched suites pass; two unrelated tests fail on my machine on main as well (useTimelineGroupEditing.test.tsx with a base64url encoding error from the runtime, and the timing-sensitive versions.test.mjs).
  • Manually in Studio against the swiss-grid example (hyperframes preview): a plain marquee on the timeline selected Intro and Graphics, then an Alt marquee over Intro left only Graphics selected with the band drawn in red. On the canvas, a plain marquee selected the stat card and the caption, then an Alt marquee over the card left only the caption selected and re-anchored the primary on it.

🤖 Generated with Claude Code

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code verdict: approve-ready. I found no blocking issues. The one open question is whether Alt is the modifier we want, which is a product call, so I'm leaving the merge decision to the maintainers. Two small suggestions are below. Neither one blocks.

What I checked

  • utils/marqueeGeometry.ts:28: resolveMarqueeMode is now the only place that reads the modifiers, and Alt wins over Shift as the PR says. On the timeline, the mode set at pointerdown (Cmd/Ctrl gives add) is still the base, and Shift or Alt held mid-drag overrides it (useTimelineRangeSelection.ts:218, :350). That matches how Shift already behaved. The edge auto-scroll path now carries Alt too, through MarqueePointer.
  • Canvas subtract (hooks/useDomSelection.ts:506): it filters the seeded group, clears the selection when nothing is left, and re-anchors the primary on the first survivor. With no hits, the selections.length === 0 early return keeps the selection unchanged. That is correct, and it now also covers subtract because it tests replace rather than !additive.
  • Timeline subtract (timelineMarquee.ts:129, useTimelineRangeSelection.ts:111): hits are deleted from the pre-drag base, survivingPrimary keeps the old primary when it is still selected, and Escape still restores the snapshot.
  • Every in-repo caller of applyMarqueeSelection and onMarqueeSelect was moved from false to "replace" (useStudioUrlState.ts, useTimelineSelectionPreviewSync.ts, marqueeCommit.ts). I grepped and found none left on a boolean.
  • I checked Alt's existing meanings in Studio. Alt+click jumps to source and Alt+drag turns off snapping, but both apply only when the press lands on an element. A marquee only starts on empty canvas (DomEditOverlay.tsx, the fresh hit-test before marquee.begin), so the two don't collide.

What I ran

  • vitest on the 9 touched suites: 142/142 pass. tsc --noEmit for packages/studio is clean.
  • Mutation check: I disabled the subtract branch in computeMarqueeSelection and the filter in applyMarqueeSelection. That made 9 tests fail across timelineMarquee, useTimelineRangeSelection and useDomSelection, so the new tests do catch the behavior.

Suggestions (non-blocking)

  1. Canvas candidate outlines while subtracting. MarqueeOverlay.tsx:16-29 turns every element the band touches red, including elements that aren't selected and that the release will leave alone. On the timeline the live selection itself shows what will happen, but on the canvas the red outlines suggest that more will be removed than really is. You could filter candidateRects down to members of the current group in subtract mode, or keep unselected candidates in the neutral style.
  2. Hosts that consume useMarqueeGestures and ignore the second argument. The API note covers the type change. One thing it doesn't cover: a host that reuses MarqueeOverlay with this hook but decides add/replace on its own will now draw the red "remove" band under Alt, yet still commit a replace or add. That's worth noting in the release notes, so those hosts can either handle mode or keep Alt out of the band.

Thanks for the careful scoping and the thorough tests.

— Rames

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful work here. resolveMarqueeMode as the single place that reads the modifiers is a nice shape, and the timeline tests drive the real pointer handlers, so they cover what people will actually do. I agree with the earlier review's two suggestions and won't repeat them. A few additions:

  1. Alt pressed at the start of a drag behaves differently on the two surfaces. On the timeline, the mode resolved at pointerdown becomes the base (useTimelineRangeSelection.ts:350, then :218 falls back to it), so if you press Alt, start dragging and then let go of Alt, the marquee keeps subtracting. On the canvas the mode is re-read from each move and from the release event (marqueeCommit.ts:283, :305), so the same gesture ends as a replace, and the selection becomes exactly the items you meant to remove. Either rule is reasonable, but it would be good for both surfaces to follow the same one, with a test that releases the modifier mid-drag to pin it.
  2. Rebase needed. useDomSelectionTypes.ts now conflicts with a change on main that added an iframe parameter to refreshDomEditSelectionFromPreview and removed refreshDomEditGroupSelectionsFromPreview. It's an adjacent-line conflict: keep both of main's changes plus your new mode signature. Everything else merges cleanly, and I found no caller added on main since your base that still passes a boolean.
  3. CI hasn't run on this head yet, so a maintainer will need to start the workflows after the rebase.

None of this blocks on the code itself. I'm leaving a comment rather than an approval because of the conflict and the missing CI run.

Verdict: COMMENT
Reasoning: The code looks correct and well tested, but the branch conflicts with main and no CI has run at this head; one small cross-surface inconsistency is worth settling first.

@jrusso1020

Copy link
Copy Markdown
Collaborator

Yes on Alt+marquee to subtract. Please make canvas and timeline behave the same way, then rebase.

@vincent65
vincent65 force-pushed the feat/marquee-alt-subtract branch from 7925be6 to 0f7959e Compare November 4, 2026 03:44
@vincent65

Copy link
Copy Markdown
Contributor Author

Thanks both, and apologies for the slow turnaround. Rebased onto main (v0.9.10) with the useDomSelectionTypes.ts conflict resolved the way Miguel described: main's iframe parameter on refreshDomEditSelectionFromPreview and its removal of refreshDomEditGroupSelectionsFromPreview are kept, with the new mode signature. The follow-up commit addresses the review:

  • Same rule on both surfaces (Miguel 1, James). The modifiers held at release decide. The timeline now re-applies the marquee from the pointerup event before it finishes, so letting go of Alt mid-drag turns a subtract back into a replace, matching the canvas. Alt at pointerdown no longer bakes a base mode on the timeline; Cmd/Ctrl still sets add as before. Pinned by a test on each surface that drags with Alt and releases without it (useTimelineRangeSelection.test.tsx, marqueeCommit.test.tsx).
  • Canvas candidate outlines while subtracting (Rames 1). useMarqueeGestures takes the live groupSelectionsRef, and in subtract mode only candidates that are in the current selection are outlined, so the band never suggests removing something that is not selected. Test added.
  • Hosts that ignore the second argument (Rames 2). Added a release note section to the PR description.
  • The timeline commit helpers moved to timelineMarqueeCommit.ts to keep useTimelineRangeSelection.ts under the 600-line cap.

Typecheck, lint, format, both comment checks and the studio suite pass locally (8305 tests; the one failure is the pre-existing base64url runtime error in useTimelineGroupEditing.test.tsx, unrelated). CI needs a maintainer to start the workflows on the new head.

@vincent65

Copy link
Copy Markdown
Contributor Author

@miguel-heygen @jrusso1020 this is ready for another look: rebased onto main and both review rounds addressed (summary in the comment above). Could one of you start the CI workflows on the new head (0f7959e)? Thanks!

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rebase and for addressing both earlier rounds. I ran it for real at this head and the behavior is right, so I'm approving and turning on auto-merge.

What I verified

  • Alt released mid-drag gives a replace on the timeline, same as the canvas (the earlier point is fixed).
  • Alt held through the drag: 4 selected became 3 live and stayed 3 on release; a band that covers nothing leaves the selection alone.
  • The touched suites pass (149 tests); with the subtract path disabled, 11 of them fail across timelineMarquee, useDomSelection and useTimelineRangeSelection.

One thing left before it can land: the required "Studio and player captures" check wants ## Before and ## After sections in the PR description (for changes under packages/studio). Add both headings on GitHub and drag a screenshot under each, for example the timeline with Alt held mid-drag and the selection after release. Once that check is green, auto-merge takes it in. "Studio: timeline viewport gate" also fails the same way on main, so that one isn't from your change.

@jrusso1020

Copy link
Copy Markdown
Collaborator

Thanks @vincent65, this is approved. The one required check left is Studio and player captures: the body needs ## Before and ## After sections, each with an image or video (the walkthrough video doesn't count since it isn't under those headings). A screenshot or short clip of the alt-marquee selection before and after is enough, and editing the body re-runs the check, so no push needed.

@vincent65

Copy link
Copy Markdown
Contributor Author

Thanks @miguel-heygen and @jrusso1020! Added ## Before and ## After sections to the description with Studio screenshots of the same gesture on main and on this branch: four clips selected, then an Alt+drag across one. On main the drag replaces the selection with that clip; here it shows the subtract band mid-drag and leaves the other three selected after release. scripts/check-pr-captures.mjs passes locally with the new body. The re-triggered workflow runs are waiting on maintainer approval since this is a fork PR.

@jrusso1020

Copy link
Copy Markdown
Collaborator

Thanks @vincent65, the captures check passes now. GitHub is still holding the merge on stale check results from the Nov 4 run, so could you rebase on main and push once more? That gives it a clean set of checks; we'll approve the CI run and re-approve right after, and auto-merge is already on.

Shift already adds marquee hits to the selection on the canvas and the
timeline; there was no way to take a few items back out of a large
selection. Both marquee paths now carry a MarqueeSelectionMode
(replace | add | subtract) resolved from the pointer modifiers in one
helper, with Alt winning over Shift. Subtracting the primary re-anchors
on a surviving member; subtracting everything clears the selection. The
band and candidate outlines turn the danger colour while subtracting.

Closes heygen-com#5586
Both surfaces now let the modifiers held at release decide: the timeline
re-applies the marquee from the pointerup event before it finishes, so
letting go of Alt mid-drag turns a subtract back into a replace, as the
canvas already did. Alt at pointerdown no longer bakes a timeline base
mode; Cmd/Ctrl still sets add. While subtracting, the canvas outlines
only candidates that are in the current selection, so the band never
suggests removing something that is not selected. The timeline commit
helpers move to timelineMarqueeCommit.ts to keep the hook under the
file-size cap.
auto-merge was automatically disabled November 5, 2026 03:55

Head branch was pushed to by a user without write access

@vincent65
vincent65 force-pushed the feat/marquee-alt-subtract branch from 0f7959e to f2554cf Compare November 5, 2026 03:55
@vincent65

vincent65 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Done, @jrusso1020: rebased onto current main (98207038) and pushed. No conflicts and no code changes, the two commits are the same as before and both show Verified. Local typecheck, lint, format, the comment checks and the studio suite pass on the new base. The new head is f2554cf2, ready for you to approve the CI run and re-approve. Thanks!

One note: the push switched auto-merge off on GitHub's side, so it will need turning back on after the re-approval. All eight workflows on the new head are waiting for approval.

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving after the rebase onto main: the patch is byte-identical to the one reviewed.

@jrusso1020
jrusso1020 enabled auto-merge November 5, 2026 03:58
@jrusso1020
jrusso1020 added this pull request to the merge queue Nov 5, 2026
Merged via the queue into heygen-com:main with commit bdf9ba0 Nov 5, 2026
78 checks passed
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.

Marquee selection: Alt removes from the selection

3 participants