diff --git a/packages/studio/src/components/editor/manualEditsDom.ts b/packages/studio/src/components/editor/manualEditsDom.ts index e34b795a7..9ac4588a2 100644 --- a/packages/studio/src/components/editor/manualEditsDom.ts +++ b/packages/studio/src/components/editor/manualEditsDom.ts @@ -360,15 +360,21 @@ function writeStudioBoxSizeVars( element: HTMLElement, size: { width: number; height: number }, ): void { + // Keep the measurement on its own migration-safe guard. Elements drafted by + // an older Studio can already carry the box-size marker without these newer + // attributes; the next resize still reaches this function before its width + // and height are overwritten, so this is the last honest layout box to save. + // Offset sizes are layout values, so a running scale animation does not + // distort them. + if (!element.hasAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR)) { + element.setAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR, String(element.offsetWidth)); + } + if (!element.hasAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR)) { + element.setAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, String(element.offsetHeight)); + } if (!element.hasAttribute(STUDIO_BOX_SIZE_ATTR)) { element.setAttribute(STUDIO_ORIGINAL_WIDTH_ATTR, element.style.getPropertyValue("width")); element.setAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR, element.style.getPropertyValue("height")); - // Measured here and only here: this branch runs once, before the draft - // writes a width, so it is the last moment the element still has the box - // the user started with. Offset sizes are layout, so a scale animation - // running on the element does not distort them. - element.setAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR, String(element.offsetWidth)); - element.setAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, String(element.offsetHeight)); element.setAttribute( STUDIO_ORIGINAL_MIN_WIDTH_ATTR, element.style.getPropertyValue("min-width"), diff --git a/packages/studio/src/components/editor/manualEditsDomPatches.test.ts b/packages/studio/src/components/editor/manualEditsDomPatches.test.ts index 21edf4103..953e78b70 100644 --- a/packages/studio/src/components/editor/manualEditsDomPatches.test.ts +++ b/packages/studio/src/components/editor/manualEditsDomPatches.test.ts @@ -267,6 +267,20 @@ describe("buildBoxSizePatches / buildClearBoxSizePatches", () => { expect(ops.every((op) => op.value === null)).toBe(true); }); + it("backfills the measured box on a legacy element that already has the resize marker", () => { + const e = div(); + e.setAttribute(STUDIO_BOX_SIZE_ATTR, "true"); + Object.defineProperties(e, { + offsetWidth: { configurable: true, value: 630 }, + offsetHeight: { configurable: true, value: 252 }, + }); + + applyStudioBoxSize(e, { width: 320, height: 128 }); + + expect(e.getAttribute(STUDIO_ORIGINAL_BOX_WIDTH_ATTR)).toBe("630"); + expect(e.getAttribute(STUDIO_ORIGINAL_BOX_HEIGHT_ATTR)).toBe("252"); + }); + it("build/clear symmetry: clear addresses every {type,property} key that build emits", () => { const e = populatedBoxEl(); assertClearCoversKeys(buildBoxSizePatches(e), buildClearBoxSizePatches(e)); diff --git a/packages/studio/src/hooks/gsapResizeDropPoint.test.ts b/packages/studio/src/hooks/gsapResizeDropPoint.test.ts index ece5e9a11..19d5c816d 100644 --- a/packages/studio/src/hooks/gsapResizeDropPoint.test.ts +++ b/packages/studio/src/hooks/gsapResizeDropPoint.test.ts @@ -77,6 +77,21 @@ interface ResizeCase { positionWrite?: "static-set" | "keyframed-tween" | "none"; } +function createResizeElement(testCase: ResizeCase): HTMLElement { + const el = document.createElement("div"); + el.id = "clip"; + if (testCase.inlineSized) { + el.setAttribute("data-hf-studio-original-width", `${testCase.box.w}px`); + el.setAttribute("data-hf-studio-original-height", `${testCase.box.h}px`); + } else { + el.setAttribute("data-hf-studio-original-box-width", `${testCase.box.w}`); + el.setAttribute("data-hf-studio-original-box-height", `${testCase.box.h}`); + el.setAttribute("data-hf-studio-original-width", ""); + el.setAttribute("data-hf-studio-original-height", ""); + } + return el; +} + function scaleTween(longhand: boolean): GsapAnimation { const at = (v: number) => (longhand ? { scaleX: v, scaleY: v } : { scale: v }); return { @@ -180,17 +195,7 @@ function persistedScale(calls: unknown[][]): { x: number; y: number } | null { async function runCase(testCase: ResizeCase) { const rotation = testCase.rotation ?? 0; - const el = document.createElement("div"); - el.id = "clip"; - if (testCase.inlineSized) { - el.setAttribute("data-hf-studio-original-width", `${testCase.box.w}px`); - el.setAttribute("data-hf-studio-original-height", `${testCase.box.h}px`); - } else { - el.setAttribute("data-hf-studio-original-box-width", `${testCase.box.w}`); - el.setAttribute("data-hf-studio-original-box-height", `${testCase.box.h}`); - el.setAttribute("data-hf-studio-original-width", ""); - el.setAttribute("data-hf-studio-original-height", ""); - } + const el = createResizeElement(testCase); // What the gesture stamps at drag start, and the draft it leaves applied. el.setAttribute("data-hf-drag-gsap-base-x", `${testCase.base.x}`); el.setAttribute("data-hf-drag-gsap-base-y", `${testCase.base.y}`); diff --git a/packages/studio/src/hooks/gsapResizeIntercept.test.ts b/packages/studio/src/hooks/gsapResizeIntercept.test.ts index 715299709..ba4a03db0 100644 --- a/packages/studio/src/hooks/gsapResizeIntercept.test.ts +++ b/packages/studio/src/hooks/gsapResizeIntercept.test.ts @@ -293,6 +293,7 @@ it("non-uniform drag commits scaleX/scaleY longhands", async () => { * left it rendering at 7532px, over three times where it was dropped, and the * next drag compounded it. */ +// fallow-ignore-next-line code-duplication it("scales from the element's real box, not a hardcoded fallback", async () => { const el = document.createElement("div"); el.id = "clip"; @@ -339,6 +340,7 @@ it("scales from the element's real box, not a hardcoded fallback", async () => { * and the longhands won. The resize computed the right number, wrote it, and * the element snapped straight back to its old size on release. */ +// fallow-ignore-next-line code-duplication it("does not mix the scale shorthand into a tween that speaks longhands", async () => { const el = document.createElement("div"); el.id = "clip";