Skip to content

Studio: after a normal drag, the committed element flies off-screen on the next button-less mousemove (stale gesture handler survives post-commit softReload) #1673

Description

@nicolasfilippini

Summary

In the Studio canvas, after moving an element with a plain click-drag (pointerdown → move → mouseup), the very next button-less pointermove (just moving the cursor over the canvas, no click) throws the element far off-screen. This is live-preview-only: the persisted source stays correct (the element keeps its committed position; a page refresh restores the on-screen render). It is independent of composition markup (reproduced across compositions) and present identically in 0.6.112 and 0.6.121. No keyboard shortcut is involved — this is the ordinary drag-to-reposition workflow.

Affected versions

Confirmed present in 0.6.112 and 0.6.121. A function-level comparison of the served dist/studio/assets/index-*.js between the two versions shows the drag/gesture/calibration code is byte-identical modulo minified identifier names — i.e. this is not a recent regression, and a version rollback does not help.

Primary bug — element flies off-screen on a button-less mousemove after a normal drag

Steps to reproduce

  1. Select an element in the canvas.
  2. Drag it with a normal click-drag (pointerdown → move → mouseup — the move is committed).
  3. Without clicking anything, move the mouse over the canvas.

Expected: nothing happens (no active drag).
Actual: the element is flung far off-screen. The Inspector still shows the correct, committed X/Y and the source --hf-studio-offset is unchanged — only the live preview is wrong. A page refresh restores the correct render.

Mechanism (confirmed by a real puppeteer repro: button genuinely held during the drag, player paused)

  • The drag itself is clean: the move is applied live through the CSS translate property (--hf-studio-offset); the selection box tracks the element 1:1; no source write happens during the move. On mouseup the commit fires exactly one PUT of the composition file, followed by a softReload of the preview.
  • The off-screen flight is driven by a GSAP transform matrix (e.g. matrix(1,0,0,1,-6067,-5054)) — i.e. the gesture-recording live-apply path (gsap.set after translate:none), not the committed --hf-studio-offset (which stays correct).
  • Root cause: the commit's softReload does not tear down the gesture/live-apply machinery — a document-level button-less pointermove handler (+ its RAF) survives the reload with a stale startPointer. The next mouse move computes delta = pointer − startPointer from that stale anchor → a huge translation → the element leaves the screen.
  • Control: with no prior drag, a button-less mousemove does nothing (transform: none). The bug therefore requires a drag + commit + softReload to "arm" the surviving handler. This is a softReload teardown defect, not an inherent cost of source persistence (SDK-cutover).

Note on workaround: we verified that in-place mitigations do not clear the stale handler — clicking to deselect, re-selecting the element, and seeking/advancing the playhead all still result in the off-screen flight. Only a full page refresh reliably tears down the RAF/listener and restores a clean state.

Secondary bug — selection box desyncs from the element (~50px) during gesture-recording (r) only

Kept for completeness. This one does NOT occur in a normal drag — it is observed only in the gesture-recording mode entered with the r key.

Steps to reproduce

  1. Select an element, press r to start gesture recording.
  2. Move the mouse to drag the element, grabbing it off-center.

Expected: the element follows the selection box 1:1.
Actual: the element renders offset from the selection box by ~the grab distance (≈50px for a ~90px element). On stop it snaps back into the box.

Mechanism (read from the served bundle)
On the first move the recorder does basePosition += pointerElementOffset (pointerElementOffset = pointer − elementCenter), so the live apply teleports the element center under the cursor — but the recorded sample subtracts the same offset back out (r.x -= cssVarOffset.x + pointerElementOffset.x / scale). The persisted coordinate is clean while the live element carries the grab offset; the overlay tracks the clean coordinate → constant desync = the grab offset.

Suggested fixes

  • Primary: on the post-commit softReload, fully tear down the gesture/live-apply session — cancel the RAF and removeEventListener the document pointermove handler (this is exactly what a manual refresh does). And/or ignore button-less pointermove deltas outside an explicit recording session. The skipReload path that already exists in the bundle (commit without reload, re-render in place) would also avoid the reload that strands the handler.
  • Secondary (r desync): make the live apply and the recorded sample use the same coordinate — don't add pointerElementOffset to basePosition for the live render (or subtract it before applying, not only before recording).

Notes

  • Live-preview only; source persistence is always correct (refresh restores).
  • Not markup-dependent (reproduced across compositions; the affected compositions use plain absolute clips with translate: var(--hf-studio-offset-x/y)).
  • Render is unaffected: the headless deterministic render path involves no mouse gestures, so the exported video never carries this defect.

Environment

  • Self-hosted Studio, Node 22, Linux, Chrome
  • Served over HTTPS behind a reverse proxy
  • Versions 0.6.112 and 0.6.121 (identical behavior)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions