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 { 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<CompositionDimensions | null>(
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);
@@ -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<RenderScale, string> = {
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 }> = {