feat(previs): 预演工作台 - 重做界面与取景手势,缩放不得进入演员体积

滚轮和按钮共用最近距离,环绕目标落在演员体内时停在包围盒外;取景按可见对象体积适配入画,取消暂存手势时不写入历史。
This commit is contained in:
ddcat666
2026-10-08 10:54:41 +08:00
parent f67a304380
commit f0359a6bee
5 changed files with 4042 additions and 1546 deletions

View File

@@ -0,0 +1,73 @@
import { Vector3 } from "three";
import { previsActorProfileForArchetype } from "@/lib/canvas/previs/previs-scene";
import type { PrevisObject } from "@/types/previs";
export type PrevisFrameBounds = {
min: Vector3;
max: Vector3;
span: Vector3;
target: Vector3;
fitDistance: number;
};
/** Smallest orbit distance allowed regardless of scene contents. */
export const PREVIS_MIN_ORBIT_DISTANCE = 0.7;
/** 单个对象的近似世界包围盒;演员按体积而不是原点计算,避免相机推进到身体内部。 */
export function resolvePrevisObjectBox(object: PrevisObject): { min: Vector3; max: Vector3 } {
const [x, y, z] = object.transform.position;
const [sx, sy, sz] = object.transform.scale;
const actor = object.kind === "actor" || object.primitive === "character";
if (actor) {
const profile = { ...previsActorProfileForArchetype(object.archetype || "adult"), ...(object.actorProfile || {}) };
const actorScaleY = Math.max(0.1, Math.abs(sy) * profile.height);
const actorHeight = object.pose === "sit" || object.pose === "squat" ? 2.05 : profile.accessory === "horns" ? 2.8 : 2.45;
const halfWidth = 0.78 * Math.max(0.8, profile.shoulderWidth) * Math.max(0.1, Math.abs(sx));
const halfDepth = 0.72 * Math.max(0.1, Math.abs(sz));
return { min: new Vector3(x - halfWidth, y - 0.12 * actorScaleY, z - halfDepth), max: new Vector3(x + halfWidth, y + actorHeight * actorScaleY, z + halfDepth) };
}
const primitiveBounds: [number, number, number] = object.primitive === "sphere" ? [0.6, 0.6, 0.6] : object.primitive === "cylinder" ? [0.55, 0.8, 0.55] : object.primitive === "plane" ? [1.2, 0.05, 1.2] : [0.6, 0.6, 0.6];
const half = new Vector3(primitiveBounds[0] * Math.max(0.1, Math.abs(sx)), primitiveBounds[1] * Math.max(0.1, Math.abs(sy)), primitiveBounds[2] * Math.max(0.1, Math.abs(sz)));
return { min: new Vector3(x, y, z).sub(half), max: new Vector3(x, y, z).add(half) };
}
/**
* 缩放的最近距离:相机不得进入包含环绕目标的对象体积(否则背面剔除让身体消失、只剩地面阴影)。
* 只看包含目标的对象,大场景里推近其它区域不受整体尺寸限制。
*/
export function resolvePrevisZoomMinDistance(objects: PrevisObject[], target: Vector3): number {
let minDistance = PREVIS_MIN_ORBIT_DISTANCE;
const margin = 0.15;
objects.forEach((object) => {
if (!object.visible || object.primitive === "plane") return;
const box = resolvePrevisObjectBox(object);
const contains = target.x >= box.min.x - margin && target.x <= box.max.x + margin && target.y >= box.min.y - margin && target.y <= box.max.y + margin && target.z >= box.min.z - margin && target.z <= box.max.z + margin;
if (!contains) return;
// 目标到盒子最远角的距离 + 近裁剪余量,保证整个体积都在相机前方。
const farthest = new Vector3(Math.max(target.x - box.min.x, box.max.x - target.x), Math.max(target.y - box.min.y, box.max.y - target.y), Math.max(target.z - box.min.z, box.max.z - target.z));
minDistance = Math.max(minDistance, farthest.length() + 0.35);
});
return minDistance;
}
/** 可见对象(可选限定 ids)的联合包围盒,以及在给定 aspect/fov 下完整入画所需的距离。 */
export function resolvePrevisFrameBounds(objects: PrevisObject[], ids?: Set<string>, aspect = 1, fov = 50): PrevisFrameBounds | null {
const visible = objects.filter((object) => object.visible && (!ids || ids.has(object.id)));
if (!visible.length) return null;
const min = new Vector3(Infinity, Infinity, Infinity);
const max = new Vector3(-Infinity, -Infinity, -Infinity);
visible.forEach((object) => {
const box = resolvePrevisObjectBox(object);
min.min(box.min);
max.max(box.max);
});
const target = min.clone().add(max).multiplyScalar(0.5);
const span = max.clone().sub(min);
const verticalHalfFov = (Math.max(1, fov) * Math.PI) / 360;
const horizontalHalfFov = Math.atan(Math.tan(verticalHalfFov) * Math.max(0.1, aspect));
const fitDistance = Math.max(span.y / 2 / Math.tan(verticalHalfFov), Math.max(span.x, span.z) / 2 / Math.tan(horizontalHalfFov)) * 1.08;
return { min, max, span, target, fitDistance };
}