diff --git a/packages/studio/src/components/editor/DomEditOverlay.tsx b/packages/studio/src/components/editor/DomEditOverlay.tsx index aa6684fb8..df41fedc5 100644 --- a/packages/studio/src/components/editor/DomEditOverlay.tsx +++ b/packages/studio/src/components/editor/DomEditOverlay.tsx @@ -13,7 +13,6 @@ import { useDomEditOverlayRects } from "./useDomEditOverlayRects"; import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures"; import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay"; import { GridOverlay } from "./GridOverlay"; -import { useOffScreenIndicators } from "./useOffScreenIndicators"; // Re-exports for external consumers — preserving existing import paths. export { @@ -215,7 +214,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({ return () => cancelAnimationFrame(frame); }); - const offScreenIndicators = useOffScreenIndicators({ iframeRef, overlayRef, compRect }); const gestures = createDomEditOverlayGestureHandlers({ overlayRef, @@ -521,64 +519,6 @@ export const DomEditOverlay = memo(function DomEditOverlay({ }} /> ))} - {offScreenIndicators.length > 0 && - compRect.width > 0 && - offScreenIndicators.map((ind) => { - const isSelected = selection?.id === ind.elementId; - return ( -
{ - if (e.button !== 0) return; - e.stopPropagation(); - e.preventDefault(); - const startX = e.clientX; - const startY = e.clientY; - const el = e.currentTarget; - el.setPointerCapture(e.pointerId); - let deltaX = 0; - let deltaY = 0; - let moved = false; - const onMove = (me: PointerEvent) => { - deltaX = me.clientX - startX; - deltaY = me.clientY - startY; - if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) moved = true; - if (moved) { - el.style.transform = `translate(${deltaX}px, ${deltaY}px)`; - } - }; - const onUp = async (ue: PointerEvent) => { - el.releasePointerCapture(ue.pointerId); - el.removeEventListener("pointermove", onMove); - el.removeEventListener("pointerup", onUp); - el.style.transform = ""; - const sel = await onSelectElementById?.(ind.elementId); - if (moved && sel && onPathOffsetCommit) { - const scale = compRect.scaleX || 1; - onPathOffsetCommit(sel, { x: deltaX / scale, y: deltaY / scale }); - } - }; - el.addEventListener("pointermove", onMove); - el.addEventListener("pointerup", onUp); - } - } - title={isSelected ? undefined : `Drag #${ind.elementId}`} - /> - ); - })} Array<{ targets?: () => Element[] }> }; - -function isHtmlElement(node: unknown): node is HTMLElement { - return ( - typeof node === "object" && - node !== null && - typeof (node as HTMLElement).getBoundingClientRect === "function" && - typeof (node as HTMLElement).tagName === "string" - ); -} - -function collectGsapTargetElements(iframe: HTMLIFrameElement): HTMLElement[] { - const win = iframe.contentWindow as - | (Window & { __timelines?: Record }) - | null; - if (!win) return []; - - let timelines: Record | undefined; - try { - timelines = win.__timelines; - } catch { - return []; - } - if (!timelines) return []; - - const seen = new Set(); - for (const tl of Object.values(timelines)) { - if (!tl?.getChildren) continue; - try { - for (const child of tl.getChildren(true)) { - if (!child.targets) continue; - for (const t of child.targets()) { - if (isHtmlElement(t)) seen.add(t); - } - } - } catch { - // cross-origin or detached timeline — skip - } - } - return Array.from(seen); -} - -function indicatorsEqual(a: OffScreenIndicator[], b: OffScreenIndicator[]): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - const ai = a[i]!; - const bi = b[i]!; - if ( - ai.key !== bi.key || - Math.abs(ai.left - bi.left) > 0.5 || - Math.abs(ai.top - bi.top) > 0.5 || - Math.abs(ai.width - bi.width) > 0.5 || - Math.abs(ai.height - bi.height) > 0.5 - ) - return false; - } - return true; -} - -export function useOffScreenIndicators({ - iframeRef, - overlayRef, - compRect, -}: { - iframeRef: RefObject; - overlayRef: RefObject; - compRect: CompRect; -}): OffScreenIndicator[] { - const [indicators, setIndicators] = useState([]); - const prevRef = useRef([]); - const compRectRef = useRef(compRect); - compRectRef.current = compRect; - - useMountEffect(() => { - let frame = 0; - - const update = () => { - frame = requestAnimationFrame(update); - - const iframe = iframeRef.current; - const overlayEl = overlayRef.current; - const cr = compRectRef.current; - if (!iframe || !overlayEl || cr.width <= 0 || cr.height <= 0) { - if (prevRef.current.length > 0) { - prevRef.current = []; - setIndicators([]); - } - return; - } - - const iframeRect = iframe.getBoundingClientRect(); - const overlayRect = overlayEl.getBoundingClientRect(); - - const doc = iframe.contentDocument; - const root = - doc?.querySelector("[data-composition-id]") ?? doc?.documentElement ?? null; - if (!root) return; - - const declaredWidth = - Number.parseFloat(root.getAttribute("data-width") ?? "") || iframeRect.width; - const declaredHeight = - Number.parseFloat(root.getAttribute("data-height") ?? "") || iframeRect.height; - const rootScaleX = iframeRect.width / declaredWidth; - const rootScaleY = iframeRect.height / declaredHeight; - - const targets = collectGsapTargetElements(iframe); - if (targets.length === 0) { - if (prevRef.current.length > 0) { - prevRef.current = []; - setIndicators([]); - } - return; - } - - // Composition bounds in overlay coordinates - const compLeft = cr.left; - const compTop = cr.top; - const compRight = compLeft + cr.width; - const compBottom = compTop + cr.height; - - const next: OffScreenIndicator[] = []; - const keyCounts = new Map(); - - for (const el of targets) { - if (!el.isConnected) continue; - - const elRect = el.getBoundingClientRect(); - if (elRect.width <= 0 && elRect.height <= 0) continue; - - // Element rect in overlay coordinates - const elLeft = iframeRect.left - overlayRect.left + elRect.left * rootScaleX; - const elTop = iframeRect.top - overlayRect.top + elRect.top * rootScaleY; - const elW = elRect.width * rootScaleX; - const elH = elRect.height * rootScaleY; - - // Check if the element is fully inside the composition - if ( - elLeft >= compLeft && - elTop >= compTop && - elLeft + elW <= compRight && - elTop + elH <= compBottom - ) { - continue; - } - - // Only elements with a real id attribute can be selected via getElementById - if (!el.id) continue; - const count = keyCounts.get(el.id) ?? 0; - keyCounts.set(el.id, count + 1); - const key = count > 0 ? `${el.id}:${count}` : el.id; - next.push({ - key, - elementId: el.id, - left: elLeft, - top: elTop, - width: elW, - height: elH, - }); - } - - if (!indicatorsEqual(prevRef.current, next)) { - prevRef.current = next; - setIndicators(next); - } - }; - - frame = requestAnimationFrame(update); - return () => cancelAnimationFrame(frame); - }); - - return indicators; -}