Files
hyperframes/packages/studio/src/utils/timelineInspector.ts
T
James 7e0a447325 refactor: drop unused exports detected by fallow auto-fix
Run `fallow fix --auto-fixable` to remove `export` keywords from symbols
fallow's reachability analysis identifies as unused. Keeps only the cases
where the symbol is still referenced internally in its own file (so
removing `export` doesn't surface a new oxlint `no-unused-vars` error).

Result: fallow dead-code findings drop from 276 → 208 (68 fewer unused
exports), with no behavior change — each symbol is still defined and used
exactly the same way within its file.

Reverted ~20 files where fallow's auto-fix would have created cascading
"declared but never used" lint errors — those are cases where the symbol
isn't used at all, and properly cleaning them up means deleting the
declaration, not just dropping `export`. Better to land that as a
separate, narrower PR rather than mixing it into a mechanical de-export.

Also reverted four false positives where fallow missed real consumers:
- `captureCost.ts` (renderOrchestrator has two separate import blocks
  from the same module; fallow only saw the first)
- `propertyPanelHelpers.ts`, `domEditingLayers.ts` (real internal uses
  fallow's reachability missed)
- `render.ts` (functions imported via `await import()` dynamic import,
  which fallow's static analysis doesn't follow)

Test plan: bun run --filter '*' typecheck (clean), oxlint + oxfmt clean,
cli/core/studio/engine vitest suites pass (335 + 917 + 576 + 605 tests).
2026-05-19 00:51:56 +00:00

117 lines
4.1 KiB
TypeScript

import type { TimelineElement } from "../player";
const TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS = 0.08;
const AUDIO_TIMELINE_TAGS = new Set(["audio", "music", "sfx", "sound", "narration"]);
const AUDIO_SOURCE_EXT_RE = /\.(aac|flac|m4a|mp3|ogg|opus|wav)(?:[?#].*)?$/i;
export function getTimelineElementKey(
element: Pick<TimelineElement, "id" | "key"> | null | undefined,
): string | null {
if (!element) return null;
return element.key ?? element.id;
}
export function isAudioTimelineElement(
element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
): boolean {
if (!element) return false;
const tag = element.tag.trim().toLowerCase();
if (AUDIO_TIMELINE_TAGS.has(tag)) return true;
return Boolean(element.src && AUDIO_SOURCE_EXT_RE.test(element.src));
}
export function canInspectTimelineElement(
element: Pick<TimelineElement, "tag" | "src"> | null | undefined,
): boolean {
return !isAudioTimelineElement(element);
}
export function shouldShowTimelineInspectorBounds(
currentTime: number,
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
): boolean {
if (!element) return false;
if (!Number.isFinite(currentTime)) return false;
if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
const start = Math.max(0, element.start);
const end = Math.max(start, start + Math.max(0, element.duration));
const epsilon = Math.max(0, epsilonSeconds);
return Math.abs(currentTime - start) <= epsilon || Math.abs(currentTime - end) <= epsilon;
}
export function isTimelineElementActiveAtTime(
currentTime: number,
element: Pick<TimelineElement, "start" | "duration"> | null | undefined,
epsilonSeconds = TIMELINE_INSPECTOR_BOUNDARY_EPSILON_SECONDS,
): boolean {
if (!element) return false;
if (!Number.isFinite(currentTime)) return false;
if (!Number.isFinite(element.start) || !Number.isFinite(element.duration)) return false;
const start = Math.max(0, element.start);
const end = Math.max(start, start + Math.max(0, element.duration));
const epsilon = Math.max(0, epsilonSeconds);
return currentTime >= start - epsilon && currentTime <= end + epsilon;
}
export interface TimelineLayerVisibility {
visible: boolean;
compositeOpacity: number;
hasBox: boolean;
inViewport: boolean;
}
export function getTimelineLayerVisibilityInPreview(
element: HTMLElement,
options: { minCompositeOpacity?: number } = {},
): TimelineLayerVisibility {
const hidden: TimelineLayerVisibility = {
visible: false,
compositeOpacity: 0,
hasBox: false,
inViewport: false,
};
if (!element.isConnected) return hidden;
const doc = element.ownerDocument;
const win = doc.defaultView;
if (!win) return hidden;
const minCompositeOpacity = options.minCompositeOpacity ?? 0.01;
let compositeOpacity = 1;
let current: HTMLElement | null = element;
while (current && current !== doc.body && current !== doc.documentElement) {
const style = win.getComputedStyle(current);
if (style.display === "none" || style.visibility === "hidden") {
return { ...hidden, compositeOpacity };
}
compositeOpacity *= Number.parseFloat(style.opacity || "1");
if (compositeOpacity <= minCompositeOpacity) {
return { ...hidden, compositeOpacity };
}
current = current.parentElement;
}
const rect = element.getBoundingClientRect();
const hasBox = rect.width > 0.5 && rect.height > 0.5;
if (!hasBox) return { visible: false, compositeOpacity, hasBox, inViewport: false };
const viewportWidth = win.innerWidth || doc.documentElement.clientWidth;
const viewportHeight = win.innerHeight || doc.documentElement.clientHeight;
const inViewport =
rect.right > 0 && rect.bottom > 0 && rect.left < viewportWidth && rect.top < viewportHeight;
return {
visible: inViewport,
compositeOpacity,
hasBox,
inViewport,
};
}
export function isTimelineLayerVisibleInPreview(
element: HTMLElement,
options: { minCompositeOpacity?: number } = {},
): boolean {
return getTimelineLayerVisibilityInPreview(element, options).visible;
}