fix(studio): show ALL template inputs (bridge V2 content-elements → layers)

The studio parser required scene.layers; a template-created project's scene_data
carries content-elements (scene.contents), so every scene parsed to null and the
editor fell back to the default 2-layer title/subtitle scene. Now parseScene bridges
contents → editable layers (Text→text, Media→image), so all of a scene's inputs
appear (e.g. c1 → 6: 4 text + 2 media). Scene name/duration also read V2 fields.

Remaining studio↔template epic (separate): edit→content-element→AE-render binding,
real AE scene-preview thumbnails, and FIX-mode (hide add-scene).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-06 23:57:44 +03:30
parent 99f0e9eab1
commit 4d32e77f9a
+72 -5
View File
@@ -43,14 +43,75 @@ function parseLayer(value: unknown): Layer | null {
}; };
} }
/**
* Bridge a V2 template/saved-project scene (content-element model, no canvas
* geometry) into editable studio layers so every input shows in the editor.
* Text/TextArea → text layer; Media/Image/Video/Audio → image layer. Laid out in a
* column since AE templates carry no canvas coords. One-time on first load; after the
* user edits + saves, the scene persists in the studio's own layer format.
*/
function layersFromContents(contents: unknown[]): Layer[] {
const str = (v: unknown) => (typeof v === "string" ? v : "");
const num = (v: unknown) => (typeof v === "number" ? v : undefined);
const items = contents.filter(isRecord);
items.sort(
(a, b) =>
(num(a.sort) ?? num(a.positionInContainer) ?? 0) -
(num(b.sort) ?? num(b.positionInContainer) ?? 0)
);
return items.map((c, i) => {
const type = str(c.type).toLowerCase();
const isMedia = ["media", "image", "video", "audio", "voiceover"].includes(type);
const key = str(c.key) || str(c.id) || `el${i}`;
const value = str(c.value) || str(c.defaultValue) || str(c.default_value);
return {
id: `c-${key}`,
type: isMedia ? "image" : "text",
name: str(c.title) || key,
x: 160,
y: 160 + i * 150,
width: 1600,
height: isMedia ? 360 : 110,
rotation: 0,
opacity: 1,
zIndex: i,
props: isMedia
? { src: value }
: {
text: value || (str(c.title) || key),
fontSize: 48,
fill: "#111827",
fontFamily: "Inter, sans-serif",
align: "center",
},
} as Layer;
});
}
function parseScene(value: unknown): Scene | null { function parseScene(value: unknown): Scene | null {
if (!isRecord(value)) return null; if (!isRecord(value)) return null;
if (typeof value.id !== "string" || typeof value.name !== "string") return null; if (typeof value.id !== "string") return null;
if (!Array.isArray(value.layers)) return null; const name =
typeof value.name === "string"
? value.name
: typeof value.title === "string"
? value.title
: typeof value.key === "string"
? value.key
: "Scene";
const layers = value.layers // Studio's own format carries `layers`; a freshly-copied V2 template carries
// `contents` (content elements / inputs) instead — bridge those to layers.
let layers: Layer[];
if (Array.isArray(value.layers)) {
layers = value.layers
.map(parseLayer) .map(parseLayer)
.filter((layer): layer is Layer => layer !== null); .filter((layer): layer is Layer => layer !== null);
} else if (Array.isArray(value.contents)) {
layers = layersFromContents(value.contents);
} else {
return null;
}
const transitionType = const transitionType =
typeof value.transitionType === "string" && typeof value.transitionType === "string" &&
@@ -60,9 +121,15 @@ function parseScene(value: unknown): Scene | null {
return { return {
id: value.id, id: value.id,
name: value.name, name,
duration: duration:
typeof value.duration === "number" ? value.duration : DEFAULT_SCENE_DURATION, typeof value.duration === "number"
? value.duration
: typeof value.sceneLengthSec === "number"
? value.sceneLengthSec
: typeof value.defaultDurationSec === "number"
? value.defaultDurationSec
: DEFAULT_SCENE_DURATION,
layers, layers,
transitionType, transitionType,
}; };