feat(studio): GSAP drag/commit/bridge editing infra#1558
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
3365d24 to
74c37f3
Compare
654d55e to
4c38428
Compare
terencecho
left a comment
There was a problem hiding this comment.
Async-semantics axis — second-pass correction crediting @rames-jusso
Updating my take. Rames flagged the commitMutation facade's broken await semantics in a parallel review, and he's right. My initial pass declared it "internally consistent" because I checked what comes AFTER each await commitMutation(...) — and in useGestureCommit / useEnableKeyframes the next step is showToast(...) or return. I concluded the await was "vestigial but harmless." That was wrong — I missed that the showToast("Recorded N keyframes", "info") at useGestureCommit.ts:198 is itself a UI claim that depends on save success. With the facade's promise now resolving on next microtask (before the save lands), on a save failure the user sees:
- The optimistic
"Recorded N keyframes"toast (because we already "succeeded"), then - The wrapper's
"Couldn't save animation: ..."toast fromsafeGsapCommit's.catch().
Two contradictory toasts. The outer try/catch in useGestureCommit is also dead for save errors now — they're swallowed by the wrapper, never re-thrown.
Verified findings (still valid)
- The facade type signature didn't change — outer fn still returns
Promise<void>— so callers compile without warning. The semantic break is invisible to TypeScript. - No pre-existing
try/catchon the 2-arg facade path; the dead try/catch is in NEW code (useGestureCommit) added in this same PR. - The 3-arg
commitMutation(selection, mutation, options)chain is untouched —useGsapKeyframeOps.ts:121,192continue to chain.catch()correctly on the underlying CommitMutation.
Endorsing Rames's fix
Easiest mitigation matches his suggestion: have useSafeGsapCommitMutation return the inner promise instead of returning void, so await semantics hold:
return useCallback(
(selection, mutation, options) =>
commitMutation(selection, mutation, options).catch((error) => {
trackGsapSaveFailure(error, selection, mutation, options.label);
showToast?.(`Couldn't save animation: ${getStudioSaveErrorMessage(error)}`, "error");
}),
[...],
);That keeps the caller's await honest AND keeps the toast-on-error behavior. Drop the void prefix, return the chained promise.
Lesson for me
Tracing "what comes after the await" was the wrong frame. The right question was "does the resolution timestamp of this promise carry meaning to downstream UI claims?" — and showToast("Recorded...") IS such a claim. Calibration for the next async-semantics pass.
— Review by tai (pr-review)
Drag-to-keyframe commit + parkPlayheadOnKeyframe, a stale-parse guard, from()/ fromTo() out-of-range replace-not-add, and the gsap.set path-offset helper.
4c38428 to
a3a26a9
Compare
74c37f3 to
52dfaa9
Compare

Stack: GSAP keyframe + motion-path editing — drag/commit bridge (#1553 → #1561).
What
Infrastructure to edit GSAP-animated elements by direct manipulation: a runtime bridge that routes canvas drags into keyframe / tween source mutations instead of CSS patches.
Why
Dragging a GSAP-owned element via CSS corrupts its transform (GSAP owns
style.transformand rewrites it every tick). Geometry edits must resolve the live tween and commit through script mutation.How
gsapRuntimeBridge.ts: resolves the live position tween for a selection and intercepts drag/resize/rotation.gsapDragCommit.ts: converts a drag delta into a keyframe / flat-tween edit (auto-converting a flat tween to keyframes when needed).useGsapAwareEditing.ts: routes geometry commits to the bridge, falling back to CSS only for non-GSAP elements.manualEditsDom.ts: GSAP-animated elements push the offset throughgsap.setand keeptranslate:nonerather than composing a CSS translate on top of GSAP's matrix.Test plan