From 976ceabedcf4f43b5bbd53ca9030008da982f248 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 11 May 2026 16:01:29 +0000 Subject: [PATCH] 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. --- packages/studio/src/App.tsx | 40 ++++++------------- .../src/components/renders/RenderQueue.tsx | 30 ++++++-------- 2 files changed, 25 insertions(+), 45 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index edb028495..355106233 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -11,7 +11,7 @@ import { useMountEffect } from "./hooks/useMountEffect"; import { NLELayout } from "./components/nle/NLELayout"; import { SourceEditor } from "./components/editor/SourceEditor"; 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 { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player"; import { AudioWaveform } from "./player/components/AudioWaveform"; @@ -278,35 +278,21 @@ export function StudioApp() { }, [captionHasSelection, captionEditMode]); // Track the active composition's authored dimensions so the render - // dropdown can derive landscape vs portrait without asking the user. - // The runtime fires "state"/"timeline" messages after compositions load. - const [compositionDimensions, setCompositionDimensions] = useState<{ - width: number; - height: number; - } | null>(null); + // dropdown can derive landscape vs portrait. The runtime emits + // `stage-size` after `applyCompositionSizing` resolves the authoritative + // dims, so we use that instead of re-parsing the iframe DOM. + const [compositionDimensions, setCompositionDimensions] = useState( + null, + ); 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 data = e.data; - if (data?.source === "hf-preview" && (data?.type === "state" || data?.type === "timeline")) { - readDimensions(); - } + if (data?.source !== "hf-preview" || data?.type !== "stage-size") return; + 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); return () => window.removeEventListener("message", handleMessage); diff --git a/packages/studio/src/components/renders/RenderQueue.tsx b/packages/studio/src/components/renders/RenderQueue.tsx index fba196055..d264c796c 100644 --- a/packages/studio/src/components/renders/RenderQueue.tsx +++ b/packages/studio/src/components/renders/RenderQueue.tsx @@ -26,13 +26,19 @@ interface RenderQueueProps { compositionDimensions?: CompositionDimensions | null; } -// User-facing render scale. Orientation is derived from the composition's -// authored aspect ratio at render time, so the user never picks an -// orientation that mismatches their comp. +// Orientation is derived from the composition's authored aspect ratio, +// not chosen by the user — picking "1080p portrait" for a landscape comp +// would just produce a wrong-aspect render. type RenderScale = "auto" | "1080p" | "4k"; const SCALE_OPTION_ORDER: RenderScale[] = ["auto", "1080p", "4k"]; +const SCALE_LABEL: Record = { + auto: "Auto", + "1080p": "1080p", + "4k": "4K", +}; + function isPortraitComp(dims: CompositionDimensions | null | undefined): boolean { // Squares and missing dims fall through to landscape — matches the legacy // default ("landscape" was the first preset). The auto option exists for @@ -50,15 +56,6 @@ function resolveResolution( 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( scale: RenderScale, dims: CompositionDimensions | null | undefined, @@ -71,17 +68,14 @@ function resolvedDimensions( 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( scale: RenderScale, dims: CompositionDimensions | null | undefined, ): string { 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 }> = {