auto-git:

[change] src/app/App.tsx
 [change] src/app/editor-store.ts
 [change] src/core/transform-session.ts
 [change] src/viewport-three/ViewportCanvas.tsx
 [change] src/viewport-three/ViewportPanel.tsx
 [change] src/viewport-three/viewport-host.ts
 [change] tests/unit/viewport-canvas.test.tsx
This commit is contained in:
2026-04-11 02:44:48 +02:00
parent 9950444027
commit 4a66cccd79
9 changed files with 7825 additions and 3324 deletions

View File

@@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest";
import { createEditorStore } from "../../src/app/editor-store";
import { createModelInstance } from "../../src/assets/model-instances";
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
import {
createProjectAssetStorageKey,
type ModelAssetRecord
} from "../../src/assets/project-assets";
import { createCommitTransformSessionCommand } from "../../src/commands/commit-transform-session-command";
import {
createTransformSession,
@@ -11,7 +14,10 @@ import {
supportsTransformAxisConstraint,
supportsTransformOperation
} from "../../src/core/transform-session";
import { cloneBoxBrushGeometry, createBoxBrush } from "../../src/document/brushes";
import {
cloneBoxBrushGeometry,
createBoxBrush
} from "../../src/document/brushes";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
import { getBoxBrushLocalVertexPosition } from "../../src/geometry/box-brush-mesh";
@@ -137,17 +143,62 @@ describe("transform session commit commands", () => {
initialSize: brush.size
});
expect(objectResolved.target).not.toBeNull();
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "translate")).toBe(true);
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "rotate")).toBe(true);
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "scale")).toBe(true);
expect(
supportsTransformOperation(
objectResolved.target as NonNullable<typeof objectResolved.target>,
"translate"
)
).toBe(true);
expect(
supportsTransformOperation(
objectResolved.target as NonNullable<typeof objectResolved.target>,
"rotate"
)
).toBe(true);
expect(
supportsTransformOperation(
objectResolved.target as NonNullable<typeof objectResolved.target>,
"scale"
)
).toBe(true);
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "translate")).toBe(true);
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "rotate")).toBe(true);
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "scale")).toBe(true);
expect(
supportsTransformOperation(
faceResolved.target as NonNullable<typeof faceResolved.target>,
"translate"
)
).toBe(true);
expect(
supportsTransformOperation(
faceResolved.target as NonNullable<typeof faceResolved.target>,
"rotate"
)
).toBe(true);
expect(
supportsTransformOperation(
faceResolved.target as NonNullable<typeof faceResolved.target>,
"scale"
)
).toBe(true);
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "translate")).toBe(true);
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "rotate")).toBe(false);
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "scale")).toBe(false);
expect(
supportsTransformOperation(
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
"translate"
)
).toBe(true);
expect(
supportsTransformOperation(
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
"rotate"
)
).toBe(false);
expect(
supportsTransformOperation(
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
"scale"
)
).toBe(false);
});
it("applies axis-constraint rules across object and component transform sessions", () => {
@@ -228,9 +279,15 @@ describe("transform session commit commands", () => {
expect(supportsTransformAxisConstraint(edgeScaleSession, "y")).toBe(false);
expect(supportsTransformAxisConstraint(edgeScaleSession, "z")).toBe(true);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "x")).toBe(true);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "y")).toBe(true);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "z")).toBe(true);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "x")).toBe(
true
);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "y")).toBe(
true
);
expect(supportsTransformAxisConstraint(vertexTranslateSession, "z")).toBe(
true
);
});
it("only enables local axis toggling on supported transform targets", () => {
@@ -311,10 +368,18 @@ describe("transform session commit commands", () => {
target: entityTarget
});
expect(supportsLocalTransformAxisConstraint(brushTranslateSession, "z")).toBe(true);
expect(supportsLocalTransformAxisConstraint(brushScaleSession, "z")).toBe(false);
expect(supportsLocalTransformAxisConstraint(faceRotateSession, "x")).toBe(false);
expect(supportsLocalTransformAxisConstraint(entityTranslateSession, "x")).toBe(true);
expect(
supportsLocalTransformAxisConstraint(brushTranslateSession, "z")
).toBe(true);
expect(supportsLocalTransformAxisConstraint(brushScaleSession, "z")).toBe(
false
);
expect(supportsLocalTransformAxisConstraint(faceRotateSession, "x")).toBe(
false
);
expect(
supportsLocalTransformAxisConstraint(entityTranslateSession, "x")
).toBe(true);
});
it("commits whitebox box rotate and scale transforms with undo and redo", () => {
@@ -371,13 +436,20 @@ describe("transform session commit commands", () => {
geometry: target.initialGeometry
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, rotateSession));
store.executeCommand(
createCommitTransformSessionCommand(
store.getState().document,
rotateSession
)
);
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual({
x: 0,
y: 37.5,
z: 12.5
});
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual(
{
x: 0,
y: 37.5,
z: 12.5
}
);
const scaleTarget = resolveTransformTarget(store.getState().document, {
kind: "brushes",
@@ -385,7 +457,9 @@ describe("transform session commit commands", () => {
}).target;
if (scaleTarget === null || scaleTarget.kind !== "brush") {
throw new Error("Expected a whitebox box transform target after rotation.");
throw new Error(
"Expected a whitebox box transform target after rotation."
);
}
const scaleSession = createTransformSession({
@@ -413,7 +487,12 @@ describe("transform session commit commands", () => {
geometry: scaleTarget.initialGeometry
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, scaleSession));
store.executeCommand(
createCommitTransformSessionCommand(
store.getState().document,
scaleSession
)
);
expect(store.getState().document.brushes[brush.id]).toMatchObject({
rotationDegrees: {
@@ -504,7 +583,9 @@ describe("transform session commit commands", () => {
geometry: createBoxBrush({ size: { x: 3, y: 2, z: 2 } }).geometry
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
store.executeCommand(
createCommitTransformSessionCommand(store.getState().document, session)
);
expect(store.getState().document.brushes[brush.id]).toMatchObject({
center: { x: 0.5, y: 1, z: 0 },
@@ -569,7 +650,9 @@ describe("transform session commit commands", () => {
geometry: createBoxBrush({ size: { x: 3, y: 3, z: 3 } }).geometry
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
store.executeCommand(
createCommitTransformSessionCommand(store.getState().document, session)
);
expect(store.getState().document.brushes[brush.id]).toMatchObject({
center: { x: 0.5, y: 1.5, z: 0.5 },
@@ -640,11 +723,17 @@ describe("transform session commit commands", () => {
geometry: deformedGeometry
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
store.executeCommand(
createCommitTransformSessionCommand(store.getState().document, session)
);
const committedBrush = store.getState().document.brushes[brush.id];
expect(getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_posZ").x).toBe(2);
expect(getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_negZ").x).toBe(1);
expect(
getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_posZ").x
).toBe(2);
expect(
getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_negZ").x
).toBe(1);
});
it("commits a model instance translate/rotate/scale transform with undo and redo", () => {
@@ -713,9 +802,13 @@ describe("transform session commit commands", () => {
}
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
store.executeCommand(
createCommitTransformSessionCommand(store.getState().document, session)
);
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
expect(
store.getState().document.modelInstances[modelInstance.id]
).toMatchObject({
position: {
x: 4,
y: 1,
@@ -734,10 +827,14 @@ describe("transform session commit commands", () => {
});
expect(store.undo()).toBe(true);
expect(store.getState().document.modelInstances[modelInstance.id]).toEqual(modelInstance);
expect(store.getState().document.modelInstances[modelInstance.id]).toEqual(
modelInstance
);
expect(store.redo()).toBe(true);
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
expect(
store.getState().document.modelInstances[modelInstance.id]
).toMatchObject({
position: {
x: 4,
y: 1,
@@ -803,7 +900,9 @@ describe("transform session commit commands", () => {
}
};
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
store.executeCommand(
createCommitTransformSessionCommand(store.getState().document, session)
);
expect(store.getState().document.entities[playerStart.id]).toMatchObject({
position: {
@@ -815,7 +914,9 @@ describe("transform session commit commands", () => {
});
expect(store.undo()).toBe(true);
expect(store.getState().document.entities[playerStart.id]).toEqual(playerStart);
expect(store.getState().document.entities[playerStart.id]).toEqual(
playerStart
);
expect(store.redo()).toBe(true);
expect(store.getState().document.entities[playerStart.id]).toMatchObject({

View File

@@ -5,8 +5,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { App } from "../../src/app/App";
import { createEditorStore } from "../../src/app/editor-store";
import { createModelInstance } from "../../src/assets/model-instances";
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
import type { ActiveTransformSession, TransformSessionState } from "../../src/core/transform-session";
import {
createProjectAssetStorageKey,
type ModelAssetRecord
} from "../../src/assets/project-assets";
import type {
ActiveTransformSession,
TransformSessionState
} from "../../src/core/transform-session";
import { createBoxBrush } from "../../src/document/brushes";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
@@ -132,7 +138,9 @@ const modelAsset = {
} satisfies ModelAssetRecord;
function getTopLeftViewportHost() {
const viewportHost = viewportHostInstances.find((instance) => instance.panelId === "topLeft");
const viewportHost = viewportHostInstances.find(
(instance) => instance.panelId === "topLeft"
);
if (viewportHost === undefined) {
throw new Error("Top-left viewport host was not mounted.");
@@ -193,7 +201,9 @@ async function renderTransformFixtureApp() {
await waitFor(() => {
expect(viewportHostInstances.length).toBeGreaterThan(0);
expect(getTopLeftViewportHost().setTransformCommitHandler).toHaveBeenCalled();
expect(
getTopLeftViewportHost().setTransformCommitHandler
).toHaveBeenCalled();
});
return {
@@ -215,8 +225,11 @@ async function renderQuadTransformFixtureApp() {
return fixture;
}
function getLatestTransformSession(store: ReturnType<typeof createEditorStore>): ActiveTransformSession {
const transformSession = store.getState().viewportTransientState.transformSession;
function getLatestTransformSession(
store: ReturnType<typeof createEditorStore>
): ActiveTransformSession {
const transformSession =
store.getState().viewportTransientState.transformSession;
if (transformSession.kind !== "active") {
throw new Error("Expected an active transform session.");
@@ -229,7 +242,9 @@ function emitTransformPreview(
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
transformSession: ActiveTransformSession
) {
const handler = viewportHost.setTransformSessionChangeHandler.mock.calls.at(-1)?.[0] as ((transformSession: TransformSessionState) => void) | undefined;
const handler = viewportHost.setTransformSessionChangeHandler.mock.calls.at(
-1
)?.[0] as ((transformSession: TransformSessionState) => void) | undefined;
if (handler === undefined) {
throw new Error("Transform session change handler was not registered.");
@@ -240,8 +255,13 @@ function emitTransformPreview(
});
}
function commitTransform(viewportHost: ReturnType<typeof getTopLeftViewportHost>, transformSession: ActiveTransformSession) {
const handler = viewportHost.setTransformCommitHandler.mock.calls.at(-1)?.[0] as ((transformSession: ActiveTransformSession) => void) | undefined;
function commitTransform(
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
transformSession: ActiveTransformSession
) {
const handler = viewportHost.setTransformCommitHandler.mock.calls.at(
-1
)?.[0] as ((transformSession: ActiveTransformSession) => void) | undefined;
if (handler === undefined) {
throw new Error("Transform commit handler was not registered.");
@@ -252,8 +272,13 @@ function commitTransform(viewportHost: ReturnType<typeof getTopLeftViewportHost>
});
}
function emitCameraStateChange(viewportHost: ReturnType<typeof getTopLeftViewportHost>, cameraState: ViewportPanelCameraState) {
const handler = viewportHost.setCameraStateChangeHandler.mock.calls.at(-1)?.[0] as ((cameraState: ViewportPanelCameraState) => void) | undefined;
function emitCameraStateChange(
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
cameraState: ViewportPanelCameraState
) {
const handler = viewportHost.setCameraStateChangeHandler.mock.calls.at(
-1
)?.[0] as ((cameraState: ViewportPanelCameraState) => void) | undefined;
if (handler === undefined) {
throw new Error("Camera state change handler was not registered.");
@@ -267,7 +292,9 @@ function emitCameraStateChange(viewportHost: ReturnType<typeof getTopLeftViewpor
describe("transform foundation integration", () => {
beforeEach(() => {
viewportHostInstances.length = 0;
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => ({}) as never);
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(
() => ({}) as never
);
});
afterEach(() => {
@@ -278,7 +305,9 @@ describe("transform foundation integration", () => {
const { store, brush, viewportHost } = await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -286,7 +315,9 @@ describe("transform foundation integration", () => {
code: "KeyG"
});
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
expect(
store.getState().viewportTransientState.transformSession
).toMatchObject({
kind: "active",
operation: "translate",
axisConstraint: null,
@@ -301,7 +332,9 @@ describe("transform foundation integration", () => {
code: "KeyX"
});
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
expect(
store.getState().viewportTransientState.transformSession
).toMatchObject({
kind: "active",
axisConstraint: "x"
});
@@ -342,7 +375,9 @@ describe("transform foundation integration", () => {
const { store, brush, viewportHost } = await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
fireEvent.click(screen.getByTestId("transform-rotate-button"));
@@ -369,11 +404,13 @@ describe("transform foundation integration", () => {
emitTransformPreview(viewportHost, rotatePreviewSession);
commitTransform(viewportHost, rotatePreviewSession);
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual({
x: 0,
y: 37.5,
z: 12.5
});
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual(
{
x: 0,
y: 37.5,
z: 12.5
}
);
fireEvent.click(screen.getByTestId("transform-scale-button"));
@@ -425,7 +462,9 @@ describe("transform foundation integration", () => {
const { store, brush } = await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
expect(screen.getByTestId("transform-translate-button")).not.toBeDisabled();
@@ -480,10 +519,13 @@ describe("transform foundation integration", () => {
});
it("moves an entity through the shared transform controller", async () => {
const { store, playerStart, viewportHost } = await renderTransformFixtureApp();
const { store, playerStart, viewportHost } =
await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Player Start Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Player Start Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -520,10 +562,13 @@ describe("transform foundation integration", () => {
});
it("cancels an active transform with Escape without committing preview changes", async () => {
const { store, playerStart, viewportHost } = await renderTransformFixtureApp();
const { store, playerStart, viewportHost } =
await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Player Start Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Player Start Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -555,14 +600,19 @@ describe("transform foundation integration", () => {
expect(store.getState().viewportTransientState.transformSession).toEqual({
kind: "none"
});
expect(store.getState().document.entities[playerStart.id]).toEqual(playerStart);
expect(store.getState().document.entities[playerStart.id]).toEqual(
playerStart
);
});
it("moves a model instance through the shared transform controller", async () => {
const { store, modelInstance, viewportHost } = await renderTransformFixtureApp();
const { store, modelInstance, viewportHost } =
await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Model Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Model Transform Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -591,7 +641,9 @@ describe("transform foundation integration", () => {
emitTransformPreview(viewportHost, previewSession);
commitTransform(viewportHost, previewSession);
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
expect(
store.getState().document.modelInstances[modelInstance.id]
).toMatchObject({
position: {
x: -1,
y: 0,
@@ -604,7 +656,9 @@ describe("transform foundation integration", () => {
const { store, brush } = await renderQuadTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
fireEvent.pointerMove(screen.getByTestId("viewport-panel-bottomRight"), {
@@ -617,7 +671,9 @@ describe("transform foundation integration", () => {
});
expect(store.getState().activeViewportPanelId).toBe("bottomRight");
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
expect(
store.getState().viewportTransientState.transformSession
).toMatchObject({
kind: "active",
operation: "translate",
sourcePanelId: "bottomRight",
@@ -632,7 +688,9 @@ describe("transform foundation integration", () => {
const { store } = await renderTransformFixtureApp();
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -644,7 +702,9 @@ describe("transform foundation integration", () => {
code: "KeyZ"
});
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
expect(
store.getState().viewportTransientState.transformSession
).toMatchObject({
kind: "active",
axisConstraint: "z",
axisConstraintSpace: "world"
@@ -655,12 +715,16 @@ describe("transform foundation integration", () => {
code: "KeyZ"
});
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
expect(
store.getState().viewportTransientState.transformSession
).toMatchObject({
kind: "active",
axisConstraint: "z",
axisConstraintSpace: "local"
});
expect(screen.getByText(/constrained move to local z\./i)).toBeInTheDocument();
expect(
screen.getByText(/constrained move to local z\./i)
).toBeInTheDocument();
});
it("keeps the persisted viewport camera state stable across transform commit, cancel, and delete", async () => {
@@ -681,10 +745,14 @@ describe("transform foundation integration", () => {
emitCameraStateChange(viewportHost, persistedCameraState);
expect(store.getState().viewportPanels.topLeft.cameraState).toEqual(persistedCameraState);
expect(store.getState().viewportPanels.topLeft.cameraState).toEqual(
persistedCameraState
);
await act(async () => {
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
fireEvent.click(
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
);
});
fireEvent.keyDown(window, {
@@ -713,9 +781,13 @@ describe("transform foundation integration", () => {
});
await waitFor(() => {
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(commitCameraCallCount);
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
commitCameraCallCount
);
});
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
persistedCameraState
);
fireEvent.keyDown(window, {
key: "g",
@@ -730,9 +802,13 @@ describe("transform foundation integration", () => {
});
await waitFor(() => {
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(cancelCameraCallCount);
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
cancelCameraCallCount
);
});
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
persistedCameraState
);
vi.spyOn(window, "confirm").mockReturnValue(true);
const deleteCameraCallCount = viewportHost.setCameraState.mock.calls.length;
@@ -743,9 +819,13 @@ describe("transform foundation integration", () => {
});
await waitFor(() => {
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(deleteCameraCallCount);
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
deleteCameraCallCount
);
});
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
persistedCameraState
);
});
it("toggles viewport grid visibility through the shared viewport host path", async () => {
@@ -756,7 +836,9 @@ describe("transform foundation integration", () => {
fireEvent.click(screen.getByTestId("viewport-grid-toggle"));
await waitFor(() => {
expect(viewportHost.setGridVisible.mock.calls.length).toBeGreaterThan(initialCallCount);
expect(viewportHost.setGridVisible.mock.calls.length).toBeGreaterThan(
initialCallCount
);
});
expect(viewportHost.setGridVisible.mock.calls.at(-1)?.[0]).toBe(false);
expect(screen.getByText(/viewport grid hidden\./i)).toBeInTheDocument();

View File

@@ -1,11 +1,21 @@
import { render, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createInactiveTransformSession, type ActiveTransformSession, type TransformSessionState } from "../../src/core/transform-session";
import {
createInactiveTransformSession,
type ActiveTransformSession,
type TransformSessionState
} from "../../src/core/transform-session";
import { createEmptySceneDocument } from "../../src/document/scene-document";
import { ViewportCanvas } from "../../src/viewport-three/ViewportCanvas";
import { createDefaultViewportPanelCameraState, type ViewportPanelCameraState } from "../../src/viewport-three/viewport-layout";
import type { CreationViewportToolPreview, ViewportToolPreview } from "../../src/viewport-three/viewport-transient-state";
import {
createDefaultViewportPanelCameraState,
type ViewportPanelCameraState
} from "../../src/viewport-three/viewport-layout";
import type {
CreationViewportToolPreview,
ViewportToolPreview
} from "../../src/viewport-three/viewport-transient-state";
const { MockViewportHost, viewportHostInstances } = vi.hoisted(() => {
const viewportHostInstances: Array<{
@@ -81,7 +91,9 @@ vi.mock("../../src/viewport-three/viewport-host", () => ({
describe("ViewportCanvas", () => {
beforeEach(() => {
viewportHostInstances.length = 0;
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => ({}) as never);
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(
() => ({}) as never
);
});
afterEach(() => {
@@ -100,10 +112,18 @@ describe("ViewportCanvas", () => {
center: null
};
const onCommitCreation = vi.fn(() => true);
const onCameraStateChange = vi.fn((_cameraState: ViewportPanelCameraState) => undefined);
const onToolPreviewChange = vi.fn((_toolPreview: ViewportToolPreview) => undefined);
const onTransformSessionChange = vi.fn((_transformSession: TransformSessionState) => undefined);
const onTransformCommit = vi.fn((_transformSession: ActiveTransformSession) => undefined);
const onCameraStateChange = vi.fn(
(_cameraState: ViewportPanelCameraState) => undefined
);
const onToolPreviewChange = vi.fn(
(_toolPreview: ViewportToolPreview) => undefined
);
const onTransformSessionChange = vi.fn(
(_transformSession: TransformSessionState) => undefined
);
const onTransformCommit = vi.fn(
(_transformSession: ActiveTransformSession) => undefined
);
const onTransformCancel = vi.fn(() => undefined);
const onSelectionChange = vi.fn();
@@ -142,10 +162,13 @@ describe("ViewportCanvas", () => {
await waitFor(() => {
expect(viewportHostInstances).toHaveLength(1);
expect(viewportHostInstances[0].setCreationCommitHandler).toHaveBeenCalledTimes(1);
expect(
viewportHostInstances[0].setCreationCommitHandler
).toHaveBeenCalledTimes(1);
});
const registeredHandler = viewportHostInstances[0].setCreationCommitHandler.mock.calls[0][0] as (
const registeredHandler = viewportHostInstances[0].setCreationCommitHandler
.mock.calls[0][0] as (
toolPreview: CreationViewportToolPreview
) => boolean;
@@ -156,7 +179,9 @@ describe("ViewportCanvas", () => {
it("applies and subscribes to persisted camera state through the viewport host", async () => {
const sceneDocument = createEmptySceneDocument();
const cameraState = createDefaultViewportPanelCameraState();
const onCameraStateChange = vi.fn((_cameraState: ViewportPanelCameraState) => undefined);
const onCameraStateChange = vi.fn(
(_cameraState: ViewportPanelCameraState) => undefined
);
render(
<ViewportCanvas
@@ -193,8 +218,12 @@ describe("ViewportCanvas", () => {
await waitFor(() => {
expect(viewportHostInstances).toHaveLength(1);
expect(viewportHostInstances[0].setCameraState).toHaveBeenCalledWith(cameraState);
expect(viewportHostInstances[0].setCameraStateChangeHandler).toHaveBeenCalledTimes(1);
expect(viewportHostInstances[0].setCameraState).toHaveBeenCalledWith(
cameraState
);
expect(
viewportHostInstances[0].setCameraStateChangeHandler
).toHaveBeenCalledTimes(1);
});
});
});