From 439257a1adf34686d9c405fff8197d9f7b842e77 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Wed, 30 Sep 2026 23:16:41 -0700 Subject: [PATCH] refactor(studio): delete the move writer no Studio path calls --- .fallowrc.jsonc | 10 --- .../editor/anchoredResizeReleaseShift.test.ts | 84 ------------------ .../src/components/editor/manualEditsDom.ts | 6 +- .../editor/manualEditsDomPatches.test.ts | 88 +------------------ .../editor/manualEditsDomPatches.ts | 30 ------- .../editor/persistSeam.integration.test.ts | 18 ---- packages/studio/src/hooks/gsapTargetCache.ts | 62 ------------- .../studio/src/hooks/useDomEditCommits.ts | 3 - .../studio/src/hooks/useDomGeometryCommit.ts | 1 - .../src/hooks/useDomGeometryCommits.test.tsx | 32 ------- .../studio/src/hooks/useDomGeometryCommits.ts | 42 --------- scripts/check-no-main-deletions.mjs | 4 + 12 files changed, 9 insertions(+), 371 deletions(-) delete mode 100644 packages/studio/src/hooks/gsapTargetCache.ts diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index da60f32e52..99968ceb77 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -62,9 +62,6 @@ "packages/studio/src/components/nle/TimelineEditorNotice.tsx", // Zoom hook extracted for downstream razor-blade PRs (#1330, #1331). "packages/studio/src/player/components/useTimelineZoom.ts", - // Cached O(1) GSAP target lookup, replacing O(n²) inline checks. - // Consumers migrate in a follow-up once useDomGeometryCommits adopts it. - "packages/studio/src/hooks/gsapTargetCache.ts", // Preview helper consumed dynamically from the studio iframe bridge. "packages/studio/src/hooks/gsapRuntimePreview.ts", ], @@ -370,13 +367,6 @@ "file": "packages/studio/src/player/lib/timelineDOM.ts", "exports": ["*"], }, - // gsapTargetCache: cached O(1) GSAP target lookup, consumed by - // useDomEditCommits and intended to replace the local copy in - // useDomGeometryCommits once callers migrate. - { - "file": "packages/studio/src/hooks/gsapTargetCache.ts", - "exports": ["isElementGsapTargeted"], - }, // Re-exports from useDomEditCommits: barrel-style re-exports // consumed by downstream studio code. { diff --git a/packages/studio/src/components/editor/anchoredResizeReleaseShift.test.ts b/packages/studio/src/components/editor/anchoredResizeReleaseShift.test.ts index 166f0e372c..51e88298ea 100644 --- a/packages/studio/src/components/editor/anchoredResizeReleaseShift.test.ts +++ b/packages/studio/src/components/editor/anchoredResizeReleaseShift.test.ts @@ -1,12 +1,6 @@ // @vitest-environment jsdom import { afterEach, describe, expect, it } from "vitest"; -import { applyStudioBoxSize, applyStudioPathOffset, readStudioBoxSize } from "./manualEditsDom"; -import { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply"; -import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches"; -import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag"; import { computeNextResizeAnchor } from "./domEditResizeLocal"; -import type { PatchOperation } from "../../utils/sourcePatcher"; -import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers"; /** * Center-anchored corner resize (CapCut model): the element scales about its @@ -38,40 +32,6 @@ afterEach(() => { document.body.innerHTML = ""; }); -/** Apply a built PatchOperation[] to a live element, mirroring sourcePatcher's - * inline-style / attribute application — i.e. what the persisted source carries - * when it is re-parsed into the DOM on the next preview load. */ -function applyPatchesToElement(el: HTMLElement, ops: PatchOperation[]): void { - for (const op of ops) { - if (op.type === "inline-style") { - if (op.value === null) el.style.removeProperty(op.property); - else el.style.setProperty(op.property, op.value); - } else if (op.type === "attribute") { - if (op.value === null) el.removeAttribute(op.property); - else el.setAttribute(op.property, op.value); - } - } -} - -/** Net translate applied to an element, resolving the studio offset var() - * expression to its px value so we compare the actually-rendered translation. */ -function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } { - const raw = el.style.getPropertyValue("translate").trim(); - if (!raw || raw === "none") return { x: 0, y: 0 }; - const vx = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-x")) || 0; - const vy = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-y")) || 0; - const parts = splitTopLevelWhitespace(raw); - const parseAxis = (part: string, varVal: number): number => { - if (part && part.includes("--hf-studio-offset")) return varVal; - const n = Number.parseFloat(part); - return Number.isFinite(n) ? n : 0; - }; - return { - x: parseAxis(parts[0] ?? "", vx), - y: parseAxis(parts[1] ?? "", vy), - }; -} - describe("center-anchored corner resize — no shift after release", () => { it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => { // Model the pointermove anchor loop that pins the element's CENTER. The physical @@ -157,48 +117,4 @@ describe("center-anchored corner resize — no shift after release", () => { } } }); - - it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => { - // The committed offset flows through the real apply → persist → reload chain - // unchanged (this hop was proved clean; the shift is upstream in the anchor - // loop above, not in persistence). - const el = document.createElement("div"); - el.style.setProperty("width", "200px"); - el.style.setProperty("height", "100px"); - document.body.appendChild(el); - - const anchorDx = -30; - const anchorDy = -18; - const finalSize = { width: 240, height: 130 }; - - applyStudioBoxSize(el, finalSize); - const memberResult = createManualOffsetDragMember({ - key: "k", - selection: { element: el } as never, - element: el, - rect: { left: 0, top: 0, width: 240, height: 130, editScaleX: 1, editScaleY: 1 }, - }); - expect(memberResult.ok).toBe(true); - if (!memberResult.ok) return; - - const finalOffset = applyManualOffsetDragCommit(memberResult.member, anchorDx, anchorDy); - - applyStudioBoxSize(el, finalSize); - const patches = buildBoxSizePatches(el); - applyStudioPathOffset(el, finalOffset); - patches.push(...buildPathOffsetPatches(el)); - - expect(resolvedTranslatePx(el)).toEqual({ x: anchorDx, y: anchorDy }); - - // Persist → fresh element re-parsed from source → reload re-stamp. - const reloaded = document.createElement("div"); - reloaded.style.setProperty("width", "200px"); - reloaded.style.setProperty("height", "100px"); - document.body.appendChild(reloaded); - applyPatchesToElement(reloaded, patches); - reapplyPositionEditsAfterSeek(reloaded.ownerDocument); - - expect(resolvedTranslatePx(reloaded)).toEqual({ x: anchorDx, y: anchorDy }); - expect(readStudioBoxSize(reloaded)).toEqual(finalSize); - }); }); diff --git a/packages/studio/src/components/editor/manualEditsDom.ts b/packages/studio/src/components/editor/manualEditsDom.ts index 092ea91cf5..2951ba4408 100644 --- a/packages/studio/src/components/editor/manualEditsDom.ts +++ b/packages/studio/src/components/editor/manualEditsDom.ts @@ -253,9 +253,9 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void { // — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the // subsequent strip/reapply math compounds into a runaway matrix that flings the // element off-canvas. So for GSAP-animated elements we keep `translate: none` -// and push the offset straight into GSAP's x/y via gsap.set; the var() offset is -// still persisted (buildPathOffsetPatches), and GSAP re-reads it at init on -// reload. Returns true when handled as GSAP (caller must skip the CSS path). +// and push the offset straight into GSAP's x/y via gsap.set; the var() offset an +// older Studio saved stays in the file, and GSAP re-reads it at init on reload. +// Returns true when handled as GSAP (caller must skip the CSS path). // fallow-ignore-next-line complexity function applyStudioPathOffsetViaGsap( element: HTMLElement, diff --git a/packages/studio/src/components/editor/manualEditsDomPatches.test.ts b/packages/studio/src/components/editor/manualEditsDomPatches.test.ts index 774d51aa91..4c9294f30b 100644 --- a/packages/studio/src/components/editor/manualEditsDomPatches.test.ts +++ b/packages/studio/src/components/editor/manualEditsDomPatches.test.ts @@ -42,7 +42,6 @@ import { STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR, } from "./studioMotionTypes"; import { - buildPathOffsetPatches, buildClearPathOffsetPatches, buildBoxSizePatches, buildClearBoxSizePatches, @@ -50,7 +49,7 @@ import { buildMotionPatches, buildClearMotionPatches, } from "./manualEditsDomPatches"; -import { applyStudioBoxSize, applyStudioPathOffset } from "./manualEditsDom"; +import { applyStudioBoxSize } from "./manualEditsDom"; /* ── helpers ── */ @@ -71,39 +70,7 @@ function assertClearCoversKeys(buildOps: PatchOperation[], clearOps: PatchOperat /* ── Path offset ─────────────────────────────────────────────────────────── */ -describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => { - function populatedPathEl(): HTMLElement { - const e = div(); - e.style.setProperty(STUDIO_OFFSET_X_PROP, "10px"); - e.style.setProperty(STUDIO_OFFSET_Y_PROP, "20px"); - e.style.setProperty("translate", "10px 20px"); - e.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, "5px 10px"); - e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "3px"); - e.style.setProperty("display", "flex"); - e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "block"); - return e; - } - - it("populated: captures offset styles, attrs, display, and transform-display marker in declaration order", () => { - const ops = buildPathOffsetPatches(populatedPathEl()); - expect(ops).toEqual([ - { type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: "10px" }, - { type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: "20px" }, - { type: "inline-style", property: "translate", value: "10px 20px" }, - { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" }, - { type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: "5px 10px" }, - { type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: "3px" }, - { type: "inline-style", property: "display", value: "flex" }, - { type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "block" }, - ]); - }); - - it("empty: bare element yields only the path-offset marker", () => { - expect(buildPathOffsetPatches(div())).toEqual([ - { type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" }, - ]); - }); - +describe("buildClearPathOffsetPatches", () => { it("clear: restores translate from STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR and display from STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR", () => { const e = div(); e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "5px"); @@ -127,11 +94,6 @@ describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => { const ops = buildClearPathOffsetPatches(e); expect(ops.find((o) => o.property === "translate")?.value).toBeNull(); }); - - it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => { - const e = populatedPathEl(); - assertClearCoversKeys(buildPathOffsetPatches(e), buildClearPathOffsetPatches(e)); - }); }); /* ── Box size ────────────────────────────────────────────────────────────── */ @@ -286,52 +248,6 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => { }); }); -/* ── Combined box-size + path-offset (anchored-corner resize) ──────────────── */ - -describe("anchored-corner combined patch: [...buildBoxSizePatches, ...buildPathOffsetPatches]", () => { - // NW/NE/SW resize commits size AND anchor offset in ONE persist. The two - // builders read the same already-mutated element and are concatenated; this - // is only safe if their {type,property} keys are disjoint (no builder - // overwrites the other's op when the source patcher applies them in order). - it("concatenation of both builders emits disjoint {type,property} keys (no collision)", () => { - const e = div(); - applyStudioBoxSize(e, { width: 300, height: 200 }); - applyStudioPathOffset(e, { x: 10, y: 20 }); - - const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)]; - const keys = combined.map(opKey); - expect(new Set(keys).size, `duplicate {type,property} key in combined patch: ${keys}`).toBe( - keys.length, - ); - }); - - it("combined patch carries BOTH markers so a soft-reload re-hydrates size and offset together", () => { - const e = div(); - applyStudioBoxSize(e, { width: 300, height: 200 }); - applyStudioPathOffset(e, { x: 10, y: 20 }); - - const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)]; - const has = (property: string) => - combined.some((op) => op.type === "attribute" && op.property === property); - expect(has(STUDIO_BOX_SIZE_ATTR)).toBe(true); - expect(has(STUDIO_PATH_OFFSET_ATTR)).toBe(true); - }); - - it("order is size-first: every box-size op precedes every path-offset op", () => { - const e = div(); - applyStudioBoxSize(e, { width: 300, height: 200 }); - applyStudioPathOffset(e, { x: 10, y: 20 }); - - const boxKeys = new Set(buildBoxSizePatches(e).map(opKey)); - const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)]; - const lastBoxIdx = combined.reduce((acc, op, i) => (boxKeys.has(opKey(op)) ? i : acc), -1); - const firstOffsetIdx = combined.findIndex( - (op) => op.type === "attribute" && op.property === STUDIO_PATH_OFFSET_ATTR, - ); - expect(firstOffsetIdx).toBeGreaterThan(lastBoxIdx); - }); -}); - /* ── Rotation ────────────────────────────────────────────────────────────── */ describe("buildClearRotationPatches", () => { diff --git a/packages/studio/src/components/editor/manualEditsDomPatches.ts b/packages/studio/src/components/editor/manualEditsDomPatches.ts index e2253aaa90..92b741d3e4 100644 --- a/packages/studio/src/components/editor/manualEditsDomPatches.ts +++ b/packages/studio/src/components/editor/manualEditsDomPatches.ts @@ -72,36 +72,6 @@ function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]): /* ── Path offset patches ─────────────────────────────────────────── */ -export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] { - const ops: PatchOperation[] = []; - collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP], ops); - // When GSAP owns the element's transform, the live inline translate is kept - // at "none" (the offset lives in GSAP's cache — see applyStudioPathOffset). - // Persist the var() expression in that case, so a reload re-folds the offset. - const inlineTranslate = element.style.getPropertyValue("translate"); - const hasOffsetVars = - element.style.getPropertyValue(STUDIO_OFFSET_X_PROP) || - element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP); - const translateValue = - inlineTranslate && inlineTranslate !== "none" - ? inlineTranslate - : hasOffsetVars - ? `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)` - : null; - if (translateValue) { - ops.push({ type: "inline-style", property: "translate", value: translateValue }); - } - ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" }); - collectAttributeOps( - element, - [STUDIO_ORIGINAL_TRANSLATE_ATTR, STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR], - ops, - ); - collectInlineStyleOps(element, ["display"], ops); - collectAttributeOps(element, [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR], ops); - return ops; -} - export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] { const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR); const ops: PatchOperation[] = [ diff --git a/packages/studio/src/components/editor/persistSeam.integration.test.ts b/packages/studio/src/components/editor/persistSeam.integration.test.ts index e3ff608861..b2fdd9d2e6 100644 --- a/packages/studio/src/components/editor/persistSeam.integration.test.ts +++ b/packages/studio/src/components/editor/persistSeam.integration.test.ts @@ -14,8 +14,6 @@ import { buildDomEditStylePatchOperation, buildDomEditTextPatchOperation, } from "./domEditingLayers"; -import { buildPathOffsetPatches } from "./manualEditsDomPatches"; -import { STUDIO_OFFSET_X_PROP, STUDIO_PATH_OFFSET_ATTR } from "./manualEditsTypes"; import { makeSelection } from "../../hooks/domSelectionTestHarness"; import { buildTextFieldChildOperations } from "../../hooks/domEditTextFieldCommitOps"; @@ -126,22 +124,6 @@ describe("persist seam source mutation", () => { expect(findByHfId(html, "qa-image").getAttribute("style")).toContain("opacity: 0.4"); }); - it("persists detached jsdom path offset operations", () => { - const element = document.createElement("div"); - element.style.setProperty(STUDIO_OFFSET_X_PROP, "24px"); - - const html = patchAndExpectChange( - indexHtml, - clientTarget({ id: "qa-shape", hfId: "qa-shape", tagName: "div" }), - buildPathOffsetPatches(element), - ); - const shape = findByHfId(html, "qa-shape"); - - expect(shape.getAttribute("style")).toContain(`${STUDIO_OFFSET_X_PROP}: 24px`); - expect(shape.getAttribute("style")).toContain("translate: var(--hf-studio-offset-x, 0px)"); - expect(shape.getAttribute(STUDIO_PATH_OFFSET_ATTR)).toBe("true"); - }); - it("persists timeline data-start attribute operation", () => { const html = patchAndExpectChange( indexHtml, diff --git a/packages/studio/src/hooks/gsapTargetCache.ts b/packages/studio/src/hooks/gsapTargetCache.ts deleted file mode 100644 index a5ba2b8387..0000000000 --- a/packages/studio/src/hooks/gsapTargetCache.ts +++ /dev/null @@ -1,62 +0,0 @@ -type TimelineLike = { getChildren?: (nested: boolean) => Array<{ targets?: () => Element[] }> }; - -let _gsapCachedTimelines: Record | undefined; -let _gsapTargetIds: Set | undefined; -let _gsapTargetNodes: WeakSet | undefined; - -function addTargetsFromTimeline(tl: TimelineLike, ids: Set, nodes: WeakSet): void { - const children = tl.getChildren?.(true); - if (!children) return; - for (const child of children) { - const targets = child.targets?.(); - if (!targets) continue; - for (const t of targets) { - nodes.add(t); - if (t.id) ids.add(t.id); - } - } -} - -function collectGsapTargets(timelines: Record): { - ids: Set; - nodes: WeakSet; -} { - const ids = new Set(); - const nodes = new WeakSet(); - for (const tl of Object.values(timelines)) { - if (!tl) continue; - try { - addTargetsFromTimeline(tl, ids, nodes); - } catch { - /* teardown race */ - } - } - return { ids, nodes }; -} - -function readTimelines(iframe: HTMLIFrameElement | null): Record | undefined { - if (!iframe?.contentWindow) return undefined; - try { - return (iframe.contentWindow as Window & { __timelines?: Record }) - .__timelines; - } catch { - return undefined; - } -} - -export function isElementGsapTargeted( - iframe: HTMLIFrameElement | null, - element: HTMLElement, -): boolean { - const timelines = readTimelines(iframe); - if (!timelines) return false; - - if (timelines !== _gsapCachedTimelines) { - const cache = collectGsapTargets(timelines); - _gsapTargetIds = cache.ids; - _gsapTargetNodes = cache.nodes; - _gsapCachedTimelines = timelines; - } - - return _gsapTargetNodes!.has(element) || !!(element.id && _gsapTargetIds!.has(element.id)); -} diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts index fa36a4f12a..2c95cb5002 100644 --- a/packages/studio/src/hooks/useDomEditCommits.ts +++ b/packages/studio/src/hooks/useDomEditCommits.ts @@ -271,12 +271,10 @@ export function useDomEditCommits({ const { stageElementPositionOffset, - handleDomPathOffsetCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, } = useDomGeometryCommits({ - previewIframeRef, showToast, commitPositionPatchToHtml, readOnlyPreview, @@ -316,7 +314,6 @@ export function useDomEditCommits({ handleDomRemoveTextField, stageElementPositionOffset, commitPositionPatchToHtml, - handleDomPathOffsetCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, diff --git a/packages/studio/src/hooks/useDomGeometryCommit.ts b/packages/studio/src/hooks/useDomGeometryCommit.ts index d112debcb2..c730430e7e 100644 --- a/packages/studio/src/hooks/useDomGeometryCommit.ts +++ b/packages/studio/src/hooks/useDomGeometryCommit.ts @@ -111,7 +111,6 @@ export function useDomGeometryCommit({ ); const { stageElementPositionOffset, handleDomBoxSizeCommit, handleDomRotationCommit } = useDomGeometryCommits({ - previewIframeRef: iframeRef, showToast, commitPositionPatchToHtml: commitWithFreshQueue, readOnlyPreview: false, diff --git a/packages/studio/src/hooks/useDomGeometryCommits.test.tsx b/packages/studio/src/hooks/useDomGeometryCommits.test.tsx index 1f8c0cd20c..aa12ffb9ee 100644 --- a/packages/studio/src/hooks/useDomGeometryCommits.test.tsx +++ b/packages/studio/src/hooks/useDomGeometryCommits.test.tsx @@ -25,7 +25,6 @@ function mountCommits( let commits: ReturnType | null = null; function Probe() { commits = useDomGeometryCommits({ - previewIframeRef: { current: null }, showToast: vi.fn(), commitPositionPatchToHtml, readOnlyPreview, @@ -61,7 +60,6 @@ describe("useDomGeometryCommits rollback", () => { function Probe() { commits = useDomGeometryCommits({ - previewIframeRef: { current: null }, showToast: vi.fn(), commitPositionPatchToHtml, readOnlyPreview: false, @@ -70,9 +68,6 @@ describe("useDomGeometryCommits rollback", () => { } act(() => root.render()); - await expect(commits!.handleDomPathOffsetCommit(selection, { x: 50, y: 60 })).rejects.toBe( - failure, - ); await expect( commits!.handleDomBoxSizeCommit(selection, { width: 200, height: 160 }, { x: 30, y: 40 }), ).rejects.toBe(failure); @@ -90,20 +85,6 @@ describe("useDomGeometryCommits read-only preview", () => { const selectionOn = (element: HTMLElement) => ({ id: element.id, selector: `#${element.id}`, element }) as unknown as DomEditSelection; - it("refuses a manual offset commit: no write, no history entry", async () => { - const element = document.createElement("div"); - element.id = "ro-offset"; - document.body.append(element); - applyStudioPathOffset(element, { x: 1, y: 2 }); - const commitPositionPatchToHtml = - vi.fn(); - const { commits, unmount } = mountCommits(commitPositionPatchToHtml, true); - await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 99, y: 99 }); - expect(readStudioPathOffset(element)).toEqual({ x: 1, y: 2 }); - expect(commitPositionPatchToHtml).not.toHaveBeenCalled(); - unmount(); - }); - it("refuses a manual box-size commit: no write, no history entry", async () => { const element = document.createElement("div"); element.id = "ro-size"; @@ -131,19 +112,6 @@ describe("useDomGeometryCommits read-only preview", () => { expect(commitPositionPatchToHtml).not.toHaveBeenCalled(); unmount(); }); - - it("still commits an offset with the flag off", async () => { - const element = document.createElement("div"); - element.id = "rw-offset"; - document.body.append(element); - const commitPositionPatchToHtml = vi - .fn() - .mockResolvedValue(undefined); - const { commits, unmount } = mountCommits(commitPositionPatchToHtml); - await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 5, y: 6 }); - expect(commitPositionPatchToHtml).toHaveBeenCalledTimes(1); - unmount(); - }); }); describe("useDomGeometryCommits element position offset", () => { diff --git a/packages/studio/src/hooks/useDomGeometryCommits.ts b/packages/studio/src/hooks/useDomGeometryCommits.ts index 5f0e7aed8d..18ca65cf3c 100644 --- a/packages/studio/src/hooks/useDomGeometryCommits.ts +++ b/packages/studio/src/hooks/useDomGeometryCommits.ts @@ -2,7 +2,6 @@ import type { RotationCommit } from "../components/editor/rotationDraft"; import { useCallback } from "react"; import { getDomEditTargetKey, type DomEditSelection } from "../components/editor/domEditing"; import { - applyStudioPathOffset, applyStudioBoxSize, captureStudioPathOffset, captureStudioBoxSize, @@ -19,33 +18,16 @@ import { savePlainRotation } from "./plainRotation"; import { prepareCropResize } from "../components/editor/cropResize"; import { translatePatch, writeTranslatePx } from "../components/editor/plainTranslate"; import { - buildPathOffsetPatches, buildBoxSizePatches, buildClearPathOffsetPatches, buildClearBoxSizePatches, buildClearRotationPatches, } from "../components/editor/manualEditsDomPatches"; import type { PatchOperation } from "../utils/sourcePatcher"; -import { isElementGsapTargeted } from "./gsapTargetCache"; - -const GSAP_CSS_FALLBACK_BLOCKED_MESSAGE = - "This element is GSAP-animated — dragging via CSS would corrupt keyframes"; - -function rejectGsapCssFallback( - selection: DomEditSelection, - previewIframeRef: React.MutableRefObject, - showToast: (message: string, tone?: "error" | "info") => void, -): Promise | null { - if (!isElementGsapTargeted(previewIframeRef.current, selection.element)) return null; - const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE); - showToast(error.message, "error"); - return Promise.reject(error); -} // ── Hook ── export interface UseDomGeometryCommitsParams { - previewIframeRef: React.MutableRefObject; showToast: (message: string, tone?: "error" | "info") => void; commitPositionPatchToHtml: ( selection: DomEditSelection, @@ -56,7 +38,6 @@ export interface UseDomGeometryCommitsParams { } export function useDomGeometryCommits({ - previewIframeRef, showToast, commitPositionPatchToHtml, readOnlyPreview, @@ -78,28 +59,6 @@ export function useDomGeometryCommits({ [commitPositionPatchToHtml, readOnlyPreview, showToast], ); - const handleDomPathOffsetCommit = useCallback( - (selection: DomEditSelection, next: { x: number; y: number }) => { - if (readOnlyPreview) return Promise.resolve(); - // ponytail: GSAP-targeted elements are blocked (no SDK position-in-script op); CSS-path - // elements fall through to commitPositionPatchToHtml → persistDomEditOperations → - // onTrySdkPersist and are already SDK-cut-over as setStyle/setAttribute (§3.3 done). - // Upgrade path for GSAP: add a moveElementGsap SDK op in a separate SDK PR. - const gsapFallback = rejectGsapCssFallback(selection, previewIframeRef, showToast); - if (gsapFallback) return gsapFallback; - const before = captureStudioPathOffset(selection.element); - applyStudioPathOffset(selection.element, next); - return commitPositionPatchToHtml(selection, buildPathOffsetPatches(selection.element), { - label: "Move layer", - coalesceKey: `path-offset:${getDomEditTargetKey(selection)}`, - }).catch((error) => { - restoreStudioPathOffset(selection.element, before); - throw error; - }); - }, - [commitPositionPatchToHtml, previewIframeRef, showToast, readOnlyPreview], - ); - const handleDomBoxSizeCommit = useCallback( ( selection: DomEditSelection, @@ -173,7 +132,6 @@ export function useDomGeometryCommits({ return { stageElementPositionOffset, - handleDomPathOffsetCommit, handleDomBoxSizeCommit, handleDomRotationCommit, handleDomManualEditsReset, diff --git a/scripts/check-no-main-deletions.mjs b/scripts/check-no-main-deletions.mjs index f9b3c3a2e7..f4155dda4e 100644 --- a/scripts/check-no-main-deletions.mjs +++ b/scripts/check-no-main-deletions.mjs @@ -43,6 +43,10 @@ const SIMULATED_CURSOR_REASON = "owner-directed removal of simulated-cursor; the Cursors section's new pointer animations replace it"; export const ALLOWED_DELETIONS = new Map([ + [ + "packages/studio/src/hooks/gsapTargetCache.ts", + "its only caller was the deleted var-offset move writer; nothing imports isElementGsapTargeted", + ], [ "packages/studio/src/player/components/automationGestureKeys.ts", "automation-lane saves now persist once per gesture through the timeline save, so no caller needs a gesture undo key",