Repository navigation
fix(studio): an undo that drops the keyframes clears the transform the old preview still shows - #7005
Conversation
Edit accuracy: accurate 271 (base branch 271), smooth 120 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
5ad46f6 to
2c25d87
Compare
There was a problem hiding this comment.
Requesting changes at 2c25d875.
Blocker: the repaint fires on every structural undo, not just the one that drops the keyframes.
repaintWhileReloading runs whenever planRestoreTargets returns no plan. That covers every structural restore: undoing a split, a delete, or an added element. It resets every element the current timelines tween back to authored style. When the restored file keeps those tweens, the preview now shows each animated element at its authored base until the reload is promoted, then it jumps back. On main, the old frame stays correct until the swap. If the undo happens while the playhead is paused mid-animation, the user sees a visible flash on every such undo.
Repro probe (happy-dom, same harness as gsapUndoRestore.test.ts): #a has to("#a",{x:100}) with its live transform at translate(100px, 0px). The restore only removes an unrelated #b, and the script is unchanged.
main:#akeepstranslate(100px, 0px). Passes.- this head:
#a.style.transformbecomes"". Fails.
Please scope the repaint to elements the restored source no longer animates, for example only when the restored GSAP script differs from the live one, and only for targets it drops. Add this case as a test that must stay green.
Also please fix: the requestPreviewReload() half is a no-op in the app, and the description says otherwise.
The only caller passes App's reloadPreview (usePreviewPersistence.ts:243). That goes through setRefreshKey, which already calls requestPreviewReload() synchronously (App.tsx:108-111, present on the merge base). So the claims that "nothing ever called the first half" and that Studio "answered no" are not true on main. The second new test passes only because it injects vi.fn() as reload. Either drop the call and that test, or keep the call as defense-in-depth and correct the title and the Why/How sections. The title currently describes the no-op half.
Verified:
- The
fails without the fix:line is present. The named test (takes GSAP's composed transform off the preview it is about to reload) fails withgsapUndoRestore.tsandgsapSoftReload.tsreverted tomainand passes at head. gsapUndoRestore.test.tsandpreviewReloading.test.tsboth pass at head (49 tests).- The bench gate (
swapPending) is untouched.
2c25d87 to
0d0379d
Compare
…e old preview still shows A restore that removes the GSAP script cannot be patched in place, so the preview reloads - and until it lands it keeps the transform the timeline composed, though the file it is going back to places the box with CSS alone. Only the elements the restored script stops animating are repainted; anything it still animates keeps what the timeline gave it.
0d0379d to
f0604dc
Compare
There was a problem hiding this comment.
Approving at f0604dcf. Both findings from my previous review are resolved.
- Structural-undo flash: the repaint now runs only when the restored GSAP script differs from the live one, and it skips elements the restored script still targets. My earlier probe passes at this head: the restore removes an unrelated
#b, the script is unchanged, and the still-animated#akeepstranslate(100px, 0px). Your new test "leaves a still-animated element alone…" covers the same case. - I also tried a mixed case: the script changes and the restore is structural, with
#astill tweened and#b's tween dropped. With theconst tl = gsap.timeline(...)form,#ais kept and#bis cleared, as intended. With the chainedgsap.timeline().to(...)form,parseGsapScriptfinds no selectors, so#ais also repainted early. That matches the limitation you disclosed. Studio and the SDK write thetlvariable form (mutate.tsrequires it), so it isn't a blocker. requestPreviewReload(): the no-op call and its test are gone, and the description no longer makes that claim.fails without the fix:: the named test fails withgsapUndoRestore.tsandgsapSoftReload.tsreverted tomain(1 failed / 43 passed) and passes at head.gsapUndoRestore,gsapSoftReloadand my probes pass: 107/107.- CI:
Commentspasses on this head.CIis still running.
What
When an undo takes the keyframes away, the preview stops showing the place the timeline had moved
the box to, instead of holding it until the reload lands.
Fixes #6989.
Why
A burst of undos could leave the box displaced by exactly the transform GSAP had committed — 240.42 px
in the issue, which is
hypot(139.893, 195.508), the x/y of thegsap.setthe sequence writes.One step of that sequence takes the keyframes away, which removes a script element. No in-place patch
expresses that, so the restore asks for a full reload, and the reload runs an effect later. In that
window the live document still carries the transform the timeline composed, though the file it is
going back to places the box with CSS and no inline transform at all. A probe through the restore
path timed it: the escalation at 17.95 s, the next undo landing on the same document at 18.76 s, the
replacement promoted at about 19.3 s. The bench measures in that window, and so does a person
watching the canvas.
How
applyUndoRestoreToPreview, where it escalates to a full reload because no in-place plan exists, nowputs the authored style back — but only on the elements the restore actually stops animating:
removes an unrelated element leaves the preview exactly as it was;
repainted. Anything it still animates keeps what the timeline gave it, because the reload will
re-render it.
It reuses
restoreAuthoredStyleandcollectResetTargets, the pair the soft reload already uses forthis. No other painting change: the reload still does the repair, this only stops the preview showing
a place the file no longer describes.
Size
92 lines. One scoped change at one escalation, plus the two tests that pin both halves of the scope.
Test plan
src/utils/gsapUndoRestore.test.tsand the wholepackages/studiosuite, plus the bench case underthe
HF_SLOW_HISTORYlever from fix(studio): a preview reload writes the composition as it is now, not as it was when it asked #6998.only removes another element must not touch a still-animated one. On
mainthe first fails and thesecond passes, so neither is vacuous.
fails without the fix:
applyUndoRestoreToPreview > takes GSAP's composed transform off the preview it is about to reloadinpackages/studio/src/utils/gsapUndoRestore.test.tsWhat I measured
All on builds made with
bun run build && bun run --cwd packages/cli build:copy.mainundo.box240.42 every rungsapUndoRestore.test.tspackages/studiosuiteoxfmt --check,oxlint,fallow audit --fail-on-issues, comment ratchet,tsc --noEmitThe one failure on
mainis the keyframe-removal test; the still-animated test passes there, which isthe point of the scope.
240.42 is
hypot(139.893, 195.508)and 139.893/-195.508 is the x/y of thegsap.setthe sequencewrites, so the number the issue reports was that transform and nothing else.
What I did NOT exercise
target, a permanent soft-reload failure) still reload without it.
its targets another way (a variable holding a node list, a selector built at runtime) would read as
dropping them, and they would be repainted a frame early.
Before
The undone box on
main: displaced by the transform the timeline left on the preview it was about to reload.After
The same run with the change: back where the authored CSS centres it, without waiting for the reload.