refactor(studio): simplify dropdown helpers + use stage-size message for dims

Cleanup from the /simplify pass on PR #715.

- App.tsx: subscribe to the runtime's `stage-size` message (which
  carries authoritative width/height post-applyCompositionSizing)
  instead of re-parsing data-width/data-height from the iframe DOM.
  Drops the cross-origin try/catch, querySelector, and parseInt logic,
  and fires once per comp load instead of on every state/timeline tick.
- App.tsx: import CompositionDimensions from RenderQueue instead of
  inlining the shape.
- RenderQueue.tsx: replace scaleLabel() with a SCALE_LABEL record,
  inline the one-call formatDims helper, and trim the type comment to
  the WHY.
This commit is contained in:
James
2026-05-11 16:01:29 +00:00
parent 534c70e308
commit 976ceabedc
2 changed files with 25 additions and 45 deletions
+13 -27
View File
@@ -11,7 +11,7 @@ import { useMountEffect } from "./hooks/useMountEffect";
import { NLELayout } from "./components/nle/NLELayout"; import { NLELayout } from "./components/nle/NLELayout";
import { SourceEditor } from "./components/editor/SourceEditor"; import { SourceEditor } from "./components/editor/SourceEditor";
import { LeftSidebar } from "./components/sidebar/LeftSidebar"; import { LeftSidebar } from "./components/sidebar/LeftSidebar";
import { RenderQueue } from "./components/renders/RenderQueue"; import { RenderQueue, type CompositionDimensions } from "./components/renders/RenderQueue";
import { useRenderQueue } from "./components/renders/useRenderQueue"; import { useRenderQueue } from "./components/renders/useRenderQueue";
import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player"; import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player";
import { AudioWaveform } from "./player/components/AudioWaveform"; import { AudioWaveform } from "./player/components/AudioWaveform";
@@ -278,35 +278,21 @@ export function StudioApp() {
}, [captionHasSelection, captionEditMode]); }, [captionHasSelection, captionEditMode]);
// Track the active composition's authored dimensions so the render // Track the active composition's authored dimensions so the render
// dropdown can derive landscape vs portrait without asking the user. // dropdown can derive landscape vs portrait. The runtime emits
// The runtime fires "state"/"timeline" messages after compositions load. // `stage-size` after `applyCompositionSizing` resolves the authoritative
const [compositionDimensions, setCompositionDimensions] = useState<{ // dims, so we use that instead of re-parsing the iframe DOM.
width: number; const [compositionDimensions, setCompositionDimensions] = useState<CompositionDimensions | null>(
height: number; null,
} | null>(null); );
useMountEffect(() => { useMountEffect(() => {
const readDimensions = () => {
const iframe = previewIframeRef.current;
let doc: Document | null = null;
try {
doc = iframe?.contentDocument ?? null;
} catch {
return;
}
if (!doc) return;
const root = doc.querySelector("[data-composition-id]");
const w = parseInt(root?.getAttribute("data-width") ?? "", 10);
const h = parseInt(root?.getAttribute("data-height") ?? "", 10);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return;
setCompositionDimensions((prev) =>
prev && prev.width === w && prev.height === h ? prev : { width: w, height: h },
);
};
const handleMessage = (e: MessageEvent) => { const handleMessage = (e: MessageEvent) => {
const data = e.data; const data = e.data;
if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) { if (data?.source !== "hf-preview" || data?.type !== "stage-size") return;
readDimensions(); const { width, height } = data as { width: number; height: number };
} if (!(width > 0) || !(height > 0)) return;
setCompositionDimensions((prev) =>
prev && prev.width === width && prev.height === height ? prev : { width, height },
);
}; };
window.addEventListener("message", handleMessage); window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage);
@@ -26,13 +26,19 @@ interface RenderQueueProps {
compositionDimensions?: CompositionDimensions | null; compositionDimensions?: CompositionDimensions | null;
} }
// User-facing render scale. Orientation is derived from the composition's // Orientation is derived from the composition's authored aspect ratio,
// authored aspect ratio at render time, so the user never picks an // not chosen by the user — picking "1080p portrait" for a landscape comp
// orientation that mismatches their comp. // would just produce a wrong-aspect render.
type RenderScale = "auto" | "1080p" | "4k"; type RenderScale = "auto" | "1080p" | "4k";
const SCALE_OPTION_ORDER: RenderScale[] = ["auto", "1080p", "4k"]; const SCALE_OPTION_ORDER: RenderScale[] = ["auto", "1080p", "4k"];
const SCALE_LABEL: Record<RenderScale, string> = {
auto: "Auto",
"1080p": "1080p",
"4k": "4K",
};
function isPortraitComp(dims: CompositionDimensions | null | undefined): boolean { function isPortraitComp(dims: CompositionDimensions | null | undefined): boolean {
// Squares and missing dims fall through to landscape — matches the legacy // Squares and missing dims fall through to landscape — matches the legacy
// default ("landscape" was the first preset). The auto option exists for // default ("landscape" was the first preset). The auto option exists for
@@ -50,15 +56,6 @@ function resolveResolution(
return portrait ? "portrait-4k" : "landscape-4k"; return portrait ? "portrait-4k" : "landscape-4k";
} }
function scaleLabel(scale: RenderScale): string {
if (scale === "auto") return "Auto";
if (scale === "1080p") return "1080p";
return "4K";
}
// Resolved output dimensions for a given scale + composition. Mirrors
// `CANVAS_DIMENSIONS` in core for the 1080p / 4K presets; `auto` echoes the
// composition's authored dims so the user can see exactly what they'll get.
function resolvedDimensions( function resolvedDimensions(
scale: RenderScale, scale: RenderScale,
dims: CompositionDimensions | null | undefined, dims: CompositionDimensions | null | undefined,
@@ -71,17 +68,14 @@ function resolvedDimensions(
return portrait ? { width: 2160, height: 3840 } : { width: 3840, height: 2160 }; return portrait ? { width: 2160, height: 3840 } : { width: 3840, height: 2160 };
} }
function formatDims(dims: CompositionDimensions | null): string {
if (!dims) return "?";
return `${dims.width}×${dims.height}`;
}
function scaleOptionLabel( function scaleOptionLabel(
scale: RenderScale, scale: RenderScale,
dims: CompositionDimensions | null | undefined, dims: CompositionDimensions | null | undefined,
): string { ): string {
const resolved = resolvedDimensions(scale, dims); const resolved = resolvedDimensions(scale, dims);
return resolved ? `${scaleLabel(scale)} · ${formatDims(resolved)}` : scaleLabel(scale); return resolved
? `${SCALE_LABEL[scale]} · ${resolved.width}×${resolved.height}`
: SCALE_LABEL[scale];
} }
const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string }> = { const FORMAT_INFO: Record<"mp4" | "webm" | "mov", { label: string; desc: string }> = {