Repository navigation
fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves - #5022
Conversation
Edit accuracy: accurate 1556 (base branch 1556), smooth 1177 of thoseThe gate passes. Quarantined, measured but not gated (0) |
…d, even after undo
…ore a later one failed
…as the edit drew it
…save adds the drag once
jrusso1020
left a comment
There was a problem hiding this comment.
Approving at 06c753d3.
Can the paint-back drift from what's saved? I followed each case through useEditHistoryActions.apply:
- Save fails with nothing written:
landed()resolves false, so the revert is the undo, and nothing gets put back. Undo waits throughsettlePendingEdits, which never throws, so thefinallycan't put the edit back after a failed save. I checked this with a throwaway test: a plain resize and a plain rotate, painted back, then the save rejects. Both end on the style from press time, andlanded()is false. - A GSAP write saved before a later one failed:
markSavedmakeslanded()true, so the server still steps it. - Plain resize: it's one
commitPositionPatchToHtmlcall (size, crop and offset together), so it can't half-save. - Server refuses or steps nothing:
showAgainputs back the last drawn look, and the held redraws then run against the edited file. - A newer edit: the newest entry is the one painted back, the same as #4877. Your second known limit covers an undo followed right away by a new drag on the same element.
Reuse: this extends #4877's pending-edit paint-back and the existing undo path. It doesn't start a second undo mechanism. The reverts reuse getOffsetDragGsap, capture/restoreStudioPathOffset and stampGestureBase. runGestureTransaction only gains an optional draw hook. elementLookRevert snapshots the whole style attribute instead of combining captureStudioBoxSize and captureStudioRotation. That's the simpler choice, and it also covers the crop's clip-path, which the box-size snapshot misses. I found nothing else this should be built on.
Simplicity: drawUnlessUndone and drawKeepingUndone can't merge into one. A redraw can wait, but the measuring draws work out the values that get saved, so they have to run on the edit as it was shown. The scale-finalize rewrite is the same steps inside one draw with shorter comments, so I don't see a smaller version.
Verified locally:
- The 14 touched or related studio test files pass (180 tests), including both
useEditHistoryActionssuites. - I mutated
studioPendingEdits.tsfive ways, and each one turned tests red:- dropping the re-revert after a draw: 5 fail
markSaveddoing nothing: 1 failsdrawUnlessUndonealways drawing: 2 failwithinnot adopting: 12 failshowAgaindropping held redraws: 2 fail
Non-blocking: after showAgain, entry.revert stays null, so reverted() still returns true. If a draw ever ran after that, drawKeepingUndone would hide the edit again. Nothing reaches that today, because undo puts the edit back only after it settles. Resetting entry.revert = revert in showAgain would make the state honest, if you want to close it.
— Rames
What
Pressing Cmd+Z while a resize, a rotate, or any edit on an element GSAP animates is still saving now shows the edit undone in the same frame as the key, and it stays undone until the server's undo lands. Before, the screen kept the edit until every save and the server undo had finished, often seconds under load, so the undo looked ignored. #4877 did this for a plain move and nudge; this covers the rest.
Why
Undo can only paint back an edit that registered a way to show itself undone. Only the plain move and nudge did. Three other gaps kept GSAP edits from using it:
await, and every write registered itself as a newer pending edit with nothing to paint back. Undo takes the newest, so it painted nothing, even for a GSAP move.How
studioPendingEdits.tsowns the rule. An edit in flight (StudioEditInFlight) can:within);drawUnlessUndone);drawKeepingUndone), so a writer that must draw to measure or to build its patches never leaves a frame showing the edit.observeGsapGesture, which every GSAP gesture handler calls first, captures the edit being adopted. Its writes join that edit and carry it to the commit.useGsapScriptCommitssends its preview sync throughdrawUnlessUndone. The file write, history, keyframe cache and SDK resync are unchanged.The GSAP resize route sends its live writes through
drawKeepingUndone: the position settle, the element-size commit, the anchor stage, the crop, and the scale finalize, which now measures and corrects inside one synchronous draw. The move's element-offset fallback does the same.Gestures register a revert:
"Show again" (the server undo failed or stepped nothing) restores the last drawn look and runs any held redraw.
The scale route reads its drop point through the same draw, so an undo during its animation fetch cannot make it measure the undone box and save a wrong position.
A group move draws the members it moves on their own CSS through it too, and a move's revert also puts back the
position/left/topa GSAP element's fallback move writes. The move's revert puts GSAP's x/y back from the gesture's base it holds in memory and keeps the base stamps the save reads.The resize's after-save pixel check measures through the same draw, so a resize undone mid-save does not log a false
commit_invariant_violation.An edit that saved one write before a later one failed counts as saved, so undo still steps it on the server instead of only painting it back.
The undo reverts moved to
components/editor/gestureUndoRevert.ts;manualOffsetDrag.tswas at its line cap.Before
On main, with every save held 3 s, 150 ms after Cmd+Z the box still shows the edit (here, still the resized size); it snaps back only when the saves and the server undo land, 3 to 6 s later.
After
The same moment on this branch: the box is already back to its size before the edit, and stays there until the server undo lands.
Test plan
dragUndoPaint.test.ts: plain resize and rotate, a GSAP drag, and a GSAP rotate are each painted back at once and shown again. They fail on main: undo has nothing to paint back.useGsapScriptCommits.test.tsx: a GSAP gesture whose write comes after anawaitis still one edit. After undo paints it back, its save writes the file, does not soft-reload, and soft-reloads on "show again". It fails on main (expected null not to be null), and without the redraw rule it fails on the soft reload: that is the flash.gsapResizeElementSize.test.tsx: a GSAP resize painted back stays undone while its save lands and still writes the new size. On main the save draws the size back on: the resize flash.studioPendingEdits.test.ts: a draw on a painted-back edit sees the edit as shown and leaves it undone; a held redraw runs only on "show again".gsapResizeDropPoint.test.ts: a scale resize undone while its animations load still lands on the drop point (fails without the drop read inside the draw: 198 px off).useGsapAwareEditing.groupPlan.test.tsx: a group member moved on its own CSS stays undone while the group saves.dragUndoPaint.test.ts: a GSAP drag whose save draws aleft/topoffset stays undone while it saves.dragUndoPaint.test.ts: a GSAP drag keeps its drag-start base through every repaint of its undo, so its save adds the drag once (before the fix a repaint left the base gone and a group move saved twice the drag).studioPendingEdits.test.ts: an edit whose first write saved before a later one failed still counts as saved.gestureTransaction.test.ts: a box undo painted back, before or during the save, is measured as the edit drew it, so no pixel change is reported.In a browser
Chrome 152 against a fixture with a 240×160 box (plain, or moved by a GSAP tween), every save held 3 s, Cmd+Z right after the edit, the box read every frame for 8 s, 3 runs per case. This branch measured at its head build:
On both builds every run ends on the pre-edit box with the file back to its pre-edit bytes.
Known limits