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,46 @@
import { describe, expect, test } from "bun:test";
import { Vector3 } from "three";
import { PREVIS_MIN_ORBIT_DISTANCE, resolvePrevisFrameBounds, resolvePrevisObjectBox, resolvePrevisZoomMinDistance } from "@/lib/canvas/previs/previs-frame-bounds";
import { createPrevisActor, createPrevisObject } from "@/lib/canvas/previs/previs-scene";
describe("previs frame bounds", () => {
test("treats actors as body volumes instead of origin points", () => {
const box = resolvePrevisObjectBox(createPrevisActor("A", [0, 0, 0]));
expect(box.max.y).toBeGreaterThan(1.5);
expect(box.max.x - box.min.x).toBeGreaterThan(0.5);
});
test("keeps the orbit camera outside an actor that contains the target", () => {
const actor = createPrevisActor("A", [0, 0, 0]);
const target = new Vector3(0, 1, 0);
const minDistance = resolvePrevisZoomMinDistance([actor], target);
const box = resolvePrevisObjectBox(actor);
// Any camera position at minDistance from the target lies outside the actor volume.
const corner = new Vector3(Math.max(-box.min.x, box.max.x), Math.max(target.y - box.min.y, box.max.y - target.y), Math.max(-box.min.z, box.max.z));
expect(minDistance).toBeGreaterThan(corner.length());
});
test("does not restrict zoom when the target is away from every object", () => {
const actor = createPrevisActor("A", [0, 0, 0]);
expect(resolvePrevisZoomMinDistance([actor], new Vector3(10, 1, 10))).toBe(PREVIS_MIN_ORBIT_DISTANCE);
});
test("ignores hidden objects and ground planes", () => {
const hidden = { ...createPrevisActor("A", [0, 0, 0]), visible: false };
const plane = createPrevisObject("plane", "floor", [0, 1, 0]);
expect(resolvePrevisZoomMinDistance([hidden, plane], new Vector3(0, 1, 0))).toBe(PREVIS_MIN_ORBIT_DISTANCE);
expect(resolvePrevisFrameBounds([hidden])).toBeNull();
});
test("fits every visible actor and needs more distance in portrait viewports", () => {
const actors = [createPrevisActor("A", [0, 0, 0]), createPrevisActor("B", [1.8, 0, -0.8])];
const wide = resolvePrevisFrameBounds(actors, undefined, 16 / 9, 50);
const tall = resolvePrevisFrameBounds(actors, undefined, 9 / 16, 50);
expect(wide).not.toBeNull();
expect(tall).not.toBeNull();
expect(wide!.min.x).toBeLessThan(0);
expect(wide!.max.x).toBeGreaterThan(1.8);
expect(tall!.fitDistance).toBeGreaterThan(wide!.fitDistance);
});
});