Skip to content

Commit c2e6e8d

Browse files
feat(studio): route GSAP tween add/update/delete through SDK (§3.5 PR1)
Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
1 parent d92de93 commit c2e6e8d

7 files changed

Lines changed: 455 additions & 80 deletions

File tree

packages/studio/src/hooks/gsapScriptCommitTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface GsapScriptCommitsParams {
7070
onCacheInvalidate: () => void;
7171
onFileContentChanged?: (path: string, content: string) => void;
7272
showToast: (message: string, tone?: "error" | "info") => void;
73-
/** Stage 7 Step 3b: SDK session for shadow GSAP dispatch (server stays authoritative). */
73+
/** Stage 7 §3.5: SDK session for routing GSAP tween ops through addGsapTween/setGsapTween/removeGsapTween. */
7474
sdkSession?: Composition | null;
75+
writeProjectFile?: (path: string, content: string) => Promise<void>;
7576
}

packages/studio/src/hooks/useDomEditSession.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export function useDomEditSession({
194194
onFileContentChanged: updateEditingFileContent,
195195
showToast,
196196
sdkSession,
197+
writeProjectFile,
197198
});
198199

199200
// ── DOM commit handlers ──

packages/studio/src/hooks/useGsapAnimationOps.ts

Lines changed: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@ import { useCallback } from "react";
22
import type { Composition } from "@hyperframes/sdk";
33
import type { DomEditSelection } from "../components/editor/domEditingTypes";
44
import { roundTo3 } from "../utils/rounding";
5-
import { runShadowGsapTween, type ShadowGsapOp } from "../utils/sdkShadow";
5+
import { sdkGsapTweenPersist } from "../utils/sdkCutover";
66
import {
77
assignGsapTargetAutoIdIfNeeded,
88
ensureElementAddressable,
99
} from "./gsapScriptCommitHelpers";
1010
import type { CommitMutation, SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
11+
import type { EditHistoryKind } from "../utils/editHistory";
1112

12-
interface GsapAnimationOpsParams {
13+
interface SdkAnimationDeps {
14+
sdkSession?: Composition | null;
15+
writeProjectFile?: (path: string, content: string) => Promise<void>;
16+
editHistory?: {
17+
recordEdit: (entry: {
18+
label: string;
19+
kind: EditHistoryKind;
20+
coalesceKey?: string;
21+
files: Record<string, { before: string; after: string }>;
22+
}) => Promise<void>;
23+
};
24+
reloadPreview?: () => void;
25+
domEditSaveTimestampRef?: React.MutableRefObject<number>;
26+
}
27+
28+
interface GsapAnimationOpsParams extends SdkAnimationDeps {
1329
projectIdRef: React.MutableRefObject<string | null>;
1430
activeCompPath: string | null;
1531
commitMutation: CommitMutation;
@@ -26,49 +42,92 @@ export function useGsapAnimationOps({
2642
commitMutationSafely,
2743
showToast,
2844
sdkSession,
45+
writeProjectFile,
46+
editHistory,
47+
reloadPreview,
48+
domEditSaveTimestampRef,
2949
}: GsapAnimationOpsParams) {
3050
const updateGsapMeta = useCallback(
31-
(
51+
async (
3252
selection: DomEditSelection,
3353
animationId: string,
3454
updates: { duration?: number; ease?: string; position?: number },
3555
) => {
36-
// Shadow op (server animationId shares the SDK id-space): existence via
37-
// runShadowGsapTween (live session) + value fidelity via the chokepoint.
38-
const shadowGsapOp: ShadowGsapOp = {
39-
kind: "set",
40-
animationId,
41-
properties: { duration: updates.duration, ease: updates.ease, position: updates.position },
42-
};
43-
// coalesceKey groups rapid meta edits into one history entry. Request
44-
// serialization is now handled per-file at the commitMutation chokepoint
45-
// (useGsapScriptCommits), so no per-op serializeKey is needed here.
46-
const metaKey = `gsap:${animationId}:meta`;
56+
if (
57+
sdkSession &&
58+
writeProjectFile &&
59+
editHistory &&
60+
reloadPreview &&
61+
domEditSaveTimestampRef
62+
) {
63+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
64+
const handled = await sdkGsapTweenPersist(
65+
targetPath,
66+
{ kind: "set", animationId, properties: updates },
67+
sdkSession,
68+
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
69+
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
70+
);
71+
if (handled) return;
72+
}
4773
commitMutationSafely(
4874
selection,
4975
{ type: "update-meta", animationId, updates },
50-
{ label: "Edit GSAP animation", coalesceKey: metaKey, shadowGsapOp },
76+
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
5177
);
5278
if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
5379
},
54-
[commitMutationSafely, sdkSession],
80+
[
81+
commitMutationSafely,
82+
activeCompPath,
83+
sdkSession,
84+
writeProjectFile,
85+
editHistory,
86+
reloadPreview,
87+
domEditSaveTimestampRef,
88+
],
5589
);
5690

5791
const deleteGsapAnimation = useCallback(
58-
(selection: DomEditSelection, animationId: string) => {
59-
const shadowGsapOp: ShadowGsapOp = { kind: "remove", animationId };
92+
async (selection: DomEditSelection, animationId: string) => {
93+
if (
94+
sdkSession &&
95+
writeProjectFile &&
96+
editHistory &&
97+
reloadPreview &&
98+
domEditSaveTimestampRef
99+
) {
100+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
101+
const handled = await sdkGsapTweenPersist(
102+
targetPath,
103+
{ kind: "remove", animationId },
104+
sdkSession,
105+
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
106+
{ label: "Delete GSAP animation" },
107+
);
108+
if (handled) return;
109+
}
60110
commitMutationSafely(
61111
selection,
62112
{ type: "delete", animationId, stripStudioEdits: true },
63113
{ label: "Delete GSAP animation", shadowGsapOp },
64114
);
65115
if (sdkSession) runShadowGsapTween(sdkSession, shadowGsapOp);
66116
},
67-
[commitMutationSafely, sdkSession],
117+
[
118+
commitMutationSafely,
119+
activeCompPath,
120+
sdkSession,
121+
writeProjectFile,
122+
editHistory,
123+
reloadPreview,
124+
domEditSaveTimestampRef,
125+
],
68126
);
69127

70128
const deleteAllForSelector = useCallback(
71129
(selection: DomEditSelection, targetSelector: string) => {
130+
// ponytail: no SDK op for delete-all-for-selector; stays server-authoritative
72131
void commitMutation(
73132
selection,
74133
{ type: "delete-all-for-selector", targetSelector },
@@ -78,8 +137,7 @@ export function useGsapAnimationOps({
78137
[commitMutation],
79138
);
80139

81-
// Pre-existing complexity (auto-id assignment + per-method defaults); this PR
82-
// adds only a guarded shadow-op construction at the tail.
140+
// fallow-ignore-next-line complexity
83141
const addGsapAnimation = useCallback(
84142
// fallow-ignore-next-line complexity
85143
async (
@@ -114,25 +172,34 @@ export function useGsapAnimationOps({
114172
fromTo: { x: 0, y: 0, opacity: 1 },
115173
};
116174

117-
// Shadow op (server stays authoritative). "set" has no SDK method, so it
118-
// is not shadowed; otherwise: existence via runShadowGsapTween (live) +
119-
// value fidelity via the chokepoint (shadowGsapOp in options).
120-
const shadowGsapOp: ShadowGsapOp | undefined =
121-
selection.hfId && method !== "set"
122-
? {
123-
kind: "add",
124-
target: selection.hfId,
125-
tween: {
126-
method,
127-
position,
128-
duration,
129-
ease: "power2.out",
130-
...(method === "fromTo"
131-
? { fromProperties: { opacity: 0 }, toProperties: toDefaults[method] }
132-
: { properties: toDefaults[method] ?? { opacity: 1 } }),
133-
},
134-
}
135-
: undefined;
175+
// SDK path: addGsapTween only supports from/to/fromTo; "set" stays server-side
176+
if (
177+
method !== "set" &&
178+
selection.hfId &&
179+
sdkSession &&
180+
writeProjectFile &&
181+
editHistory &&
182+
reloadPreview &&
183+
domEditSaveTimestampRef
184+
) {
185+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
186+
const spec = {
187+
method: method as "to" | "from" | "fromTo",
188+
position,
189+
duration,
190+
ease: "power2.out" as const,
191+
properties: toDefaults[method] ?? { opacity: 1 },
192+
fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
193+
};
194+
const handled = await sdkGsapTweenPersist(
195+
targetPath,
196+
{ kind: "add", target: selection.hfId, spec },
197+
sdkSession,
198+
{ editHistory, writeProjectFile, reloadPreview, domEditSaveTimestampRef },
199+
{ label: `Add GSAP ${method} animation` },
200+
);
201+
if (handled) return;
202+
}
136203

137204
await commitMutation(
138205
selection,
@@ -151,7 +218,17 @@ export function useGsapAnimationOps({
151218

152219
if (sdkSession && shadowGsapOp) runShadowGsapTween(sdkSession, shadowGsapOp);
153220
},
154-
[activeCompPath, commitMutation, projectIdRef, showToast, sdkSession],
221+
[
222+
activeCompPath,
223+
commitMutation,
224+
projectIdRef,
225+
showToast,
226+
sdkSession,
227+
writeProjectFile,
228+
editHistory,
229+
reloadPreview,
230+
domEditSaveTimestampRef,
231+
],
155232
);
156233

157234
return {

0 commit comments

Comments
 (0)