Files
hyperframes/packages/studio/src/components/editor/useDomEditOverlayRects.ts
T
Miguel Ángel 91bdffffe6 fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)
* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each)

* fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files

* feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson

Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux:
- Detects the platform automatically
- Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM)
- Falls back to clear manual instructions with exact commands
- 'hyperframes browser ensure' guides through the setup interactively
- After setup, all render commands work without any flags

* fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds

Path exclusions are insufficient — Defender re-scans new files created
during bun install before the exclusion takes effect. Disable real-time
monitoring for the entire job duration instead (standard CI practice).

* refactor(studio): split all files >500 LOC + extract useToast, delete allowlist

All 11 large files split into focused modules under 500 LOC.
App.tsx extracted toast logic into useToast hook (493 LOC now).
.filesize-allowlist deleted — no longer needed.

* fix: remove unused imports from split files, extract useToast from App.tsx

App.tsx: 504 → 493 lines (toast logic extracted to useToast hook)
timelineDOM.ts: remove unused imports from re-export pattern
MotionPanel.tsx: remove unused clampStudioCustomEasePoints import
studioMotionOps.ts: remove unused StudioGsapMotionDirection import

* fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs)

* fix(producer): use node --experimental-strip-types instead of tsx for build:fonts

Eliminates the tsx binary dependency that Windows Defender locks during
bun install, causing EPERM errors. Node 22.6+ strips TypeScript types
natively with no external binary.

* chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500)

* fix(ci): disable Windows Defender before checkout to prevent all EPERM races

* fix(producer): skip build:fonts if fontData.generated.ts already exists

The generated file is tracked in git, so CI doesn't need to regenerate
it. This avoids @fontsource/inter node_modules access on Windows which
triggers EPERM from Defender scanning during bun install.
2026-05-13 01:48:12 +02:00

208 lines
6.7 KiB
TypeScript

