184 lines
5.6 KiB
TypeScript
184 lines
5.6 KiB
TypeScript
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<string, DirectorMotion>;
|
|
objectTracks?: Record<string, DirectorObjectTrack>;
|
|
backgroundUrl?: string;
|
|
backgroundHistory?: string[];
|
|
};
|
|
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: {},
|
|
};
|
|
}
|
|
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",
|
|
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)]),
|
|
),
|
|
};
|
|
}
|