import { readObjectTracks, type DirectorObjectTrack } from "./director-object-tracks"; import { directorBodies } from "./director-extensions"; import { directorPoses, type DirectorPoseName } from "./director-poses"; import { defaultDirectorMotion, readDirectorMotion, validDirectorPose, type DirectorMotion, } from "./director-motion"; export type DirectorVector = [number, number, number]; export type DirectorObject = { id: string; name: string; kind: "actor" | "box" | "sphere" | "model"; url?: string; color?: string; visible?: boolean; locked?: boolean; pose?: DirectorPoseName; body?: keyof typeof directorBodies; position: DirectorVector; rotation: DirectorVector; scale: DirectorVector; }; export type DirectorCamera = { followObjectId?: string; id: string; name: string; position: DirectorVector; target: DirectorVector; fov: number; }; export type DirectorSnapshot = { id: string; title: string; url: string; width: number; height: number; cameraId: string; }; export type DirectorScene = { objects: DirectorObject[]; cameras: DirectorCamera[]; activeCameraId: string; ratio: string; snapshots: DirectorSnapshot[]; motion: DirectorMotion; motionTracks?: Record; objectTracks?: Record; backgroundUrl?: string; backgroundHistory?: string[]; skyColor?: string; gridVisible?: boolean; groundVisible?: boolean; }; export const directorRatios = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"]; export function directorImageSize(ratio: string) { const [w, h] = (directorRatios.includes(ratio) ? ratio : "16:9").split(":").map(Number); return w >= h ? { width: 1280, height: Math.round((1280 * h) / w) } : { width: Math.round((1280 * w) / h), height: 1280 }; } export function defaultDirectorScene(): DirectorScene { return { objects: [ { id: "actor-a", name: "基础人偶", kind: "actor", position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1], }, ], cameras: [ { id: "camera-1", name: "机位1", position: [4, 3, 7], target: [0, 1, 0], fov: 50, }, ], activeCameraId: "camera-1", ratio: "16:9", snapshots: [], motion: defaultDirectorMotion(), motionTracks: {}, objectTracks: {}, skyColor: "#080808", gridVisible: true, groundVisible: true, }; } export function readDirectorScene(value: unknown): DirectorScene { const fallback = defaultDirectorScene(); if (!value || typeof value !== "object") return fallback; const v = value as DirectorScene; const vector = (a: unknown): a is DirectorVector => Array.isArray(a) && a.length === 3 && a.every((n) => typeof n === "number" && Number.isFinite(n)); const cameras = Array.isArray(v.cameras) ? v.cameras.filter( (c) => c && typeof c.id === "string" && typeof c.name === "string" && vector(c.position) && vector(c.target) && Number.isFinite(c.fov) && c.fov >= 15 && c.fov <= 90 && validDirectorPose(c), ) : []; const objects = Array.isArray(v.objects) ? v.objects.filter( (o) => o && typeof o.id === "string" && typeof o.name === "string" && ["actor", "box", "sphere", "model"].includes(o.kind) && vector(o.position) && vector(o.rotation) && vector(o.scale) && o.scale.every((n) => n > 0) && (o.kind !== "model" || /^https?:\/\//i.test(o.url || "")), ) : fallback.objects; const snapshots = Array.isArray(v.snapshots) ? v.snapshots.filter( (s) => s && typeof s.id === "string" && typeof s.title === "string" && /^https?:\/\//i.test(s.url) && s.width > 0 && s.height > 0, ) : []; const validCameras = cameras.length ? cameras : fallback.cameras; return { objects: objects.map((o) => { const clean = { ...o }; if ( o.color !== undefined && (typeof o.color !== "string" || !/^#[0-9a-f]{6}$/i.test(o.color)) ) delete clean.color; if (o.visible !== undefined) clean.visible = o.visible !== false; if (o.locked !== undefined) clean.locked = o.locked === true; if (o.pose !== undefined && !Object.hasOwn(directorPoses, o.pose)) delete clean.pose; if (o.body !== undefined && !Object.hasOwn(directorBodies, o.body)) delete clean.body; return clean; }), cameras: validCameras.map((c) => { const clean = { ...c }; if (!objects.some((o) => o.id === c.followObjectId)) delete clean.followObjectId; return clean; }), activeCameraId: validCameras.some((c) => c.id === v.activeCameraId) ? v.activeCameraId : validCameras[0].id, ratio: directorRatios.includes(v.ratio) ? v.ratio : "16:9", skyColor: typeof v.skyColor === "string" && /^#[0-9a-f]{6}$/i.test(v.skyColor) ? v.skyColor : fallback.skyColor, gridVisible: v.gridVisible !== false, groundVisible: v.groundVisible !== false, snapshots, motion: readDirectorMotion(v.motion), ...(typeof v.backgroundUrl === "string" && /^https?:\/\//i.test(v.backgroundUrl) ? { backgroundUrl: v.backgroundUrl } : {}), ...(Array.isArray(v.backgroundHistory) ? { backgroundHistory: [ ...new Set( v.backgroundHistory.filter( (url) => typeof url === "string" && /^https?:\/\//i.test(url), ), ), ].slice(-20), } : {}), objectTracks: readObjectTracks(v.objectTracks, objects, readDirectorMotion(v.motion).duration), motionTracks: Object.fromEntries( Object.entries(v.motionTracks || {}) .filter(([id]) => validCameras.some((c) => c.id === id)) .map(([id, motion]) => [id, readDirectorMotion(motion)]), ), }; }