From b6bf1b11909335869a79204b183ceb4cc0bd081b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 12 Jun 2026 00:12:26 -0400 Subject: [PATCH] fix(core): split-into-property-groups and replace-with-keyframes mutations (#1355) * fix(core): per-property-group keyframe foundations Add PropertyGroupName type system (position/scale/size/rotation/visual/other), PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup functions. Parser generates group-aware animation IDs, resolves position strings (+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs across all mutations. * fix(core): add split-into-property-groups and replace-with-keyframes mutations Server-side mutations for atomic property-group splitting and keyframe replacement. Client commitMutation returns early on changed:false instead of throwing. --- packages/core/src/studio-api/routes/files.ts | 41 ++++++++++++++++++- .../studio/src/hooks/useGsapScriptCommits.ts | 12 +++++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/core/src/studio-api/routes/files.ts b/packages/core/src/studio-api/routes/files.ts index 9fd909920..31b879bbb 100644 --- a/packages/core/src/studio-api/routes/files.ts +++ b/packages/core/src/studio-api/routes/files.ts @@ -409,6 +409,20 @@ type GsapMutationRequest = }>; ease?: string; } + | { + type: "replace-with-keyframes"; + animationId: string; + targetSelector: string; + position: number; + duration: number; + keyframes: Array<{ + percentage: number; + properties: Record; + ease?: string; + auto?: boolean; + }>; + ease?: string; + } | { type: "split-animations"; originalId: string; @@ -416,6 +430,10 @@ type GsapMutationRequest = splitTime: number; elementStart: number; elementDuration: number; + } + | { + type: "split-into-property-groups"; + animationId: string; }; // ── GSAP mutation executor ────────────────────────────────────────────────── @@ -445,6 +463,7 @@ async function executeGsapMutation( removeArcPathFromScript, addAnimationWithKeyframesToScript, splitAnimationsInScript, + splitIntoPropertyGroups, } = parser; function requireAnimation( @@ -617,6 +636,18 @@ async function executeGsapMutation( ); return result.script; } + case "replace-with-keyframes": { + const script = removeAnimationFromScript(block.scriptText, body.animationId); + const added = addAnimationWithKeyframesToScript( + script, + body.targetSelector, + body.position, + body.duration, + body.keyframes, + body.ease, + ); + return added.script; + } case "split-animations": { if ( typeof body.originalId !== "string" || @@ -647,6 +678,10 @@ async function executeGsapMutation( elementDuration: body.elementDuration, }); } + case "split-into-property-groups": { + const result = splitIntoPropertyGroups(block.scriptText, body.animationId); + return result.script; + } default: return respond({ error: `unknown mutation type: ${(body as { type: string }).type}` }, 400); } @@ -1061,8 +1096,9 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void { if (result instanceof Response) return result; const newScript = typeof result === "string" ? result : result.script; - const newHtml = block.replaceScript(newScript); - if (newHtml !== html) { + const changed = newScript !== block.scriptText; + const newHtml = changed ? block.replaceScript(newScript) : html; + if (changed) { writeFileSync(res.absPath, newHtml, "utf-8"); } @@ -1070,6 +1106,7 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void { const freshParsed = parseGsapScript(newScript); const responsePayload: Record = { ok: true, + changed, parsed: freshParsed, before: html, after: newHtml, diff --git a/packages/studio/src/hooks/useGsapScriptCommits.ts b/packages/studio/src/hooks/useGsapScriptCommits.ts index 70eee5215..998c2b2cf 100644 --- a/packages/studio/src/hooks/useGsapScriptCommits.ts +++ b/packages/studio/src/hooks/useGsapScriptCommits.ts @@ -51,6 +51,7 @@ function ensureElementAddressable(selection: DomEditSelection): { interface MutationResult { ok: boolean; + changed?: boolean; parsed?: ParsedGsap; before?: string; after?: string; @@ -131,9 +132,16 @@ export function useGsapScriptCommits({ const pid = projectIdRef.current; if (!pid) return; const targetPath = selection.sourceFile || activeCompPath || "index.html"; - const result = await mutateGsapScript(pid, targetPath, mutation); - if (!result?.ok) return; + if (!result) { + if (options.skipReload) return; + throw new Error(`Mutation failed: ${mutation.type}`); + } + + if (result.changed === false) { + if (options.skipReload) return; + return; + } domEditSaveTimestampRef.current = Date.now();