From 8fd5bf8f51dbe42be6b626cc33e52b1e38a8ef3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 19 May 2026 15:16:09 -0400 Subject: [PATCH 1/3] fix(studio): remove Ask agent popup, fix preview selection, fix manual edits in renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three interrelated studio UX and rendering fixes: 1. Remove the "Ask agent" popup that auto-triggered when clicking large raster elements in the preview. The modal intercepted clicks meant for editable elements and blocked normal selection workflow. 2. Rewrite preview click selection to respect visual stacking order. The previous scoring algorithm weighted DOM depth at 10,000× per level, causing elements inside sub-compositions to beat visually- on-top elements (e.g., clicking Pip Studio selected Sf Chrome instead). The new algorithm trusts elementsFromPoint order and only prefers a deeper candidate when it is a descendant of the current pick — never jumping to an unrelated element painted behind it. 3. Fix manual edits (resize) not surviving video rendering. The producer's seek-reapply script handled translate and rotation but was missing box-size (width/height) reapplication after each GSAP seek. Also added data-hf-studio-box-size to the detection list in htmlCompiler so the script is injected for resize-only edits. --- .../helpers/manualEditsRenderScript.ts | 13 +++++ .../producer/src/services/htmlCompiler.ts | 1 + .../components/editor/domEditingElement.ts | 51 +++++++------------ .../studio/src/hooks/useDomEditSession.ts | 5 -- .../studio/src/hooks/usePreviewInteraction.ts | 42 +-------------- 5 files changed, 35 insertions(+), 77 deletions(-) diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts index 976f49df0..e3cc14330 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts @@ -25,8 +25,11 @@ export function createStudioPositionSeekReapplyScript(): string { function studioPositionSeekReapplyRuntime(): void { const OFFSET_X_PROP = "--hf-studio-offset-x"; const OFFSET_Y_PROP = "--hf-studio-offset-y"; + const WIDTH_PROP = "--hf-studio-width"; + const HEIGHT_PROP = "--hf-studio-height"; const ROTATION_PROP = "--hf-studio-rotation"; const PATH_OFFSET_ATTR = "data-hf-studio-path-offset"; + const BOX_SIZE_ATTR = "data-hf-studio-box-size"; const ROTATION_ATTR = "data-hf-studio-rotation"; const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate"; const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate"; @@ -36,6 +39,7 @@ function studioPositionSeekReapplyRuntime(): void { if ( !document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && + !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]") ) @@ -209,6 +213,15 @@ function studioPositionSeekReapplyRuntime(): void { ); } } + const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]'); + for (let i = 0; i < boxSizeEls.length; i++) { + const el = boxSizeEls[i] as HTMLElement; + if (!(el instanceof HTMLElement)) continue; + const w = el.style.getPropertyValue(WIDTH_PROP); + const h = el.style.getPropertyValue(HEIGHT_PROP); + if (w) el.style.setProperty("width", w); + if (h) el.style.setProperty("height", h); + } const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]'); for (let i = 0; i < rotEls.length; i++) { const el = rotEls[i] as HTMLElement; diff --git a/packages/producer/src/services/htmlCompiler.ts b/packages/producer/src/services/htmlCompiler.ts index 4b9362404..5894a3e58 100644 --- a/packages/producer/src/services/htmlCompiler.ts +++ b/packages/producer/src/services/htmlCompiler.ts @@ -904,6 +904,7 @@ export async function compileForRender( // positions survive frame-by-frame rendering without a JSON sidecar. const HF_POSITION_ATTRS = [ 'data-hf-studio-path-offset="true"', + 'data-hf-studio-box-size="true"', 'data-hf-studio-rotation="true"', 'data-hf-studio-motion="', ]; diff --git a/packages/studio/src/components/editor/domEditingElement.ts b/packages/studio/src/components/editor/domEditingElement.ts index 4a339c859..7d986234a 100644 --- a/packages/studio/src/components/editor/domEditingElement.ts +++ b/packages/studio/src/components/editor/domEditingElement.ts @@ -12,11 +12,9 @@ import type { import { buildStableSelector, escapeCssString, - getElementDepth, getSelectorIndex, getSourceFileForElement, isHtmlElement, - isTextBearingTag, normalizeTimelineCompositionSource, querySelectorAllSafely, } from "./domEditingDom"; @@ -68,23 +66,6 @@ function hasRenderedBox(el: HTMLElement): boolean { // ─── Visual scoring ────────────────────────────────────────────────────────── -function isEditableTextLeafForScoring(el: HTMLElement): boolean { - return isTextBearingTag(el.tagName.toLowerCase()) && el.children.length === 0; -} - -function getVisualElementScore(el: HTMLElement, pointerStackIndex: number): number { - const tagName = el.tagName.toLowerCase(); - const rect = el.getBoundingClientRect(); - const area = Math.max(1, rect.width * rect.height); - const smallerElementBonus = Math.max(0, 1_000_000 - Math.min(area, 1_000_000)) / 1_000; - const visualLeafBonus = - isEditableTextLeafForScoring(el) || ["img", "video", "canvas", "svg"].includes(tagName) - ? 2_000 - : 0; - - return getElementDepth(el) * 10_000 + visualLeafBonus + smallerElementBonus - pointerStackIndex; -} - // ─── Layer patch target ────────────────────────────────────────────────────── const DOM_LAYER_IGNORED_TAGS = new Set([ @@ -172,25 +153,31 @@ export function resolveVisualDomEditSelectionTarget( elementsFromPoint: Iterable, options: Pick, ): HTMLElement | null { - let best: { element: HTMLElement; score: number } | null = null; - let pointerStackIndex = 0; + const candidates: HTMLElement[] = []; for (const entry of elementsFromPoint) { - if (!isHtmlElement(entry)) { - pointerStackIndex += 1; - continue; - } - + if (!isHtmlElement(entry)) continue; if (hasRenderedBox(entry) && getDomLayerPatchTarget(entry, options.activeCompositionPath)) { - const score = getVisualElementScore(entry, pointerStackIndex); - if (!best || score > best.score) { - best = { element: entry, score }; - } + candidates.push(entry); } - pointerStackIndex += 1; } - return best?.element ?? null; + if (candidates.length === 0) return null; + + // candidates are in visual stacking order (topmost first, from elementsFromPoint). + // Start with the topmost and only replace with a descendant that is more + // specific within the same visual subtree. Never jump to an unrelated + // element that happens to be painted behind the current pick. + let best = candidates[0]; + + for (let i = 1; i < candidates.length; i++) { + const candidate = candidates[i]; + if (best.contains(candidate)) { + best = candidate; + } + } + + return best; } // ─── Raster detection ──────────────────────────────────────────────────────── diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index ba0260e06..d33508440 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -151,7 +151,6 @@ export function useDomEditSession({ setAgentModalOpen, setAgentPromptSelectionContext, setAgentModalAnchorPoint, - preloadAgentPromptSnippet, handleAskAgent, handleAgentModalSubmit, } = useAskAgentModal({ @@ -181,10 +180,6 @@ export function useDomEditSession({ applyDomSelection, resolveDomSelectionFromPreviewPoint, updateDomEditHoverSelection, - preloadAgentPromptSnippet, - setAgentPromptSelectionContext, - setAgentModalAnchorPoint, - setAgentModalOpen, onClickToSource, }); diff --git a/packages/studio/src/hooks/usePreviewInteraction.ts b/packages/studio/src/hooks/usePreviewInteraction.ts index 289acc1e9..0da7e6645 100644 --- a/packages/studio/src/hooks/usePreviewInteraction.ts +++ b/packages/studio/src/hooks/usePreviewInteraction.ts @@ -1,16 +1,8 @@ import { useCallback } from "react"; import { liveTime, usePlayerStore } from "../player"; -import { - getPreviewLocalPointer, - buildRasterClickSelectionContext, - pauseStudioPreviewPlayback, -} from "../utils/studioPreviewHelpers"; +import { pauseStudioPreviewPlayback } from "../utils/studioPreviewHelpers"; import { STUDIO_PREVIEW_SELECTION_ENABLED } from "../components/editor/manualEditingAvailability"; -import { - isLargeRasterDomEditSelection, - type DomEditSelection, -} from "../components/editor/domEditing"; -import type { AgentModalAnchorPoint } from "../utils/studioHelpers"; +import { type DomEditSelection } from "../components/editor/domEditing"; // ── Types ── @@ -32,12 +24,6 @@ export interface UsePreviewInteractionParams { ) => DomEditSelection | null; updateDomEditHoverSelection: (selection: DomEditSelection | null) => void; - // From useAskAgentModal - preloadAgentPromptSnippet: (selection: DomEditSelection) => Promise; - setAgentPromptSelectionContext: (context: string | undefined) => void; - setAgentModalAnchorPoint: (point: AgentModalAnchorPoint | null) => void; - setAgentModalOpen: (open: boolean) => void; - onClickToSource?: (selection: DomEditSelection) => void; } @@ -51,10 +37,6 @@ export function usePreviewInteraction({ applyDomSelection, resolveDomSelectionFromPreviewPoint, updateDomEditHoverSelection, - preloadAgentPromptSnippet, - setAgentPromptSelectionContext, - setAgentModalAnchorPoint, - setAgentModalOpen, onClickToSource, }: UsePreviewInteractionParams) { const handlePreviewCanvasMouseDown = useCallback( @@ -69,37 +51,17 @@ export function usePreviewInteraction({ } e.preventDefault(); e.stopPropagation(); - const localPointer = previewIframeRef.current - ? getPreviewLocalPointer(previewIframeRef.current, e.clientX, e.clientY) - : null; applyDomSelection(nextSelection, { additive: e.shiftKey }); if (!e.shiftKey && e.altKey && onClickToSource) { onClickToSource(nextSelection); } - if ( - !e.shiftKey && - localPointer && - isLargeRasterDomEditSelection(nextSelection, localPointer.viewport) - ) { - setAgentPromptSelectionContext( - buildRasterClickSelectionContext(nextSelection, localPointer), - ); - setAgentModalAnchorPoint({ x: e.clientX, y: e.clientY }); - void preloadAgentPromptSnippet(nextSelection); - setAgentModalOpen(true); - } }, [ applyDomSelection, captionEditMode, compositionLoading, onClickToSource, - preloadAgentPromptSnippet, resolveDomSelectionFromPreviewPoint, - previewIframeRef, - setAgentModalAnchorPoint, - setAgentModalOpen, - setAgentPromptSelectionContext, ], ); From 6498807e1aeafdbe47f7caa9159b988d73a15443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 19 May 2026 15:32:45 -0400 Subject: [PATCH 2/3] fix(core): strip GSAP translate from transform after reapplying manual edits When GSAP animates an element (e.g. scale, opacity), it captures the element's translate into its internal transform matrix (m41/m42). The render reapply script was setting the CSS `translate` property but leaving GSAP's translate baked into `transform`, causing the manual edit offset to be ignored or doubled. Port the same stripGsapTranslateFromTransform logic the studio uses: parse the transform matrix, zero out m41/m42, and remove or rewrite the transform property so the CSS `translate` takes effect cleanly. --- .../helpers/manualEditsRenderScript.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts index e3cc14330..cc4106fa5 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts @@ -195,6 +195,27 @@ function studioPositionSeekReapplyRuntime(): void { (tl.totalTime as (t: number, s: boolean) => void)(lastSeekTime, false); }; + const stripGsapTranslateFromTransform = (el: HTMLElement): void => { + const transform = el.style.getPropertyValue("transform"); + if (!transform || transform === "none") return; + const win = el.ownerDocument.defaultView as (Window & typeof globalThis) | null; + const MatrixCtor = (win as unknown as { DOMMatrix?: typeof DOMMatrix })?.DOMMatrix; + if (!MatrixCtor) return; + try { + const m = new MatrixCtor(transform); + if (m.m41 === 0 && m.m42 === 0) return; + m.m41 = 0; + m.m42 = 0; + if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) { + el.style.removeProperty("transform"); + } else { + el.style.setProperty("transform", m.toString()); + } + } catch { + /* non-parseable transform — leave as-is */ + } + }; + const reapplyAll = (): void => { const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]'); for (let i = 0; i < offsetEls.length; i++) { @@ -211,6 +232,7 @@ function studioPositionSeekReapplyRuntime(): void { "var(" + OFFSET_Y_PROP + ", 0px)", ), ); + stripGsapTranslateFromTransform(el); } } const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]'); From f7d63fbebd890684958775ed42131a65fc60c0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 19 May 2026 16:05:37 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(studio):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20add=20tests,=20strip=20GSAP=20transform=20for=20rot?= =?UTF-8?q?ation,=20remove=20dead=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback from Rames and Vai: 1. Add 7 new tests for createStudioPositionSeekReapplyScript: box-size reapplication, GSAP translate stripping (identity removal, scale+translate preservation, transform:none no-op), and rotation- only elements with GSAP-baked translate. 2. Add pinning test for the PiP-over-sub-composition selection bug: elementsFromPoint returns [pipVideo, subCompRoot, sfChromeImg] as siblings — assert the topmost (pipVideo) wins. 3. Apply stripGsapTranslateFromTransform to rotation-only elements too, not just path-offset elements. A rotation-only element with a GSAP-animated translate would have its position clobbered. 4. Remove dead exports: getPreviewLocalPointer, buildRasterClickSelectionContext, getPreviewPlayer, seekStudioPreview, PreviewPlayerCompat, PreviewLocalPointer from studioPreviewHelpers.ts. Unexport resolvePreviewLocalPointer. --- .filesize-allowlist | 1 + .../helpers/manualEditsRenderScript.test.ts | 184 +++++++++++++++++- .../helpers/manualEditsRenderScript.ts | 1 + .../src/components/editor/domEditing.test.ts | 23 +++ .../studio/src/utils/studioPreviewHelpers.ts | 72 +------ 5 files changed, 211 insertions(+), 70 deletions(-) diff --git a/.filesize-allowlist b/.filesize-allowlist index 94f488521..b0cfc09e1 100644 --- a/.filesize-allowlist +++ b/.filesize-allowlist @@ -9,3 +9,4 @@ packages/studio/src/utils/sourcePatcher.test.ts packages/studio/src/App.tsx packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.test.ts +packages/studio/src/components/editor/domEditing.test.ts diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts index 3e41a15f2..f052a6d2c 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vitest"; import { Window } from "happy-dom"; -import { createStudioManualEditsRenderBodyScript } from "./manualEditsRenderScript"; +import { + createStudioManualEditsRenderBodyScript, + createStudioPositionSeekReapplyScript, +} from "./manualEditsRenderScript"; function runScript( window: Window, @@ -380,3 +383,182 @@ describe("createStudioManualEditsRenderBodyScript", () => { expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); }); }); + +describe("createStudioPositionSeekReapplyScript", () => { + function runPositionScript( + window: Window, + timers: { + setInterval?: typeof globalThis.setInterval; + clearInterval?: typeof globalThis.clearInterval; + } = {}, + ): void { + Object.assign(window, { SyntaxError }); + const script = createStudioPositionSeekReapplyScript(); + const execute = new Function( + "window", + "document", + "HTMLElement", + "DOMMatrix", + "setInterval", + "clearInterval", + script, + ); + execute( + window, + window.document, + window.HTMLElement, + globalThis.DOMMatrix, + timers.setInterval ?? + (((callback: TimerHandler) => { + void callback; + return 0 as never; + }) as typeof globalThis.setInterval), + timers.clearInterval ?? globalThis.clearInterval, + ); + } + + it("reapplies box-size after seek", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + const originalSeek = () => { + card.style.removeProperty("width"); + card.style.removeProperty("height"); + }; + (window as unknown as { __hf: Record }).__hf = { seek: originalSeek }; + + runPositionScript(window); + const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek; + wrappedSeek(1); + + expect(card.style.getPropertyValue("width")).toBe("200px"); + expect(card.style.getPropertyValue("height")).toBe("100px"); + }); + + it("strips GSAP translate from transform after reapplying path offset", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + const originalSeek = () => { + card.style.setProperty("transform", "matrix(1, 0, 0, 1, 120, 60)"); + }; + (window as unknown as { __hf: Record }).__hf = { seek: originalSeek }; + + runPositionScript(window); + const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek; + wrappedSeek(1); + + expect(card.style.getPropertyValue("translate")).toContain("--hf-studio-offset-x"); + const transform = card.style.getPropertyValue("transform"); + if (transform && transform !== "none") { + const m = new DOMMatrix(transform); + expect(m.m41).toBe(0); + expect(m.m42).toBe(0); + } + }); + + it("preserves non-translate components when stripping GSAP transform", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + const originalSeek = () => { + card.style.setProperty("transform", "matrix(0.5, 0, 0, 0.5, 80, 40)"); + }; + (window as unknown as { __hf: Record }).__hf = { seek: originalSeek }; + + runPositionScript(window); + const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek; + wrappedSeek(1); + + const transform = card.style.getPropertyValue("transform"); + expect(transform).toBeTruthy(); + expect(transform).not.toContain("80"); + expect(transform).not.toContain("40"); + }); + + it("removes transform entirely when it becomes identity after stripping translate", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + const originalSeek = () => { + card.style.setProperty("transform", "matrix(1, 0, 0, 1, 50, 25)"); + }; + (window as unknown as { __hf: Record }).__hf = { seek: originalSeek }; + + runPositionScript(window); + const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek; + wrappedSeek(1); + + const transform = card.style.getPropertyValue("transform"); + expect(!transform || transform === "none" || transform === "").toBe(true); + }); + + it("no-ops when transform is 'none'", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + (window as unknown as { __hf: Record }).__hf = { seek: () => {} }; + runPositionScript(window); + + expect(card.style.getPropertyValue("transform")).toBe("none"); + }); + + it("strips GSAP translate for rotation-only elements", () => { + const window = new Window(); + window.document.body.innerHTML = ` +
+
+ `; + const card = window.document.getElementById("card") as unknown as HTMLElement; + + const originalSeek = () => { + card.style.setProperty("transform", "matrix(1, 0, 0, 1, 100, 50)"); + }; + (window as unknown as { __hf: Record }).__hf = { seek: originalSeek }; + + runPositionScript(window); + const wrappedSeek = (window as unknown as { __hf: { seek: (t: number) => void } }).__hf.seek; + wrappedSeek(1); + + expect(card.style.getPropertyValue("rotate")).toContain("--hf-studio-rotation"); + const transform = card.style.getPropertyValue("transform"); + expect(!transform || transform === "none" || transform === "").toBe(true); + }); +}); diff --git a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts index cc4106fa5..f5b57f9f1 100644 --- a/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts +++ b/packages/core/src/studio-api/helpers/manualEditsRenderScript.ts @@ -251,6 +251,7 @@ function studioPositionSeekReapplyRuntime(): void { const rot = el.style.getPropertyValue(ROTATION_PROP); if (rot) { el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)")); + stripGsapTranslateFromTransform(el); } } reapplyMotionTimeline(); diff --git a/packages/studio/src/components/editor/domEditing.test.ts b/packages/studio/src/components/editor/domEditing.test.ts index a15d31083..6368e0fde 100644 --- a/packages/studio/src/components/editor/domEditing.test.ts +++ b/packages/studio/src/components/editor/domEditing.test.ts @@ -321,6 +321,29 @@ describe("resolveVisualDomEditSelectionTarget", () => { expect(visualTarget).toBe(headline); expect(explicitSelection?.id).toBe("container"); }); + + it("prefers the visually-on-top sibling over a deeper element in a separate visual layer", () => { + const document = createDocument(` +
+
+ +
+
+ `); + const pipStudio = document.getElementById("pip-studio") as HTMLElement; + const sfChrome = document.getElementById("sf-chrome") as HTMLElement; + const subComp = document.getElementById("sub-comp") as HTMLElement; + setElementRect(pipStudio, { left: 50, top: 50, width: 320, height: 320 }); + setElementRect(sfChrome, { left: 0, top: 0, width: 1920, height: 1080 }); + setElementRect(subComp, { left: 0, top: 0, width: 1920, height: 1080 }); + + expect( + resolveVisualDomEditSelectionTarget([pipStudio, subComp, sfChrome], { + activeCompositionPath: "index.html", + }), + ).toBe(pipStudio); + }); }); describe("isLargeRasterDomEditSelection", () => { diff --git a/packages/studio/src/utils/studioPreviewHelpers.ts b/packages/studio/src/utils/studioPreviewHelpers.ts index 6c9485bcd..654016539 100644 --- a/packages/studio/src/utils/studioPreviewHelpers.ts +++ b/packages/studio/src/utils/studioPreviewHelpers.ts @@ -1,24 +1,18 @@ -import type { DomEditViewport, DomEditSelection } from "../components/editor/domEditing"; +import type { DomEditViewport } from "../components/editor/domEditing"; import { resolveVisualDomEditSelectionTarget } from "../components/editor/domEditing"; import { getDomLayerPatchTarget, isElementComputedVisible, } from "../components/editor/domEditingElement"; -import { usePlayerStore, liveTime } from "../player"; import { getEventTargetElement } from "./studioHelpers"; -export interface PreviewLocalPointer { +interface PreviewLocalPointer { x: number; y: number; viewport: DomEditViewport; } -export interface PreviewPlayerCompat { - getTime: () => number; - renderSeek: (timeSeconds: number) => void; -} - -export function resolvePreviewLocalPointer( +function resolvePreviewLocalPointer( iframe: HTMLIFrameElement, doc: Document, win: Window, @@ -42,24 +36,6 @@ export function resolvePreviewLocalPointer( }; } -export function getPreviewLocalPointer( - iframe: HTMLIFrameElement, - clientX: number, - clientY: number, -): PreviewLocalPointer | null { - let doc: Document | null = null; - let win: Window | null = null; - try { - doc = iframe.contentDocument; - win = iframe.contentWindow; - } catch { - return null; - } - if (!doc || !win) return null; - - return resolvePreviewLocalPointer(iframe, doc, win, clientX, clientY); -} - const POINTER_EVENTS_OVERRIDE_ID = "__hf_studio_pointer_events_override__"; function forcePointerEventsAuto(doc: Document): HTMLStyleElement | null { @@ -122,21 +98,6 @@ export function getPreviewTargetFromPointer( } } -export function buildRasterClickSelectionContext( - selection: DomEditSelection, - localPointer: PreviewLocalPointer, -): string { - return [ - "The user clicked a large raster/background element in the Studio preview.", - `Preview click: x=${Math.round(localPointer.x)}px, y=${Math.round(localPointer.y)}px in a ${Math.round( - localPointer.viewport.width, - )}x${Math.round(localPointer.viewport.height)} composition.`, - `Selected target: <${selection.tagName}> ${selection.selector ?? selection.id ?? selection.label}.`, - "Visible copy or artwork at that point may be baked into the selected image/background rather than a selectable DOM text layer.", - "If the request mentions text seen at the click location, inspect or replace the image asset, or recreate that visible copy as editable DOM.", - ].join("\n"); -} - function objectLike(value: unknown): object | null { return value && (typeof value === "object" || typeof value === "function") ? value : null; } @@ -162,33 +123,6 @@ function readPlaybackTime(target: object | null, key: string): number | null { } } -export function getPreviewPlayer(win: Window | null | undefined): PreviewPlayerCompat | null { - const player = objectLike(win ? Reflect.get(win, "__player") : null); - if (!player) return null; - const getTime = Reflect.get(player, "getTime"); - const renderSeek = Reflect.get(player, "renderSeek"); - if (typeof getTime !== "function" || typeof renderSeek !== "function") return null; - return { - getTime: () => { - const value = getTime.call(player); - return typeof value === "number" && Number.isFinite(value) ? value : 0; - }, - renderSeek: (timeSeconds: number) => { - renderSeek.call(player, timeSeconds); - }, - }; -} - -export function seekStudioPreview(iframe: HTMLIFrameElement | null, timeSeconds: number): boolean { - const player = getPreviewPlayer(iframe?.contentWindow); - if (!player) return false; - const nextTime = Math.max(0, timeSeconds); - player.renderSeek(nextTime); - usePlayerStore.getState().setCurrentTime(nextTime); - liveTime.notify(nextTime); - return true; -} - export function pauseStudioPreviewPlayback(iframe: HTMLIFrameElement | null): number | null { const win = iframe?.contentWindow; if (!win) return null;