/**
* RAF-driven hook that tracks overlay, hover, and group rects from the iframe DOM.
* Runs a requestAnimationFrame loop and writes React state only when rects change.
*/
import { useRef, useState, type RefObject } from "react";
import { useMountEffect } from "../../hooks/useMountEffect";
import { type DomEditSelection, findElementForSelection } from "./domEditing";
import {
type GroupOverlayItem,
type OverlayRect,
type ResolvedElementRef,
groupOverlayItemsEqual,
isElementVisibleForOverlay,
rectsEqual,
resolveElementForOverlay,
selectionCacheKey,
toOverlayRect,
} from "./domEditOverlayGeometry";
interface UseDomEditOverlayRectsOptions {
iframeRef: RefObject<HTMLIFrameElement | null>;
overlayRef: RefObject<HTMLDivElement | null>;
selectionRef: RefObject<DomEditSelection | null>;
activeCompositionPathRef: RefObject<string | null>;
groupSelectionsRef: RefObject<DomEditSelection[]>;
hoverSelectionRef: RefObject<DomEditSelection | null>;
rafPausedRef: RefObject<boolean>;
}
interface UseDomEditOverlayRectsResult {
overlayRect: OverlayRect | null;
overlayRectRef: RefObject<OverlayRect | null>;
setOverlayRect: (next: OverlayRect | null) => void;
hoverRect: OverlayRect | null;
hoverRectRef: RefObject<OverlayRect | null>;
setHoverRect: (next: OverlayRect | null) => void;
groupOverlayItems: GroupOverlayItem[];
groupOverlayItemsRef: RefObject<GroupOverlayItem[]>;
setGroupOverlayItems: (next: GroupOverlayItem[]) => void;
}
export function useDomEditOverlayRects({
iframeRef,
overlayRef,
selectionRef,
activeCompositionPathRef,
groupSelectionsRef,
hoverSelectionRef,
rafPausedRef,
}: UseDomEditOverlayRectsOptions): UseDomEditOverlayRectsResult {
const [overlayRect, setOverlayRectState] = useState<OverlayRect | null>(null);
const [hoverRect, setHoverRectState] = useState<OverlayRect | null>(null);
const [groupOverlayItems, setGroupOverlayItemsState] = useState<GroupOverlayItem[]>([]);
const overlayRectRef = useRef<OverlayRect | null>(null);
const hoverRectRef = useRef<OverlayRect | null>(null);
const groupOverlayItemsRef = useRef<GroupOverlayItem[]>([]);
const resolvedElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
const resolvedHoverElementRef = useRef<{ key: string; element: HTMLElement } | null>(null);
const resolvedGroupElementRef = useRef<Map<string, HTMLElement>>(new Map());
const setOverlayRect = (next: OverlayRect | null) => {
if (rectsEqual(overlayRectRef.current, next)) return;
overlayRectRef.current = next;
setOverlayRectState(next);
};
const setHoverRect = (next: OverlayRect | null) => {
if (rectsEqual(hoverRectRef.current, next)) return;
hoverRectRef.current = next;
setHoverRectState(next);
};
const setGroupOverlayItems = (next: GroupOverlayItem[]) => {
if (groupOverlayItemsEqual(groupOverlayItemsRef.current, next)) return;
groupOverlayItemsRef.current = next;
setGroupOverlayItemsState(next);
};
const resolveGroupElement = (doc: Document, sel: DomEditSelection) => {
const key = selectionCacheKey(sel);
const cached = resolvedGroupElementRef.current.get(key);
if (cached?.isConnected && cached.ownerDocument === doc) return cached;
const next = findElementForSelection(doc, sel, activeCompositionPathRef.current);
if (next) {
resolvedGroupElementRef.current.set(key, next);
} else {
resolvedGroupElementRef.current.delete(key);
}
return next;
};
useMountEffect(() => {
let frame = 0;
const clearAll = () => {
setOverlayRect(null);
setHoverRect(null);
setGroupOverlayItems([]);
};
const update = () => {
frame = requestAnimationFrame(update);
if (rafPausedRef.current) return;
const sel = selectionRef.current;
const iframe = iframeRef.current;
const overlayEl = overlayRef.current;
if (!iframe || !overlayEl) {
resolvedElementRef.current = null;
resolvedHoverElementRef.current = null;
resolvedGroupElementRef.current.clear();
clearAll();
return;
}
const doc = iframe.contentDocument;
if (!doc) {
resolvedElementRef.current = null;
resolvedHoverElementRef.current = null;
resolvedGroupElementRef.current.clear();
clearAll();
return;
}
if (sel) {
const el = resolveElementForOverlay(
doc,
sel,
activeCompositionPathRef.current,
resolvedElementRef as ResolvedElementRef,
);
if (el && isElementVisibleForOverlay(el)) {
setOverlayRect(toOverlayRect(overlayEl, iframe, el));
} else {
setOverlayRect(null);
}
} else {
resolvedElementRef.current = null;
setOverlayRect(null);
}
const group = groupSelectionsRef.current;
if (group.length > 0) {
const nextGroupItems: GroupOverlayItem[] = [];
const liveGroupKeys = new Set<string>();
for (const groupSelection of group) {
const key = selectionCacheKey(groupSelection);
liveGroupKeys.add(key);
const el = resolveGroupElement(doc, groupSelection);
const rect = el ? toOverlayRect(overlayEl, iframe, el) : null;
if (el && rect)
nextGroupItems.push({ key, selection: groupSelection, element: el, rect });
}
for (const key of resolvedGroupElementRef.current.keys()) {
if (!liveGroupKeys.has(key)) resolvedGroupElementRef.current.delete(key);
}
setGroupOverlayItems(nextGroupItems);
} else {
resolvedGroupElementRef.current.clear();
setGroupOverlayItems([]);
}
const hoverSel = hoverSelectionRef.current;
const hoverMatchesSelection = Boolean(
sel && hoverSel && selectionCacheKey(sel) === selectionCacheKey(hoverSel),
);
const hoverMatchesGroup = Boolean(
hoverSel && group.some((entry) => selectionCacheKey(entry) === selectionCacheKey(hoverSel)),
);
if (!hoverSel || hoverMatchesSelection || hoverMatchesGroup) {
resolvedHoverElementRef.current = null;
setHoverRect(null);
return;
}
const hoverEl = resolveElementForOverlay(
doc,
hoverSel,
activeCompositionPathRef.current,
resolvedHoverElementRef as ResolvedElementRef,
);
if (!hoverEl) {
setHoverRect(null);
return;
}
setHoverRect(toOverlayRect(overlayEl, iframe, hoverEl));
};
frame = requestAnimationFrame(update);
return () => cancelAnimationFrame(frame);
});
return {
overlayRect,
overlayRectRef,
setOverlayRect,
hoverRect,
hoverRectRef,
setHoverRect,
groupOverlayItems,
groupOverlayItemsRef,
setGroupOverlayItems,
};
}