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] 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, ], );