feat: 增加轨迹
This commit is contained in:
1 parent
b9fd289caf
commit
e74cf6d28b
42 files changed
+4010
-570
No files matched your search
@@ -1,8 +1,14 @@
|
||||
import { forwardRef } from "react";
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import { PerspectiveCamera, Vector3 } from "three";
|
||||
import { DoubleSide, PerspectiveCamera, Vector3 } from "three";
|
||||
import { clearViewportCaptureHandler, requestViewportCapture } from "../io/captureBridge";
|
||||
import {
|
||||
DEFAULT_DIRECTOR_VIEW_SNAPSHOT,
|
||||
getViewportGizmoHitButtonStyle,
|
||||
VIEWPORT_WHEEL_ZOOM_SPEED,
|
||||
} from "./DirectorCanvas";
|
||||
import { GROUND_PLANE_SIZE } from "./sceneGround";
|
||||
import { getViewportAspectFrameRect } from "./viewportAspectFrame";
|
||||
|
||||
const mockCameraPositionSet = vi.hoisted(() => vi.fn());
|
||||
@@ -150,27 +156,33 @@ vi.mock("@react-three/drei", async () => {
|
||||
return {
|
||||
...actual,
|
||||
Grid: ({
|
||||
args,
|
||||
cellColor,
|
||||
cellThickness,
|
||||
fadeDistance,
|
||||
infiniteGrid,
|
||||
position,
|
||||
sectionColor,
|
||||
side,
|
||||
}: {
|
||||
args?: [number, number];
|
||||
cellColor?: string;
|
||||
cellThickness?: number;
|
||||
fadeDistance?: number;
|
||||
infiniteGrid?: boolean;
|
||||
position?: [number, number, number];
|
||||
sectionColor?: string;
|
||||
side?: number;
|
||||
}) => (
|
||||
<div
|
||||
data-args={JSON.stringify(args)}
|
||||
data-cell-color={cellColor}
|
||||
data-cell-thickness={String(cellThickness)}
|
||||
data-fade-distance={String(fadeDistance)}
|
||||
data-infinite-grid={String(infiniteGrid)}
|
||||
data-infinite-grid={String(Boolean(infiniteGrid))}
|
||||
data-position={JSON.stringify(position)}
|
||||
data-section-color={sectionColor}
|
||||
data-side={String(side)}
|
||||
data-testid="viewport-grid"
|
||||
/>
|
||||
),
|
||||
@@ -204,9 +216,33 @@ vi.mock("@react-three/drei", async () => {
|
||||
type="button"
|
||||
/>
|
||||
),
|
||||
OrbitControls: forwardRef(({ enabled }: { enabled?: boolean }) => (
|
||||
<div data-enabled={String(enabled)} data-testid="orbit-controls" />
|
||||
)),
|
||||
OrbitControls: forwardRef(
|
||||
({
|
||||
enabled,
|
||||
onChange,
|
||||
zoomSpeed,
|
||||
}: {
|
||||
enabled?: boolean;
|
||||
onChange?: (event: unknown) => void;
|
||||
zoomSpeed?: number;
|
||||
}) => (
|
||||
<div data-enabled={String(enabled)} data-testid="orbit-controls" data-zoom-speed={String(zoomSpeed)}>
|
||||
{/* 模拟用户直接操作控件(滚轮缩放 / 拖拽)时控件抛出的 change 事件载荷 */}
|
||||
<button
|
||||
aria-label="模拟操作视角控件"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange?.({
|
||||
target: {
|
||||
object: { fov: 50, position: { x: 9, y: 2.2, z: 0 } },
|
||||
target: { x: 0, y: 1.05, z: 0 },
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
PerspectiveCamera: () => null,
|
||||
Html: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
||||
Line: () => null,
|
||||
@@ -255,14 +291,57 @@ it("does not render a full-viewport transform drag layer over the 3D viewport",
|
||||
expect(screen.queryByRole("application", { name: "3D视口缩放拖拽层" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders only the dark major viewport grid lines", () => {
|
||||
it("renders only the dark major viewport grid lines on the same footprint as the ground", () => {
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-cell-thickness", "0");
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-position", "[0,0.002,0]");
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-section-color", "#2A4065");
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-fade-distance", "80");
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-infinite-grid", "true");
|
||||
const grid = screen.getByTestId("viewport-grid");
|
||||
expect(grid).toHaveAttribute("data-cell-thickness", "0");
|
||||
expect(grid).toHaveAttribute("data-position", "[0,0.002,0]");
|
||||
expect(grid).toHaveAttribute("data-section-color", "#2A4065");
|
||||
expect(grid).toHaveAttribute("data-fade-distance", "80");
|
||||
// 网格不能是"无限网格":默认尺寸(1×1 × (1+fadeDistance))比地面小得多,
|
||||
// 边缘不重合就会被看成另一层地面。这里必须和地面同尺寸。
|
||||
expect(grid).toHaveAttribute("data-infinite-grid", "false");
|
||||
expect(grid).toHaveAttribute("data-args", `[${GROUND_PLANE_SIZE},${GROUND_PLANE_SIZE}]`);
|
||||
});
|
||||
|
||||
it("does not bounce the camera back while the user is driving OrbitControls", () => {
|
||||
render(<App />);
|
||||
mockCameraPositionSet.mockClear();
|
||||
|
||||
// 用户滚轮 / 拖拽时控件每帧都会抛 change;模拟其中一次
|
||||
fireEvent.click(screen.getByRole("button", { name: "模拟操作视角控件" }));
|
||||
|
||||
// 相机归 OrbitControls 逐帧驱动,不能再把回调里那份(上一帧的)快照写回去,
|
||||
// 否则缩放时会出现"缩小时突然又放大一点"的抖动
|
||||
expect(mockCameraPositionSet).not.toHaveBeenCalled();
|
||||
// 快照本身仍照常同步给 UI:坐标控件命中按钮跟着这次视角变化重算
|
||||
const expectedStyle = getViewportGizmoHitButtonStyle(
|
||||
{ fov: 50, position: [9, 2.2, 0], target: [0, 1.05, 0] },
|
||||
[1, 0, 0]
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "切换到 X 正向视图" })).toHaveStyle({
|
||||
left: expectedStyle.left,
|
||||
top: expectedStyle.top,
|
||||
});
|
||||
});
|
||||
|
||||
it("speeds up wheel zoom beyond the three.js default so scrolling feels responsive", () => {
|
||||
render(<App />);
|
||||
|
||||
const controls = screen.getByTestId("orbit-controls");
|
||||
const zoomSpeed = Number(controls.getAttribute("data-zoom-speed"));
|
||||
|
||||
expect(zoomSpeed).toBe(VIEWPORT_WHEEL_ZOOM_SPEED);
|
||||
// three 的 zoomSpeed 默认 1(每格滚轮只推拉约 5%),必须大于它才是"变快"
|
||||
expect(zoomSpeed).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("keeps the viewport grid visible when the view drops below the ground plane", () => {
|
||||
render(<App />);
|
||||
|
||||
// drei 的 Grid 默认 BackSide:视角转到地面以下时会因背面剔除整个消失
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-side", String(DoubleSide));
|
||||
});
|
||||
|
||||
it("keeps the viewport grid slightly above the configured ground plane", () => {
|
||||
@@ -282,6 +361,35 @@ it("keeps the viewport grid slightly above the configured ground plane", () => {
|
||||
expect(screen.getByTestId("viewport-grid")).toHaveAttribute("data-position", "[0,1.502,0]");
|
||||
});
|
||||
|
||||
it("shares the scene transform with the ground plane so the grid never becomes a second floor", () => {
|
||||
useDirectorStore.setState({
|
||||
...useDirectorStore.getState(),
|
||||
project: {
|
||||
...useDirectorStore.getState().project,
|
||||
scene: {
|
||||
...useDirectorStore.getState().project.scene,
|
||||
scale: 2,
|
||||
position: [1, 3, -2],
|
||||
rotation: [0, 0.5, 0],
|
||||
groundHeight: 1.5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
const grid = screen.getByTestId("viewport-grid");
|
||||
const sceneGroup = grid.closest("group");
|
||||
|
||||
// 网格与地面同在场景组内:共享场景平移/旋转/缩放,不会各自留在不同坐标空间
|
||||
expect(sceneGroup).not.toBeNull();
|
||||
expect(sceneGroup).toHaveAttribute("position", "1,3,-2");
|
||||
expect(sceneGroup).toHaveAttribute("rotation", "0,0.5,0");
|
||||
expect(sceneGroup).toHaveAttribute("scale", "2,2,2");
|
||||
// 网格自身的 y 相对场景原点,仍是「地面高度 + 微小抬升」
|
||||
expect(grid).toHaveAttribute("data-position", "[0,1.502,0]");
|
||||
});
|
||||
|
||||
it("opens the scene inspector when users click empty 3D viewport space", () => {
|
||||
useDirectorStore.setState({
|
||||
...useDirectorStore.getState(),
|
||||
@@ -351,11 +459,29 @@ it("offsets the native viewport gizmo inward when overlay side panels are open",
|
||||
|
||||
const gizmo = screen.getByLabelText("3D视口原生坐标控件");
|
||||
|
||||
// 右侧悬浮卡片宽 300px,内缩 16px,再加 16px 间距 → 坐标控件贴在其左侧
|
||||
expect(gizmo).toHaveStyle({
|
||||
right: "320px",
|
||||
right: "332px",
|
||||
});
|
||||
});
|
||||
|
||||
it("resets the director view from the button below the native gizmo", () => {
|
||||
useDirectorStore.getState().setViewMode("camera");
|
||||
render(<App />);
|
||||
|
||||
const resetButton = screen.getByRole("button", { name: "重置视角" });
|
||||
expect(resetButton).toHaveClass("viewport-view-reset-button");
|
||||
// 文字按钮:不再用图标 + 悬停提示
|
||||
expect(resetButton).toHaveTextContent("重置视角");
|
||||
expect(resetButton.querySelector("svg")).not.toBeInTheDocument();
|
||||
mockCameraPositionSet.mockClear();
|
||||
|
||||
fireEvent.click(resetButton);
|
||||
|
||||
expect(useDirectorStore.getState().viewMode).toBe("director");
|
||||
expect(mockCameraPositionSet).toHaveBeenCalledWith(...DEFAULT_DIRECTOR_VIEW_SNAPSHOT.position);
|
||||
});
|
||||
|
||||
it("syncs native viewport gizmo axis clicks back to the main director view", () => {
|
||||
render(<App />);
|
||||
mockCameraPositionSet.mockClear();
|
||||
@@ -544,9 +670,10 @@ it("captures screenshots from the same safe-area frame shown by the aspect overl
|
||||
source: "capture-panel",
|
||||
});
|
||||
|
||||
// 悬浮卡片左右等宽:300 + 16 + 16,与 DirectorCanvas 的安全区保持一致
|
||||
const expectedFrame = getViewportAspectFrameRect("16:9", 1000, 700, 124, {
|
||||
left: 220,
|
||||
right: 300,
|
||||
left: 332,
|
||||
right: 332,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user