mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
* 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.
208 lines
6.7 KiB
TypeScript
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,
|
|
};
|
|
}
|