mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
fix(studio): remove Ask agent popup, fix preview selection, fix manual edits in renderer (#963)
## Summary Three interrelated studio UX and rendering fixes, plus a critical fix for manual edits not surviving video export. ### 1. Remove "Ask agent" popup on preview click The "Ask agent" modal auto-triggered whenever clicking on a large raster element (images, backgrounds covering >40% of the viewport). This intercepted clicks meant for editable elements underneath, making it very annoying to select elements in compositions with large backgrounds or sub-composition screenshots. **Removed:** the `isLargeRasterDomEditSelection` check + `setAgentModalOpen(true)` trigger in `usePreviewInteraction.ts`. The manual "Ask agent" button via the context menu still works — only the auto-popup is gone. **Files:** `usePreviewInteraction.ts`, `useDomEditSession.ts` ### 2. Fix preview click selecting wrong element across visual layers Clicking the PiP video overlay would select the Sf Chrome image behind it, because the scoring algorithm weighted DOM depth at 10,000× per level. Elements inside sub-compositions (deeper in the DOM tree) always won over visually-on-top elements at the root level, regardless of z-order. **Fix:** replaced the weighted scoring with a visual-stacking-order-first algorithm. `resolveVisualDomEditSelectionTarget` now trusts `elementsFromPoint` order (topmost first) and only prefers a deeper candidate when it's a direct descendant of the current pick — never jumping to an unrelated element painted behind it. **Files:** `domEditingElement.ts` ### 3. Fix manual edits not surviving video export Two gaps in the producer's seek-reapply script (`studioPositionSeekReapplyRuntime`): **a) Missing box-size reapplication.** The script handled translate and rotation but not width/height. Also, `data-hf-studio-box-size` was absent from the detection list in `htmlCompiler.ts`, so the script wasn't even injected for compositions with resize-only edits. **b) GSAP transform matrix clobbering translate.** When GSAP animates an element (e.g. `scale`, `opacity`), it captures the element's translate into its internal transform matrix (`m41`/`m42`). The render script was setting the CSS `translate` property but leaving GSAP's translate baked into `transform`, causing the manual edit offset to be ignored. Ported the same `stripGsapTranslateFromTransform` logic the studio uses: parse the DOMMatrix, zero out m41/m42, and remove or rewrite the `transform` property so the CSS `translate` takes effect cleanly. **Files:** `manualEditsRenderScript.ts`, `htmlCompiler.ts` ## Test plan - [x] `bun test` — 50 tests pass across `domEditing.test.ts` and `manualEditsRenderScript.test.ts` - [x] Pre-commit hooks pass (typecheck, lint, format, commitlint) - [x] Verified in studio preview: clicking elements selects the visually-on-top one, "Ask agent" popup no longer appears - [x] Rendered a composition with a manually-moved PiP video — position survives in the output video
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-box-size="true"
|
||||
style="--hf-studio-width: 200px; --hf-studio-height: 100px; width: 200px; height: 100px">
|
||||
</div>
|
||||
`;
|
||||
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<string, unknown> }).__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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-path-offset="true"
|
||||
data-hf-studio-original-translate=""
|
||||
style="--hf-studio-offset-x: 50px; --hf-studio-offset-y: 30px; translate: var(--hf-studio-offset-x, 0px) var(--hf-studio-offset-y, 0px)">
|
||||
</div>
|
||||
`;
|
||||
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<string, unknown> }).__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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-path-offset="true"
|
||||
data-hf-studio-original-translate=""
|
||||
style="--hf-studio-offset-x: 10px; --hf-studio-offset-y: 20px; translate: var(--hf-studio-offset-x, 0px) var(--hf-studio-offset-y, 0px)">
|
||||
</div>
|
||||
`;
|
||||
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<string, unknown> }).__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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-path-offset="true"
|
||||
data-hf-studio-original-translate=""
|
||||
style="--hf-studio-offset-x: 10px; --hf-studio-offset-y: 20px; translate: var(--hf-studio-offset-x, 0px) var(--hf-studio-offset-y, 0px)">
|
||||
</div>
|
||||
`;
|
||||
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<string, unknown> }).__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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-path-offset="true"
|
||||
data-hf-studio-original-translate=""
|
||||
style="--hf-studio-offset-x: 10px; --hf-studio-offset-y: 20px; translate: var(--hf-studio-offset-x, 0px) var(--hf-studio-offset-y, 0px); transform: none">
|
||||
</div>
|
||||
`;
|
||||
const card = window.document.getElementById("card") as unknown as HTMLElement;
|
||||
|
||||
(window as unknown as { __hf: Record<string, unknown> }).__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 = `
|
||||
<div id="card"
|
||||
data-hf-studio-rotation="true"
|
||||
data-hf-studio-original-rotate=""
|
||||
style="--hf-studio-rotation: 45deg; rotate: var(--hf-studio-rotation, 0deg)">
|
||||
</div>
|
||||
`;
|
||||
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<string, unknown> }).__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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 + "]")
|
||||
)
|
||||
@@ -191,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++) {
|
||||
@@ -207,8 +232,18 @@ function studioPositionSeekReapplyRuntime(): void {
|
||||
"var(" + OFFSET_Y_PROP + ", 0px)",
|
||||
),
|
||||
);
|
||||
stripGsapTranslateFromTransform(el);
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -216,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();
|
||||
|
||||
@@ -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="',
|
||||
];
|
||||
|
||||
@@ -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(`
|
||||
<div id="comp-root">
|
||||
<div id="sub-comp" class="sub-comp">
|
||||
<img id="sf-chrome" class="sf-chrome" style="width:100%;height:100%" />
|
||||
</div>
|
||||
<video id="pip-studio" class="pip-studio" style="position:absolute;z-index:15" />
|
||||
</div>
|
||||
`);
|
||||
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", () => {
|
||||
|
||||
@@ -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<Element | null | undefined>,
|
||||
options: Pick<DomEditContextOptions, "activeCompositionPath">,
|
||||
): 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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<void>;
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user