fix(studio): undo no longer snaps back a box you started dragging again - #5039
Conversation
Edit accuracy: accurate 2040 (base branch 2040), smooth 1619 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at e9c4cc12.
The deferred refresh fires when the gesture ends. The wait is afterStudioManualEditGestures. It runs on the hf-manual-edit-gesture-ended event, which endStudioManualEditGesture dispatches, and only once no gesture is left in the document. I traced every way a gesture can end, and each one calls it:
- a release: drag, resize and rotate end in
.finallyafter their save settles, so the reload comes after the save, as the body says; - a tap that barely moved;
- a cancel:
clearPointerStateends box-size and rotation gestures, and path-offset members go throughrestoreManualOffsetDragMembers.
A gesture that never ended would now hold the undo's repaint too. But it would already hold the shadow reload today, so this adds no new way to get stuck. Several undos during one hold each queue a reloadPreview(). Those fire in the same event dispatch and reloadPreview is setRefreshKey(k => k + 1), so React batches them into one reload.
The three paths hold together. Under a gesture:
- the predicted undo isn't painted;
- a refused prediction falls back to
syncHistoryPreviewAfterApply, which checks the gesture again; - the server-confirmed restore waits.
So whatever order the key press, the server's answer and the drag start come in, nothing paints under the held box. The gesture check comes after the nested-file reads, which is the right order, since a gesture can start during that await.
Reuse: this is the same wait the shadow reload uses (useShadowPreviewReload.ts:147-149), with the same markScenesStale + reloadPreview pair as the full undo path in applyUndoRestoreToPreview. The gesture marker is the one #5022's undo already reads. Nothing new is built.
Simplicity: one helper (heldPreviewDoc), one early return and two added conditions. That's the minimum for three entry points.
Nits, non-blocking:
- The stale-scene path list survives a mutation. Changing
restore.paths ?? Object.keys(restore.files ?? {})toObject.keys(restore.files ?? {})keeps all 8 tests green. Yet the refused path passes only{ paths }, so that change would mark nothing stale there. An assertion in the refused-prediction test would pin it. - In the held case the nested-file reads are awaited and then discarded. That's harmless, just wasted work.
Tests and mutations: usePreviewPersistence* passes 8/8, three runs in a row. Mutations:
- removing the held branch: caught;
- reloading at once instead of after the gesture: caught;
- dropping
markScenesStale: caught; - dropping the predicted-undo guard: caught;
- dropping the put-back guard: caught;
- the path-list change: survives (nit above).
CI at this head: 60 pass, 22 pending, 0 failed when I checked.
— Rames
What
Undo a resize of a GSAP-animated box, then start dragging the same box and hold it. About 20 ms after the server finishes the undo, the box jumps back to where the undo put it, under your pointer. After this change, the box stays where you are dragging it.
Why
When the server confirms an undo, Studio refreshes the preview in place. It re-runs the GSAP script, re-seeks the timeline and re-applies manual edits. That writes GSAP's values over the element the new drag is holding. Studio's other preview reload already holds while a gesture is live, and it loads the file again once that gesture's save lands (
useShadowPreviewReload,refreshPlayer). The undo refresh was the one path that skipped that rule.How
usePreviewPersistenceis where undo writes to the live preview. It now checks one thing: whether a gesture is live in the preview. If one is:reloadPreview()only when the last gesture ends (afterStudioManualEditGestures, the same wait the shadow reload uses). Before it reloads, it marks the restored files' scenes stale, as every other full undo reload does. That reload then waits for the gesture's save and loads the current file. Asking at once instead left the reload pending through the whole hold and blanked the timeline filmstrip. The selection is kept, because clearing it would drop the drag.Before
Main (1bdb5ed): resize a GSAP box, Cmd+Z, then drag the box and hold. When the undo lands, the box jumps back to the tween's position while the pointer stays put.
before-main.mp4
After
This branch: the box stays under the pointer through the undo, and the timeline filmstrip stays painted.
after-branch-trim.mp4
Real Chrome, 4 runs per build, read from the box's rect every frame:
Test plan
usePreviewPersistence.history.test.tsxfail on main and pass with the fix: