mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe (#1498)
* feat(sdk,studio): ws-1.2 — percentage-based removeGsapKeyframe Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * feat(sdk,studio): ws-1.3 — removeGsapProperty SDK op + Studio hook cutover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * feat(sdk,studio): ws-1.4 — deleteAllForSelector SDK op + Studio hook cutover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> * fix(core): cascade-remove GSAP tweens in removeElementFromHtml (WS-2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com> --------- Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
co-authored by
Miguel Ángel
Claude Sonnet 4.6
parent
a5016ed416
commit
ceb815c318
@@ -878,6 +878,27 @@ export function removeKeyframeFromScript(
|
||||
return ms.toString();
|
||||
}
|
||||
|
||||
export function removePropertyFromAnimation(
|
||||
script: string,
|
||||
animationId: string,
|
||||
property: string,
|
||||
from = false,
|
||||
): string {
|
||||
const parsed = parseGsapScriptAcornForWrite(script);
|
||||
if (!parsed) return script;
|
||||
const target = parsed.located.find((l) => l.id === animationId);
|
||||
if (!target) return script;
|
||||
const { call } = target;
|
||||
const objNode = from ? (call.method === "fromTo" ? call.fromArg : null) : call.varsArg;
|
||||
if (!objNode) return script;
|
||||
const propNode = findPropertyNode(objNode, property);
|
||||
if (!propNode) return script;
|
||||
const allProps = (objNode.properties ?? []).filter((p: any) => isObjectProperty(p));
|
||||
const ms = new MagicString(script);
|
||||
removeProp(ms, propNode, allProps);
|
||||
return ms.toString();
|
||||
}
|
||||
|
||||
// ── Label write ops ───────────────────────────────────────────────────────────
|
||||
|
||||
export function addLabelToScript(script: string, name: string, position: number): string {
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
} from "../core.types";
|
||||
import { validateCompositionGsap } from "./gsapSerialize";
|
||||
import { ensureHfIds } from "./hfIds.js";
|
||||
import { parseGsapScriptAcornForWrite } from "./gsapParserAcorn.js";
|
||||
import { removeAnimationFromScript } from "./gsapWriterAcorn.js";
|
||||
import type { ValidationResult } from "../core.types";
|
||||
|
||||
const MEDIA_TYPES = new Set<string>(["video", "image", "audio"]);
|
||||
@@ -672,15 +674,40 @@ export function addElementToHtml(
|
||||
};
|
||||
}
|
||||
|
||||
function selectorTargetsId(selector: string, id: string): boolean {
|
||||
return (
|
||||
selector === `#${id}` ||
|
||||
selector === `[data-hf-id="${id}"]` ||
|
||||
selector === `[data-hf-id='${id}']`
|
||||
);
|
||||
}
|
||||
|
||||
function stripGsapForId(script: string, elementId: string): string {
|
||||
const parsed = parseGsapScriptAcornForWrite(script);
|
||||
if (!parsed) return script;
|
||||
let current = script;
|
||||
for (const { id: animId, animation } of parsed.located) {
|
||||
if (selectorTargetsId(animation.targetSelector, elementId)) {
|
||||
current = removeAnimationFromScript(current, animId);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function cascadeRemoveGsapById(doc: Document, elementId: string): void {
|
||||
for (const script of Array.from(doc.querySelectorAll("script"))) {
|
||||
const text = script.textContent ?? "";
|
||||
if (!text.includes("gsap") && !text.includes("ScrollTrigger")) continue;
|
||||
const updated = stripGsapForId(text, elementId);
|
||||
if (updated !== text) script.textContent = updated;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeElementFromHtml(html: string, elementId: string): string {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
const el = doc.getElementById(elementId);
|
||||
if (el) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
doc.getElementById(elementId)?.remove();
|
||||
cascadeRemoveGsapById(doc, elementId);
|
||||
return "<!DOCTYPE html>\n" + doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
addAnimationToScript,
|
||||
updateAnimationInScript,
|
||||
removeAnimationFromScript,
|
||||
removePropertyFromAnimation,
|
||||
addKeyframeToScript,
|
||||
removeKeyframeFromScript,
|
||||
updateKeyframeInScript,
|
||||
@@ -136,7 +137,48 @@ function targets(target: HfId | HfId[]): HfId[] {
|
||||
|
||||
// ─── Op dispatch ────────────────────────────────────────────────────────────
|
||||
|
||||
function dispatchRemoveGsapKeyframe(
|
||||
parsed: ParsedDocument,
|
||||
op: Extract<EditOp, { type: "removeGsapKeyframe" }>,
|
||||
): MutationResult {
|
||||
return "percentage" in op
|
||||
? handleRemoveGsapKeyframeByPercentage(parsed, op.animationId, op.percentage)
|
||||
: handleRemoveGsapKeyframe(parsed, op.animationId, op.keyframeIndex);
|
||||
}
|
||||
|
||||
function applyGsapOp(parsed: ParsedDocument, op: EditOp): MutationResult | undefined {
|
||||
switch (op.type) {
|
||||
case "addGsapTween":
|
||||
return handleAddGsapTween(parsed, op.target, op.tween);
|
||||
case "setGsapTween":
|
||||
return handleSetGsapTween(parsed, op.animationId, op.properties);
|
||||
case "removeGsapProperty":
|
||||
return handleRemoveGsapProperty(parsed, op.animationId, op.property, op.from);
|
||||
case "removeGsapTween":
|
||||
return handleRemoveGsapTween(parsed, op.animationId);
|
||||
case "deleteAllForSelector":
|
||||
return handleDeleteAllForSelector(parsed, op.selector);
|
||||
case "setGsapKeyframe":
|
||||
return handleSetGsapKeyframe(
|
||||
parsed,
|
||||
op.animationId,
|
||||
op.keyframeIndex,
|
||||
op.position,
|
||||
op.value,
|
||||
op.ease,
|
||||
);
|
||||
case "addGsapKeyframe":
|
||||
return handleAddGsapKeyframe(parsed, op.animationId, op.position, op.value);
|
||||
case "removeGsapKeyframe":
|
||||
return dispatchRemoveGsapKeyframe(parsed, op);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
|
||||
const gsap = applyGsapOp(parsed, op);
|
||||
if (gsap !== undefined) return gsap;
|
||||
switch (op.type) {
|
||||
case "setStyle":
|
||||
return handleSetStyle(parsed, targets(op.target), op.styles);
|
||||
@@ -160,31 +202,14 @@ export function applyOp(parsed: ParsedDocument, op: EditOp): MutationResult {
|
||||
return handleSetCompositionMetadata(parsed, op);
|
||||
case "setVariableValue":
|
||||
return handleSetVariableValue(parsed, op.id, op.value);
|
||||
case "addGsapTween":
|
||||
return handleAddGsapTween(parsed, op.target, op.tween);
|
||||
case "setGsapTween":
|
||||
return handleSetGsapTween(parsed, op.animationId, op.properties);
|
||||
case "removeGsapTween":
|
||||
return handleRemoveGsapTween(parsed, op.animationId);
|
||||
case "setGsapKeyframe":
|
||||
return handleSetGsapKeyframe(
|
||||
parsed,
|
||||
op.animationId,
|
||||
op.keyframeIndex,
|
||||
op.position,
|
||||
op.value,
|
||||
op.ease,
|
||||
);
|
||||
case "addGsapKeyframe":
|
||||
return handleAddGsapKeyframe(parsed, op.animationId, op.position, op.value);
|
||||
case "removeGsapKeyframe":
|
||||
return handleRemoveGsapKeyframe(parsed, op.animationId, op.keyframeIndex);
|
||||
case "setClassStyle":
|
||||
return handleSetClassStyle(parsed, op.selector, op.styles);
|
||||
case "addLabel":
|
||||
return handleAddLabel(parsed, op.name, op.position);
|
||||
case "removeLabel":
|
||||
return handleRemoveLabel(parsed, op.name);
|
||||
case "setClassStyle":
|
||||
return handleSetClassStyle(parsed, op.selector, op.styles);
|
||||
default:
|
||||
throw new UnsupportedOpError((op as EditOp).type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,6 +714,20 @@ function handleSetGsapTween(
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function handleRemoveGsapProperty(
|
||||
parsed: ParsedDocument,
|
||||
animationId: string,
|
||||
property: string,
|
||||
from: boolean | undefined,
|
||||
): MutationResult {
|
||||
const script = getGsapScript(parsed.document);
|
||||
if (!script) return EMPTY;
|
||||
const newScript = removePropertyFromAnimation(script, animationId, property, from ?? false);
|
||||
if (newScript === script) return EMPTY;
|
||||
setGsapScript(parsed.document, newScript);
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function handleRemoveGsapTween(parsed: ParsedDocument, animationId: string): MutationResult {
|
||||
const script = getGsapScript(parsed.document);
|
||||
if (!script) return EMPTY;
|
||||
@@ -698,6 +737,24 @@ function handleRemoveGsapTween(parsed: ParsedDocument, animationId: string): Mut
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function handleDeleteAllForSelector(parsed: ParsedDocument, selector: string): MutationResult {
|
||||
const script = getGsapScript(parsed.document);
|
||||
if (!script) return EMPTY;
|
||||
const parsedForWrite = parseGsapScriptAcornForWrite(script);
|
||||
if (!parsedForWrite) return EMPTY;
|
||||
const matching = parsedForWrite.located.filter((l) => l.animation.targetSelector === selector);
|
||||
if (matching.length === 0) return EMPTY;
|
||||
let newScript = script;
|
||||
for (const m of [...matching].reverse()) {
|
||||
newScript = removeAnimationFromScript(newScript, m.id);
|
||||
}
|
||||
if (newScript === script) return EMPTY;
|
||||
setGsapScript(parsed.document, newScript);
|
||||
// ponytail: skips stripStudioEditsFromTarget (data-hf-studio-path-offset cleanup) —
|
||||
// studio path offset is cosmetic once all animations are gone; session reloads after write
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function resolveKeyframe(parsed: ParsedDocument, animationId: string, keyframeIndex: number) {
|
||||
const script = getGsapScript(parsed.document);
|
||||
if (!script) return null;
|
||||
@@ -772,6 +829,28 @@ function handleAddGsapKeyframe(
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function handleRemoveGsapKeyframeByPercentage(
|
||||
parsed: ParsedDocument,
|
||||
animationId: string,
|
||||
percentage: number,
|
||||
): MutationResult {
|
||||
const script = getGsapScript(parsed.document);
|
||||
if (!script) return EMPTY;
|
||||
const parsedForWrite = parseGsapScriptAcornForWrite(script);
|
||||
const located = parsedForWrite?.located.find((l) => l.id === animationId);
|
||||
const kfs = located?.animation.keyframes?.keyframes;
|
||||
if (!kfs) return EMPTY;
|
||||
// No-op on ambiguity: duplicate-percentage keyframes can't be disambiguated.
|
||||
const TOLERANCE = 0.001;
|
||||
const matches = kfs.filter((k) => Math.abs(k.percentage - percentage) <= TOLERANCE);
|
||||
if (matches.length !== 1) return EMPTY;
|
||||
const pct = matches[0]!.percentage;
|
||||
const newScript = removeKeyframeFromScript(script, animationId, pct);
|
||||
if (newScript === script) return EMPTY;
|
||||
setGsapScript(parsed.document, newScript);
|
||||
return gsapScriptChange(script, newScript);
|
||||
}
|
||||
|
||||
function handleRemoveGsapKeyframe(
|
||||
parsed: ParsedDocument,
|
||||
animationId: string,
|
||||
@@ -873,7 +952,9 @@ export function validateOp(parsed: ParsedDocument, op: EditOp): CanResult {
|
||||
case "setGsapKeyframe":
|
||||
case "addGsapKeyframe":
|
||||
case "removeGsapKeyframe":
|
||||
case "removeGsapProperty":
|
||||
case "removeGsapTween":
|
||||
case "deleteAllForSelector":
|
||||
case "removeLabel":
|
||||
if (getGsapScript(parsed.document) === null)
|
||||
return canErr(
|
||||
|
||||
@@ -102,7 +102,10 @@ export type EditOp =
|
||||
value: Record<string, unknown>;
|
||||
}
|
||||
| { type: "removeGsapKeyframe"; animationId: string; keyframeIndex: number }
|
||||
| { type: "removeGsapKeyframe"; animationId: string; percentage: number }
|
||||
| { type: "removeGsapProperty"; animationId: string; property: string; from?: boolean }
|
||||
| { type: "removeGsapTween"; animationId: string }
|
||||
| { type: "deleteAllForSelector"; selector: string }
|
||||
| { type: "addLabel"; name: string; position: number }
|
||||
| { type: "removeLabel"; name: string };
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@ import { useDomGeometryCommits } from "./useDomGeometryCommits";
|
||||
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
||||
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
||||
|
||||
// Re-export so existing consumers keep their import path
|
||||
export { GSAP_CSS_FALLBACK_BLOCKED_MESSAGE } from "./useDomGeometryCommits";
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
||||
@@ -45,8 +42,6 @@ interface RecordEditInput {
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}
|
||||
|
||||
export type { PersistDomEditOperations } from "./domEditCommitTypes";
|
||||
|
||||
export interface UseDomEditCommitsParams {
|
||||
activeCompPath: string | null;
|
||||
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
||||
|
||||
@@ -2,7 +2,11 @@ import { useCallback } from "react";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
||||
import { roundTo3 } from "../utils/rounding";
|
||||
import { sdkGsapTweenPersist, type CutoverDeps } from "../utils/sdkCutover";
|
||||
import {
|
||||
sdkGsapTweenPersist,
|
||||
sdkGsapDeleteAllForSelectorPersist,
|
||||
type CutoverDeps,
|
||||
} from "../utils/sdkCutover";
|
||||
import {
|
||||
assignGsapTargetAutoIdIfNeeded,
|
||||
ensureElementAddressable,
|
||||
@@ -80,15 +84,25 @@ export function useGsapAnimationOps({
|
||||
);
|
||||
|
||||
const deleteAllForSelector = useCallback(
|
||||
(selection: DomEditSelection, targetSelector: string) => {
|
||||
// ponytail: no SDK op for delete-all-for-selector; stays server-authoritative
|
||||
async (selection: DomEditSelection, targetSelector: string) => {
|
||||
if (sdkSession && sdkDeps) {
|
||||
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
||||
const handled = await sdkGsapDeleteAllForSelectorPersist(
|
||||
targetPath,
|
||||
targetSelector,
|
||||
sdkSession,
|
||||
sdkDeps,
|
||||
{ label: "Delete all animations for element" },
|
||||
);
|
||||
if (handled) return;
|
||||
}
|
||||
void commitMutation(
|
||||
selection,
|
||||
{ type: "delete-all-for-selector", targetSelector },
|
||||
{ label: "Delete all animations for element" },
|
||||
);
|
||||
},
|
||||
[commitMutation],
|
||||
[commitMutation, activeCompPath, sdkSession, sdkDeps],
|
||||
);
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
|
||||
@@ -3,7 +3,11 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
||||
import { executeOptimistic } from "../utils/optimisticUpdate";
|
||||
import { sdkGsapKeyframePersist, type CutoverDeps } from "../utils/sdkCutover";
|
||||
import {
|
||||
sdkGsapKeyframePersist,
|
||||
sdkGsapRemoveKeyframePersist,
|
||||
type CutoverDeps,
|
||||
} from "../utils/sdkCutover";
|
||||
import type { KeyframeCacheEntry } from "../player/store/playerStore";
|
||||
import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
|
||||
import { readKeyframeSnapshot, writeKeyframeCache } from "./gsapKeyframeCacheHelpers";
|
||||
@@ -151,9 +155,6 @@ export function useGsapKeyframeOps({
|
||||
|
||||
const removeKeyframe = useCallback(
|
||||
(selection: DomEditSelection, animationId: string, percentage: number) => {
|
||||
// ponytail: SDK removeGsapKeyframe uses keyframeIndex (not percentage); mismatch with
|
||||
// Studio's percentage-based API. Resolving index requires parsing GSAP state at call
|
||||
// time — deferred. removeKeyframe stays server-authoritative.
|
||||
const sourceFile = selection.sourceFile || activeCompPath || "index.html";
|
||||
const mutation = { type: "remove-keyframe", animationId, percentage };
|
||||
void executeOptimisticKeyframeCacheUpdate({
|
||||
@@ -162,19 +163,31 @@ export function useGsapKeyframeOps({
|
||||
apply: (prev) => ({
|
||||
...prev,
|
||||
keyframes: prev.keyframes.filter(
|
||||
(kf) => Math.abs((kf.tweenPercentage ?? kf.percentage) - percentage) > 0.2,
|
||||
(kf) => Math.abs((kf.tweenPercentage ?? kf.percentage) - percentage) > 0.001,
|
||||
),
|
||||
}),
|
||||
persist: () =>
|
||||
commitMutation(selection, mutation, {
|
||||
persist: async () => {
|
||||
if (sdkSession && sdkDeps) {
|
||||
const handled = await sdkGsapRemoveKeyframePersist(
|
||||
sourceFile,
|
||||
animationId,
|
||||
percentage,
|
||||
sdkSession,
|
||||
sdkDeps,
|
||||
{ label: `Remove keyframe at ${percentage}%` },
|
||||
);
|
||||
if (handled) return;
|
||||
}
|
||||
await commitMutation(selection, mutation, {
|
||||
label: `Remove keyframe at ${percentage}%`,
|
||||
softReload: true,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}).catch((error) => {
|
||||
trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
|
||||
});
|
||||
},
|
||||
[activeCompPath, commitMutation, trackGsapSaveFailure],
|
||||
[activeCompPath, commitMutation, trackGsapSaveFailure, sdkSession, sdkDeps],
|
||||
);
|
||||
|
||||
const convertToKeyframes = useCallback(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
||||
import { sdkGsapTweenPersist, type CutoverDeps } from "../utils/sdkCutover";
|
||||
import {
|
||||
sdkGsapTweenPersist,
|
||||
sdkGsapRemovePropertyPersist,
|
||||
type CutoverDeps,
|
||||
} from "../utils/sdkCutover";
|
||||
import { PROPERTY_DEFAULTS } from "./gsapScriptCommitHelpers";
|
||||
import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
|
||||
|
||||
@@ -110,16 +114,47 @@ export function useGsapPropertyDebounce(
|
||||
[commitMutationSafely, sdk],
|
||||
);
|
||||
|
||||
const removeGsapProperty = useCallback(
|
||||
(selection: DomEditSelection, animationId: string, property: string) => {
|
||||
// ponytail: null ≠ removal in upsertProp; remove-property stays server-authoritative
|
||||
commitMutationSafely(
|
||||
selection,
|
||||
{ type: "remove-property", animationId, property },
|
||||
{ label: `Remove GSAP ${property}` },
|
||||
);
|
||||
const removeProperty = useCallback(
|
||||
async (selection: DomEditSelection, animationId: string, property: string, from: boolean) => {
|
||||
const { sdkSession, sdkDeps, activeCompPath } = sdk ?? {};
|
||||
if (sdkSession && sdkDeps) {
|
||||
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
||||
const handled = await sdkGsapRemovePropertyPersist(
|
||||
targetPath,
|
||||
animationId,
|
||||
property,
|
||||
from,
|
||||
sdkSession,
|
||||
sdkDeps,
|
||||
{ label: `Remove GSAP ${from ? `from-${property}` : property}` },
|
||||
);
|
||||
if (handled) return;
|
||||
}
|
||||
if (from) {
|
||||
commitMutationSafely(
|
||||
selection,
|
||||
{ type: "remove-from-property", animationId, property },
|
||||
{
|
||||
label: `Remove GSAP from-${property}`,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
commitMutationSafely(
|
||||
selection,
|
||||
{ type: "remove-property", animationId, property },
|
||||
{
|
||||
label: `Remove GSAP ${property}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[commitMutationSafely],
|
||||
[commitMutationSafely, sdk],
|
||||
);
|
||||
|
||||
const removeGsapProperty = useCallback(
|
||||
(selection: DomEditSelection, animationId: string, property: string) =>
|
||||
removeProperty(selection, animationId, property, false),
|
||||
[removeProperty],
|
||||
);
|
||||
|
||||
const updateGsapFromProperty = useCallback(
|
||||
@@ -185,15 +220,9 @@ export function useGsapPropertyDebounce(
|
||||
);
|
||||
|
||||
const removeGsapFromProperty = useCallback(
|
||||
(selection: DomEditSelection, animationId: string, property: string) => {
|
||||
// ponytail: null ≠ removal in upsertProp; remove-from-property stays server-authoritative
|
||||
commitMutationSafely(
|
||||
selection,
|
||||
{ type: "remove-from-property", animationId, property },
|
||||
{ label: `Remove GSAP from-${property}` },
|
||||
);
|
||||
},
|
||||
[commitMutationSafely],
|
||||
(selection: DomEditSelection, animationId: string, property: string) =>
|
||||
removeProperty(selection, animationId, property, true),
|
||||
[removeProperty],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -192,32 +192,44 @@ type SdkGsapTweenOp =
|
||||
| { kind: "set"; animationId: string; properties: Partial<GsapTweenSpec> }
|
||||
| { kind: "remove"; animationId: string };
|
||||
|
||||
export async function sdkGsapTweenPersist(
|
||||
export function sdkGsapTweenPersist(
|
||||
targetPath: string,
|
||||
op: SdkGsapTweenOp,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
if (op.kind === "add" && sdkSession && !sdkSession.getElement(op.target))
|
||||
return Promise.resolve(false);
|
||||
// dispatchGsapOpAndPersist returns false on before===after — that catches stale
|
||||
// animationIds and unsupported shapes (e.g. from-prop on a plain tween), falling
|
||||
// back to the server path. This subsumes explicit existence guards for set/remove.
|
||||
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) => {
|
||||
s.batch(() => {
|
||||
if (op.kind === "add") {
|
||||
s.addGsapTween(op.target, op.spec);
|
||||
} else if (op.kind === "set") {
|
||||
s.setGsapTween(op.animationId, op.properties);
|
||||
} else {
|
||||
s.removeGsapTween(op.animationId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function dispatchGsapOpAndPersist(
|
||||
targetPath: string,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options: CutoverOptions | undefined,
|
||||
dispatch: (s: Composition) => void,
|
||||
): Promise<boolean> {
|
||||
if (!sdkSession) return false;
|
||||
if (wrongCompositionFile(deps, targetPath)) return false;
|
||||
try {
|
||||
if (op.kind === "add" && !sdkSession.getElement(op.target)) return false;
|
||||
const before = sdkSession.serialize();
|
||||
sdkSession.batch(() => {
|
||||
if (op.kind === "add") {
|
||||
sdkSession.addGsapTween(op.target, op.spec);
|
||||
} else if (op.kind === "set") {
|
||||
sdkSession.setGsapTween(op.animationId, op.properties);
|
||||
} else {
|
||||
sdkSession.removeGsapTween(op.animationId);
|
||||
}
|
||||
});
|
||||
dispatch(sdkSession);
|
||||
const after = sdkSession.serialize();
|
||||
// No-op (stale animationId, unsupported shape e.g. from-prop on a plain
|
||||
// tween): fall back to the server path so it surfaces the proper error
|
||||
// instead of writing a phantom before==after undo step. Subsumes a
|
||||
// per-op existence guard for the set/remove branches.
|
||||
if (after === before) return false;
|
||||
await persistSdkSerialize(after, targetPath, before, deps, options);
|
||||
trackStudioEvent("sdk_cutover_success", { opCount: 1 });
|
||||
@@ -228,7 +240,7 @@ export async function sdkGsapTweenPersist(
|
||||
}
|
||||
}
|
||||
|
||||
export async function sdkGsapKeyframePersist(
|
||||
export function sdkGsapKeyframePersist(
|
||||
targetPath: string,
|
||||
animationId: string,
|
||||
position: number,
|
||||
@@ -237,22 +249,48 @@ export async function sdkGsapKeyframePersist(
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
if (!sdkSession) return false;
|
||||
if (wrongCompositionFile(deps, targetPath)) return false;
|
||||
try {
|
||||
const before = sdkSession.serialize();
|
||||
sdkSession.batch(() =>
|
||||
sdkSession.dispatch({ type: "addGsapKeyframe", animationId, position, value }),
|
||||
);
|
||||
const after = sdkSession.serialize();
|
||||
if (after === before) return false;
|
||||
await persistSdkSerialize(after, targetPath, before, deps, options);
|
||||
trackStudioEvent("sdk_cutover_success", { opCount: 1 });
|
||||
return true;
|
||||
} catch (err) {
|
||||
trackStudioEvent("sdk_cutover_fallback", { error: String(err) });
|
||||
return false;
|
||||
}
|
||||
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
|
||||
s.batch(() => s.dispatch({ type: "addGsapKeyframe", animationId, position, value })),
|
||||
);
|
||||
}
|
||||
|
||||
export function sdkGsapRemoveKeyframePersist(
|
||||
targetPath: string,
|
||||
animationId: string,
|
||||
percentage: number,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
|
||||
s.dispatch({ type: "removeGsapKeyframe", animationId, percentage }),
|
||||
);
|
||||
}
|
||||
|
||||
export function sdkGsapRemovePropertyPersist(
|
||||
targetPath: string,
|
||||
animationId: string,
|
||||
property: string,
|
||||
from: boolean,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
|
||||
s.dispatch({ type: "removeGsapProperty", animationId, property, from }),
|
||||
);
|
||||
}
|
||||
|
||||
export function sdkGsapDeleteAllForSelectorPersist(
|
||||
targetPath: string,
|
||||
selector: string,
|
||||
sdkSession: Composition | null | undefined,
|
||||
deps: CutoverDeps,
|
||||
options?: CutoverOptions,
|
||||
): Promise<boolean> {
|
||||
return dispatchGsapOpAndPersist(targetPath, sdkSession, deps, options, (s) =>
|
||||
s.dispatch({ type: "deleteAllForSelector", selector }),
|
||||
);
|
||||
}
|
||||
|
||||
export async function sdkDeletePersist(
|
||||
|
||||
Reference in New Issue
Block a user