Skip to content

fix(studio): shift+click on a group member removes it for good - #4754

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/studio-shift-click-removes-group-member
Sep 30, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/studio-shift-click-removes-group-member

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

In the Studio preview, shift+click on a member of a group selection now removes it, and it stays removed. Before, the member came back a moment later, so a group could never be trimmed by clicking. The same happened when a marquee caught just one member of a live group.

Why

The timeline keeps the selection set, and it syncs that set back onto the canvas. When shift+click shrank a group to one element, the canvas announced only a new anchor and left the timeline's set at two members. About 70 ms later the sync saw two wanted and one on the canvas, and re-selected both (apply nextGroup:1, then timeline-sync wanted:2 had:1, then marquee hits:2 in the hf-select debug log). A move right after that dragged the removed element too.

The mirror guessed intent from the group size: a one-element group whose anchor is already in the set was treated as a late async primary and kept the old set. A shift toggle or a marquee that ends at one element looks the same by size.

Related work

Refs #3146, which introduced the set-preserving rule this narrows.

How

The caller now says when its group is the whole new selection. applyDomSelection passes that for a shift toggle, and applyMarqueeSelection always does, so announceTimelineSelection publishes the new set in the same step as the canvas change. There is no race left to lose, and no timer. A plain single selection of an element already inside the live set still keeps the set, as before.

Test plan

  • Unit tests added/updated: useDomSelection.test.ts now reads back the set it last published, the way the timeline store does. Two new cases (shift+click removes a member, one-element marquee over a group) fail on main with the set still holding both ids, and pass here. A third case pins the old rule: re-announcing one member keeps the set; it fails if the mirror always replaces the set.
  • Manual testing performed on a fixture project with the timeline open: group two elements, shift+click one member (the timeline and inspector show one element at 150 ms and at 1 s, no sync re-select in the log), shift+click a non-member (added), shift+click it again (removed), move what is left (only that element's position is written), undo (file reverts). A one-element marquee over a group of two leaves one.
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments"

Before

Group of two, then shift+click on Tiny. A second later both are selected again.

Before: group of two
Before: one second after shift+click on a member, both are selected again

After

Same gesture. Tiny is removed and stays removed; shift+click on Headline adds it.

After: one second after shift+click on a member, only Medium is selected
After: shift+click on a non-member adds it

Removing a member with shift+click shrank the canvas group, but the timeline kept the old set, and the timeline syncs its set back onto the canvas, so the member came back about 70 ms later. A one-element marquee over a live group had the same fault. The canvas now publishes the new set whenever the caller states the whole selection (a shift toggle or a marquee), while a late single primary inside the live set still keeps it.
@miguel-heygen
miguel-heygen marked this pull request as ready for review September 30, 2026 06:58

@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.

Approve at c75cb1f8.

The cause in the body checks out against the code. A shift toggle that shrank a group to one element looked, by size alone, like a late async primary inside the live set, so the mirror kept the old set, and the timeline sync re-selected the removed member. The explicit replaceSet from the caller fixes that without a timer.

Callers checked. I checked every caller of announceTimelineSelection in useDomSelection.ts:

  • applyDomSelection (l.183) passes isAdditiveSelection. Both shift-add and shift-remove state the full group, so replacing the set is right. With preserveGroup plus additive, replaceDomEditGroupSelection also returns the full group.
  • applyMarqueeSelection (l.514) always passes true. The additive branch seeds the current group, and the non-additive branch is the whole marquee, so both are complete.
  • refreshDomEditGroupSelectionsFromPreview (l.430) and the clear paths (l.115, l.187) keep the old behaviour. That is correct for the refresh, which is the late-re-resolve case the preserve rule exists for.

Mutation check. With useDomSelection.ts and domSelectionTimelineMirror.ts reverted to main, the two new cases fail: "shift+click removes a member" and "a marquee catches one member of a live group". The re-announce case that pins the old rule passes on both.

Tests. 1074/1074 pass across src/hooks in studio, and studio tsc --noEmit is clean.

Non-blocking. The new mirror comment lists "timeline sync" among the callers that state the group in full, but no timeline-sync path passes replaceSet. Only the shift toggle and the marquee do. It is worth trimming so the next reader doesn't go looking for it.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 84811c4 Sep 30, 2026
55 checks passed
@miguel-heygen
miguel-heygen deleted the fix/studio-shift-click-removes-group-member branch September 30, 2026 07:17
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.

3 participants