Skip to content

Commit 998b018

Browse files
committed
refactor(studio): delete the move writer no Studio path calls
1 parent 2eb1919 commit 998b018

8 files changed

Lines changed: 5 additions & 278 deletions

‎packages/studio/src/components/editor/anchoredResizeReleaseShift.test.ts‎

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
// @vitest-environment jsdom
22
import { afterEach, describe, expect, it } from "vitest";
3-
import { applyStudioBoxSize, applyStudioPathOffset, readStudioBoxSize } from "./manualEditsDom";
4-
import { reapplyPositionEditsAfterSeek } from "./manualEditsSeekReapply";
5-
import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches";
6-
import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag";
73
import { computeNextResizeAnchor } from "./domEditResizeLocal";
8-
import type { PatchOperation } from "../../utils/sourcePatcher";
9-
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
104

115
/**
126
* Center-anchored corner resize (CapCut model): the element scales about its
@@ -38,40 +32,6 @@ afterEach(() => {
3832
document.body.innerHTML = "";
3933
});
4034

41-
/** Apply a built PatchOperation[] to a live element, mirroring sourcePatcher's
42-
* inline-style / attribute application — i.e. what the persisted source carries
43-
* when it is re-parsed into the DOM on the next preview load. */
44-
function applyPatchesToElement(el: HTMLElement, ops: PatchOperation[]): void {
45-
for (const op of ops) {
46-
if (op.type === "inline-style") {
47-
if (op.value === null) el.style.removeProperty(op.property);
48-
else el.style.setProperty(op.property, op.value);
49-
} else if (op.type === "attribute") {
50-
if (op.value === null) el.removeAttribute(op.property);
51-
else el.setAttribute(op.property, op.value);
52-
}
53-
}
54-
}
55-
56-
/** Net translate applied to an element, resolving the studio offset var()
57-
* expression to its px value so we compare the actually-rendered translation. */
58-
function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } {
59-
const raw = el.style.getPropertyValue("translate").trim();
60-
if (!raw || raw === "none") return { x: 0, y: 0 };
61-
const vx = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-x")) || 0;
62-
const vy = Number.parseFloat(el.style.getPropertyValue("--hf-studio-offset-y")) || 0;
63-
const parts = splitTopLevelWhitespace(raw);
64-
const parseAxis = (part: string, varVal: number): number => {
65-
if (part && part.includes("--hf-studio-offset")) return varVal;
66-
const n = Number.parseFloat(part);
67-
return Number.isFinite(n) ? n : 0;
68-
};
69-
return {
70-
x: parseAxis(parts[0] ?? "", vx),
71-
y: parseAxis(parts[1] ?? "", vy),
72-
};
73-
}
74-
7535
describe("center-anchored corner resize — no shift after release", () => {
7636
it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => {
7737
// 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", () => {
157117
}
158118
}
159119
});
160-
161-
it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => {
162-
// The committed offset flows through the real apply → persist → reload chain
163-
// unchanged (this hop was proved clean; the shift is upstream in the anchor
164-
// loop above, not in persistence).
165-
const el = document.createElement("div");
166-
el.style.setProperty("width", "200px");
167-
el.style.setProperty("height", "100px");
168-
document.body.appendChild(el);
169-
170-
const anchorDx = -30;
171-
const anchorDy = -18;
172-
const finalSize = { width: 240, height: 130 };
173-
174-
applyStudioBoxSize(el, finalSize);
175-
const memberResult = createManualOffsetDragMember({
176-
key: "k",
177-
selection: { element: el } as never,
178-
element: el,
179-
rect: { left: 0, top: 0, width: 240, height: 130, editScaleX: 1, editScaleY: 1 },
180-
});
181-
expect(memberResult.ok).toBe(true);
182-
if (!memberResult.ok) return;
183-
184-
const finalOffset = applyManualOffsetDragCommit(memberResult.member, anchorDx, anchorDy);
185-
186-
applyStudioBoxSize(el, finalSize);
187-
const patches = buildBoxSizePatches(el);
188-
applyStudioPathOffset(el, finalOffset);
189-
patches.push(...buildPathOffsetPatches(el));
190-
191-
expect(resolvedTranslatePx(el)).toEqual({ x: anchorDx, y: anchorDy });
192-
193-
// Persist → fresh element re-parsed from source → reload re-stamp.
194-
const reloaded = document.createElement("div");
195-
reloaded.style.setProperty("width", "200px");
196-
reloaded.style.setProperty("height", "100px");
197-
document.body.appendChild(reloaded);
198-
applyPatchesToElement(reloaded, patches);
199-
reapplyPositionEditsAfterSeek(reloaded.ownerDocument);
200-
201-
expect(resolvedTranslatePx(reloaded)).toEqual({ x: anchorDx, y: anchorDy });
202-
expect(readStudioBoxSize(reloaded)).toEqual(finalSize);
203-
});
204120
});

