70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
import { z } from "zod";
|
||
import { defaultDirectorScene, type DirectorScene } from "./director-scene";
|
||
import { validDirectorPose } from "./director-motion";
|
||
const vector = z.tuple([
|
||
z.number().finite().min(-100).max(100),
|
||
z.number().finite().min(-100).max(100),
|
||
z.number().finite().min(-100).max(100),
|
||
]);
|
||
const scale = z.tuple([
|
||
z.number().min(0.05).max(30),
|
||
z.number().min(0.05).max(30),
|
||
z.number().min(0.05).max(30),
|
||
]);
|
||
const reconstructionSchema = z.object({
|
||
summary: z.string().min(1).max(2000),
|
||
objects: z
|
||
.array(
|
||
z.object({
|
||
name: z.string().min(1).max(80),
|
||
kind: z.enum(["actor", "box", "sphere"]),
|
||
position: vector,
|
||
rotation: vector,
|
||
scale,
|
||
color: z.string().regex(/^#[0-9a-f]{6}$/i),
|
||
}),
|
||
)
|
||
.min(1)
|
||
.max(30),
|
||
camera: z
|
||
.object({ position: vector, target: vector, fov: z.number().min(15).max(90) })
|
||
.refine(validDirectorPose, "相机不能与注视点重合"),
|
||
});
|
||
export function reconstructionPrompt(prompt: string) {
|
||
return `根据参考图重建可编辑的3D布景(灰盒),不是输出图片或GLB。只输出JSON,字段:summary、objects、camera。只重建看得见的主要人物和物体,忽略细碎纹理,不编造遮挡后的形状。以米为近似单位,Y轴向上,地面y=0;对象局部几何 actor 约2米高,box边长1米,sphere直径1米,scale必须据此设置;对象中心/脚底位置遵循 actor脚底、box和sphere中心。rotation是弧度。所有坐标在±100内,scale在0.05到30内。objects最多30项,每项{name,kind:"actor"|"box"|"sphere",position:[x,y,z],rotation:[rx,ry,rz],scale:[sx,sy,sz],color:"#RRGGBB"}。camera={position:[x,y,z],target:[x,y,z],fov:15到90},还原参考图的构图。summary说明不确定的比例与缺失信息。用户布景要求:${prompt || "尽量保留原图主体布局与机位"}`;
|
||
}
|
||
export function parseReconstructedScene(value: unknown): {
|
||
summary: string;
|
||
directorScene: DirectorScene;
|
||
} {
|
||
const parsed = reconstructionSchema.parse(value),
|
||
base = defaultDirectorScene();
|
||
return {
|
||
summary: parsed.summary,
|
||
directorScene: {
|
||
...base,
|
||
objects: parsed.objects.map((o, i) => ({ ...o, id: `reconstructed-${i}` })),
|
||
cameras: [{ ...parsed.camera, id: "reconstructed-camera", name: "参考图机位" }],
|
||
activeCameraId: "reconstructed-camera",
|
||
},
|
||
};
|
||
}
|
||
export function insertReconstructedScene(
|
||
current: DirectorScene,
|
||
generated: DirectorScene,
|
||
replace: boolean,
|
||
id: () => string,
|
||
): DirectorScene {
|
||
const objects = generated.objects.map((o) => ({ ...o, id: id() }));
|
||
if ((replace ? 0 : current.objects.length) + objects.length > 200)
|
||
throw Error("场景最多支持 200 个对象");
|
||
const camera = { ...generated.cameras[0], id: id(), followObjectId: undefined };
|
||
return {
|
||
...current,
|
||
objects: replace ? objects : [...current.objects, ...objects],
|
||
cameras: [...current.cameras, camera],
|
||
activeCameraId: camera.id,
|
||
...(replace ? { objectTracks: {} } : {}),
|
||
};
|
||
}
|