113 lines
4.8 KiB
TypeScript
113 lines
4.8 KiB
TypeScript
import {
|
||
applyExternalViewSnapshot,
|
||
applySnapshotToControls,
|
||
DEFAULT_DIRECTOR_VIEW_SNAPSHOT,
|
||
getViewportGizmoHitButtonStyle,
|
||
getViewportSnapshotFromGizmoDirection,
|
||
shouldRenderViewportGrid,
|
||
} from "./DirectorCanvas";
|
||
import { createDefaultDirectorProject } from "../store/directorStore";
|
||
import { getCameraViewSnapshotFromShot } from "../schema/cameraGeometry";
|
||
import { PerspectiveCamera, Vector3 } from "three";
|
||
|
||
function expectTupleToBeCloseTo(
|
||
received: [number, number, number],
|
||
expected: [number, number, number]
|
||
) {
|
||
received.forEach((value, index) => {
|
||
expect(value).toBeCloseTo(expected[index], 5);
|
||
});
|
||
}
|
||
|
||
it("keeps the viewport grid visible when there is no panorama background", () => {
|
||
expect(shouldRenderViewportGrid(false, false)).toBe(true);
|
||
});
|
||
|
||
it("flushes leftover orbit inertia before writing the reset view back", () => {
|
||
const dampingStateDuringUpdate: boolean[] = [];
|
||
const controls = {
|
||
enableDamping: true,
|
||
target: new Vector3(9, 9, 9),
|
||
update: () => dampingStateDuringUpdate.push(controls.enableDamping),
|
||
};
|
||
const camera = new PerspectiveCamera();
|
||
|
||
applySnapshotToControls(controls, camera, DEFAULT_DIRECTOR_VIEW_SNAPSHOT);
|
||
|
||
// 两次 update 都在「阻尼关闭」状态下跑:第一次结算残余惯性,第二次稳在新视角上。
|
||
// 如果带着残余惯性直接写回,画布还在滑行时点重置视角就会被继续推走(表现为重置无效)。
|
||
expect(dampingStateDuringUpdate).toEqual([false, false]);
|
||
// 用完把阻尼恢复原状
|
||
expect(controls.enableDamping).toBe(true);
|
||
expect(controls.target.toArray()).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.target);
|
||
expect(camera.fov).toBe(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.fov);
|
||
expect(camera.position.toArray()).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.position);
|
||
});
|
||
|
||
it("writes an external view change on the spot instead of deferring it to the next render", () => {
|
||
// 机位视角下没有 OrbitControls:写不进去,交给切回导演视角时的副作用补写
|
||
expect(applyExternalViewSnapshot(null, DEFAULT_DIRECTOR_VIEW_SNAPSHOT)).toBe(false);
|
||
|
||
const dampingStateDuringUpdate: boolean[] = [];
|
||
const controls = {
|
||
enableDamping: true,
|
||
target: new Vector3(9, 9, 9),
|
||
object: new PerspectiveCamera(),
|
||
update: () => dampingStateDuringUpdate.push(controls.enableDamping),
|
||
};
|
||
|
||
expect(applyExternalViewSnapshot(controls, DEFAULT_DIRECTOR_VIEW_SNAPSHOT)).toBe(true);
|
||
|
||
// 当场就把残余惯性结算掉并落到目标视角(惯性滑行中点重置视角不会再被推走)
|
||
expect(dampingStateDuringUpdate).toEqual([false, false]);
|
||
expect(controls.target.toArray()).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.target);
|
||
expect(controls.object.position.toArray()).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.position);
|
||
expect(controls.enableDamping).toBe(true);
|
||
});
|
||
|
||
it("keeps the viewport grid visible when a panorama background is active", () => {
|
||
expect(shouldRenderViewportGrid(true, false)).toBe(true);
|
||
});
|
||
|
||
it("keeps the viewport grid visible when snap to grid is enabled during a panorama background", () => {
|
||
expect(shouldRenderViewportGrid(true, true)).toBe(true);
|
||
});
|
||
|
||
it("starts the director and default camera view from a centered front composition", () => {
|
||
const defaultCamera = createDefaultDirectorProject().cameras[0];
|
||
|
||
expect(DEFAULT_DIRECTOR_VIEW_SNAPSHOT).toEqual({
|
||
fov: 50,
|
||
position: [0, 1.55, 5.4],
|
||
target: [0, 1.05, 0],
|
||
});
|
||
const defaultCameraView = getCameraViewSnapshotFromShot(defaultCamera);
|
||
|
||
expect(defaultCameraView.fov).toBe(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.fov);
|
||
expectTupleToBeCloseTo(defaultCameraView.position, defaultCamera.transform.position);
|
||
expect(defaultCameraView.target).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.target);
|
||
expect(defaultCamera.transform.position[0]).toBe(0);
|
||
expect(defaultCamera.transform.position[1]).toBeGreaterThan(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.target[1]);
|
||
});
|
||
|
||
it("keeps the current orbit distance when native gizmo axis clicks switch viewport direction", () => {
|
||
const snapshot = getViewportSnapshotFromGizmoDirection(
|
||
DEFAULT_DIRECTOR_VIEW_SNAPSHOT,
|
||
new Vector3(1, 0, 0)
|
||
);
|
||
|
||
expect(snapshot.fov).toBe(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.fov);
|
||
expect(snapshot.target).toEqual(DEFAULT_DIRECTOR_VIEW_SNAPSHOT.target);
|
||
expectTupleToBeCloseTo(snapshot.position, [5.423099, 1.05, 0]);
|
||
});
|
||
|
||
it("positions the transparent native gizmo hit target over the visible X axis head", () => {
|
||
const style = getViewportGizmoHitButtonStyle(DEFAULT_DIRECTOR_VIEW_SNAPSHOT, [1, 0, 0]);
|
||
|
||
// 命中框以 .viewport-gizmo-overlay 的 80px 图层为基准(见 index.css 契约测试):
|
||
// 中心 40px,X 轴正向落在屏幕半径 25px 处,15px 命中框需回退半个身位。
|
||
expect(style.left).toBe("57.5px");
|
||
expect(style.top).toBe("32.5px");
|
||
expect(style.zIndex).toBe(100);
|
||
});
|