‎packages/studio/src/components/editor/manualEditsDom.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ function stripGsapTranslateFromTransform(element: HTMLElement): void {
253253
// — as the non-GSAP path does — composes ON TOP of GSAP's transform, and the
254254
// subsequent strip/reapply math compounds into a runaway matrix that flings the
255255
// element off-canvas. So for GSAP-animated elements we keep `translate: none`
256-
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset is
257-
// still persisted (buildPathOffsetPatches), and GSAP re-reads it at init on
258-
// reload. Returns true when handled as GSAP (caller must skip the CSS path).
256+
// and push the offset straight into GSAP's x/y via gsap.set; the var() offset an
257+
// older Studio saved stays in the file, and GSAP re-reads it at init on reload.
258+
// Returns true when handled as GSAP (caller must skip the CSS path).
259259
// fallow-ignore-next-line complexity
260260
function applyStudioPathOffsetViaGsap(
261261
element: HTMLElement,

‎packages/studio/src/components/editor/manualEditsDomPatches.test.ts‎

Lines changed: 2 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import {
4242
STUDIO_MOTION_ORIGINAL_VISIBILITY_ATTR,
4343
} from "./studioMotionTypes";
4444
import {
45-
buildPathOffsetPatches,
4645
buildClearPathOffsetPatches,
4746
buildBoxSizePatches,
4847
buildClearBoxSizePatches,
@@ -51,7 +50,7 @@ import {
5150
buildMotionPatches,
5251
buildClearMotionPatches,
5352
} from "./manualEditsDomPatches";
54-
import { applyStudioBoxSize, applyStudioPathOffset } from "./manualEditsDom";
53+
import { applyStudioBoxSize } from "./manualEditsDom";
5554

5655
/* ── helpers ── */
5756

@@ -72,39 +71,7 @@ function assertClearCoversKeys(buildOps: PatchOperation[], clearOps: PatchOperat
7271

7372
/* ── Path offset ─────────────────────────────────────────────────────────── */
7473

75-
describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
76-
function populatedPathEl(): HTMLElement {
77-
const e = div();
78-
e.style.setProperty(STUDIO_OFFSET_X_PROP, "10px");
79-
e.style.setProperty(STUDIO_OFFSET_Y_PROP, "20px");
80-
e.style.setProperty("translate", "10px 20px");
81-
e.setAttribute(STUDIO_ORIGINAL_TRANSLATE_ATTR, "5px 10px");
82-
e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "3px");
83-
e.style.setProperty("display", "flex");
84-
e.setAttribute(STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, "block");
85-
return e;
86-
}
87-
88-
it("populated: captures offset styles, attrs, display, and transform-display marker in declaration order", () => {
89-
const ops = buildPathOffsetPatches(populatedPathEl());
90-
expect(ops).toEqual([
91-
{ type: "inline-style", property: STUDIO_OFFSET_X_PROP, value: "10px" },
92-
{ type: "inline-style", property: STUDIO_OFFSET_Y_PROP, value: "20px" },
93-
{ type: "inline-style", property: "translate", value: "10px 20px" },
94-
{ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
95-
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSLATE_ATTR, value: "5px 10px" },
96-
{ type: "attribute", property: STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, value: "3px" },
97-
{ type: "inline-style", property: "display", value: "flex" },
98-
{ type: "attribute", property: STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR, value: "block" },
99-
]);
100-
});
101-
102-
it("empty: bare element yields only the path-offset marker", () => {
103-
expect(buildPathOffsetPatches(div())).toEqual([
104-
{ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" },
105-
]);
106-
});
107-
74+
describe("buildClearPathOffsetPatches", () => {
10875
it("clear: restores translate from STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR and display from STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR", () => {
10976
const e = div();
11077
e.setAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR, "5px");
@@ -128,11 +95,6 @@ describe("buildPathOffsetPatches / buildClearPathOffsetPatches", () => {
12895
const ops = buildClearPathOffsetPatches(e);
12996
expect(ops.find((o) => o.property === "translate")?.value).toBeNull();
13097
});
131-
132-
it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => {
133-
const e = populatedPathEl();
134-
assertClearCoversKeys(buildPathOffsetPatches(e), buildClearPathOffsetPatches(e));
135-
});
13698
});
13799

138100
/* ── Box size ────────────────────────────────────────────────────────────── */
@@ -287,52 +249,6 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => {
287249
});
288250
});
289251

290-
/* ── Combined box-size + path-offset (anchored-corner resize) ──────────────── */
291-
292-
describe("anchored-corner combined patch: [...buildBoxSizePatches, ...buildPathOffsetPatches]", () => {
293-
// NW/NE/SW resize commits size AND anchor offset in ONE persist. The two
294-
// builders read the same already-mutated element and are concatenated; this
295-
// is only safe if their {type,property} keys are disjoint (no builder
296-
// overwrites the other's op when the source patcher applies them in order).
297-
it("concatenation of both builders emits disjoint {type,property} keys (no collision)", () => {
298-
const e = div();
299-
applyStudioBoxSize(e, { width: 300, height: 200 });
300-
applyStudioPathOffset(e, { x: 10, y: 20 });
301-
302-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
303-
const keys = combined.map(opKey);
304-
expect(new Set(keys).size, `duplicate {type,property} key in combined patch: ${keys}`).toBe(
305-
keys.length,
306-
);
307-
});
308-
309-
it("combined patch carries BOTH markers so a soft-reload re-hydrates size and offset together", () => {
310-
const e = div();
311-
applyStudioBoxSize(e, { width: 300, height: 200 });
312-
applyStudioPathOffset(e, { x: 10, y: 20 });
313-
314-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
315-
const has = (property: string) =>
316-
combined.some((op) => op.type === "attribute" && op.property === property);
317-
expect(has(STUDIO_BOX_SIZE_ATTR)).toBe(true);
318-
expect(has(STUDIO_PATH_OFFSET_ATTR)).toBe(true);
319-
});
320-
321-
it("order is size-first: every box-size op precedes every path-offset op", () => {
322-
const e = div();
323-
applyStudioBoxSize(e, { width: 300, height: 200 });
324-
applyStudioPathOffset(e, { x: 10, y: 20 });
325-
326-
const boxKeys = new Set(buildBoxSizePatches(e).map(opKey));
327-
const combined = [...buildBoxSizePatches(e), ...buildPathOffsetPatches(e)];
328-
const lastBoxIdx = combined.reduce((acc, op, i) => (boxKeys.has(opKey(op)) ? i : acc), -1);
329-
const firstOffsetIdx = combined.findIndex(
330-
(op) => op.type === "attribute" && op.property === STUDIO_PATH_OFFSET_ATTR,
331-
);
332-
expect(firstOffsetIdx).toBeGreaterThan(lastBoxIdx);
333-
});
334-
});
335-
336252
/* ── Rotation ────────────────────────────────────────────────────────────── */
337253

338254
describe("buildRotationPatches / buildClearRotationPatches", () => {

‎packages/studio/src/components/editor/manualEditsDomPatches.ts‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -72,36 +72,6 @@ function appendTransformDisplayOps(element: HTMLElement, ops: PatchOperation[]):
7272

7373
/* ── Path offset patches ─────────────────────────────────────────── */
7474

75-
export function buildPathOffsetPatches(element: HTMLElement): PatchOperation[] {
76-
const ops: PatchOperation[] = [];
77-
collectInlineStyleOps(element, [STUDIO_OFFSET_X_PROP, STUDIO_OFFSET_Y_PROP], ops);
78-
// When GSAP owns the element's transform, the live inline translate is kept
79-
// at "none" (the offset lives in GSAP's cache — see applyStudioPathOffset).
80-
// Persist the var() expression in that case, so a reload re-folds the offset.
81-
const inlineTranslate = element.style.getPropertyValue("translate");
82-
const hasOffsetVars =
83-
element.style.getPropertyValue(STUDIO_OFFSET_X_PROP) ||
84-
element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
85-
const translateValue =
86-
inlineTranslate && inlineTranslate !== "none"
87-
? inlineTranslate
88-
: hasOffsetVars
89-
? `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`
90-
: null;
91-
if (translateValue) {
92-
ops.push({ type: "inline-style", property: "translate", value: translateValue });
93-
}
94-
ops.push({ type: "attribute", property: STUDIO_PATH_OFFSET_ATTR, value: "true" });
95-
collectAttributeOps(
96-
element,
97-
[STUDIO_ORIGINAL_TRANSLATE_ATTR, STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR],
98-
ops,
99-
);
100-
collectInlineStyleOps(element, ["display"], ops);
101-
collectAttributeOps(element, [STUDIO_ORIGINAL_TRANSFORM_DISPLAY_ATTR], ops);
102-
return ops;
103-
}
104-
10575
export function buildClearPathOffsetPatches(element: HTMLElement): PatchOperation[] {
10676
const originalInlineTranslate = element.getAttribute(STUDIO_ORIGINAL_INLINE_TRANSLATE_ATTR);
10777
const ops: PatchOperation[] = [

‎packages/studio/src/components/editor/persistSeam.integration.test.ts‎

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import {
1414
buildDomEditStylePatchOperation,
1515
buildDomEditTextPatchOperation,
1616
} from "./domEditingLayers";
17-
import { buildPathOffsetPatches } from "./manualEditsDomPatches";
18-
import { STUDIO_OFFSET_X_PROP, STUDIO_PATH_OFFSET_ATTR } from "./manualEditsTypes";
1917
import { makeSelection } from "../../hooks/domSelectionTestHarness";
2018
import { buildTextFieldChildOperations } from "../../hooks/domEditTextFieldCommitOps";
2119

@@ -126,22 +124,6 @@ describe("persist seam source mutation", () => {
126124
expect(findByHfId(html, "qa-image").getAttribute("style")).toContain("opacity: 0.4");
127125
});
128126

129-
it("persists detached jsdom path offset operations", () => {
130-
const element = document.createElement("div");
131-
element.style.setProperty(STUDIO_OFFSET_X_PROP, "24px");
132-
133-
const html = patchAndExpectChange(
134-
indexHtml,
135-
clientTarget({ id: "qa-shape", hfId: "qa-shape", tagName: "div" }),
136-
buildPathOffsetPatches(element),
137-
);
138-
const shape = findByHfId(html, "qa-shape");
139-
140-
expect(shape.getAttribute("style")).toContain(`${STUDIO_OFFSET_X_PROP}: 24px`);
141-
expect(shape.getAttribute("style")).toContain("translate: var(--hf-studio-offset-x, 0px)");
142-
expect(shape.getAttribute(STUDIO_PATH_OFFSET_ATTR)).toBe("true");
143-
});
144-
145127
it("persists timeline data-start attribute operation", () => {
146128
const html = patchAndExpectChange(
147129
indexHtml,

‎packages/studio/src/hooks/useDomEditCommits.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ export function useDomEditCommits({
271271

272272
const {
273273
stageElementPositionOffset,
274-
handleDomPathOffsetCommit,
275274
handleDomBoxSizeCommit,
276275
handleDomRotationCommit,
277276
handleDomManualEditsReset,
@@ -316,7 +315,6 @@ export function useDomEditCommits({
316315
handleDomRemoveTextField,
317316
stageElementPositionOffset,
318317
commitPositionPatchToHtml,
319-
handleDomPathOffsetCommit,
320318
handleDomBoxSizeCommit,
321319
handleDomRotationCommit,
322320
handleDomManualEditsReset,

‎packages/studio/src/hooks/useDomGeometryCommits.test.tsx‎

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ describe("useDomGeometryCommits rollback", () => {
7070
}
7171

7272
act(() => root.render(<Probe />));
73-
await expect(commits!.handleDomPathOffsetCommit(selection, { x: 50, y: 60 })).rejects.toBe(
74-
failure,
75-
);
7673
await expect(
7774
commits!.handleDomBoxSizeCommit(selection, { width: 200, height: 160 }, { x: 30, y: 40 }),
7875
).rejects.toBe(failure);
@@ -90,20 +87,6 @@ describe("useDomGeometryCommits read-only preview", () => {
9087
const selectionOn = (element: HTMLElement) =>
9188
({ id: element.id, selector: `#${element.id}`, element }) as unknown as DomEditSelection;
9289

93-
it("refuses a manual offset commit: no write, no history entry", async () => {
94-
const element = document.createElement("div");
95-
element.id = "ro-offset";
96-
document.body.append(element);
97-
applyStudioPathOffset(element, { x: 1, y: 2 });
98-
const commitPositionPatchToHtml =
99-
vi.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>();
100-
const { commits, unmount } = mountCommits(commitPositionPatchToHtml, true);
101-
await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 99, y: 99 });
102-
expect(readStudioPathOffset(element)).toEqual({ x: 1, y: 2 });
103-
expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
104-
unmount();
105-
});
106-
10790
it("refuses a manual box-size commit: no write, no history entry", async () => {
10891
const element = document.createElement("div");
10992
element.id = "ro-size";
@@ -131,19 +114,6 @@ describe("useDomGeometryCommits read-only preview", () => {
131114
expect(commitPositionPatchToHtml).not.toHaveBeenCalled();
132115
unmount();
133116
});
134-
135-
it("still commits an offset with the flag off", async () => {
136-
const element = document.createElement("div");
137-
element.id = "rw-offset";
138-
document.body.append(element);
139-
const commitPositionPatchToHtml = vi
140-
.fn<UseDomGeometryCommitsParams["commitPositionPatchToHtml"]>()
141-
.mockResolvedValue(undefined);
142-
const { commits, unmount } = mountCommits(commitPositionPatchToHtml);
143-
await commits().handleDomPathOffsetCommit(selectionOn(element), { x: 5, y: 6 });
144-
expect(commitPositionPatchToHtml).toHaveBeenCalledTimes(1);
145-
unmount();
146-
});
147117
});
148118

149119
describe("useDomGeometryCommits element position offset", () => {

0 commit comments

Comments
 (0)