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 { createCommitTransformSessionCommand } from "../../src/commands/commit-transform-session-command"; import { createTransformSession, resolveTransformTarget, supportsTransformOperation } 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"; const modelAsset = { id: "asset-model-transform-fixture", kind: "model", sourceName: "transform-fixture.glb", mimeType: "model/gltf-binary", storageKey: createProjectAssetStorageKey("asset-model-transform-fixture"), byteLength: 64, metadata: { kind: "model", format: "glb", sceneName: "Transform Fixture", nodeCount: 1, meshCount: 1, materialNames: [], textureNames: [], animationNames: [], boundingBox: { min: { x: -0.5, y: 0, z: -0.5 }, max: { x: 0.5, y: 1, z: 0.5 }, size: { x: 1, y: 1, z: 1 } }, warnings: [] } } satisfies ModelAssetRecord; describe("transform session commit commands", () => { it("requires Object mode for whitebox box transforms and does not silently promote component selections", () => { const brush = createBoxBrush({ id: "brush-main" }); const document = { ...createEmptySceneDocument(), brushes: { [brush.id]: brush } }; const faceResolved = resolveTransformTarget(document, { kind: "brushFace", brushId: brush.id, faceId: "posZ" }); const edgeResolved = resolveTransformTarget( document, { kind: "brushEdge", brushId: brush.id, edgeId: "edgeX_posY_negZ" }, "edge" ); const vertexResolved = resolveTransformTarget( document, { kind: "brushVertex", brushId: brush.id, vertexId: "posX_posY_negZ" }, "vertex" ); const faceModeBrushResolved = resolveTransformTarget( document, { kind: "brushes", ids: [brush.id] }, "face" ); const objectResolved = resolveTransformTarget(document, { kind: "brushes", ids: [brush.id] }); expect(faceResolved.target).toBeNull(); expect(faceResolved.message).toContain("Face selection"); expect(edgeResolved.target).toBeNull(); expect(edgeResolved.message).toContain("Edge transforms"); expect(vertexResolved.target).toBeNull(); expect(vertexResolved.message).toContain("Vertex transforms"); expect(faceModeBrushResolved.target).toBeNull(); expect(faceModeBrushResolved.message).toContain("Object mode"); expect(objectResolved.target).toMatchObject({ kind: "brush", brushId: brush.id, initialCenter: brush.center, initialRotationDegrees: brush.rotationDegrees, initialSize: brush.size }); expect(objectResolved.target).not.toBeNull(); expect(supportsTransformOperation(objectResolved.target as NonNullable, "translate")).toBe(true); expect(supportsTransformOperation(objectResolved.target as NonNullable, "rotate")).toBe(true); expect(supportsTransformOperation(objectResolved.target as NonNullable, "scale")).toBe(true); }); it("commits whitebox box rotate and scale transforms with undo and redo", () => { const brush = createBoxBrush({ id: "brush-transform-main", center: { x: 1.25, y: 1.5, z: -0.75 }, size: { x: 2.5, y: 2, z: 4 } }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Brush Transform Fixture" }), brushes: { [brush.id]: brush } } }); const target = resolveTransformTarget(store.getState().document, { kind: "brushes", ids: [brush.id] }).target; if (target === null || target.kind !== "brush") { throw new Error("Expected a whitebox box transform target."); } const rotateSession = createTransformSession({ source: "keyboard", sourcePanelId: "topLeft", operation: "rotate", target }); rotateSession.preview = { kind: "brush", center: { ...brush.center }, rotationDegrees: { x: 0, y: 37.5, z: 12.5 }, size: { ...brush.size } }; store.executeCommand(createCommitTransformSessionCommand(store.getState().document, rotateSession)); 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", ids: [brush.id] }).target; if (scaleTarget === null || scaleTarget.kind !== "brush") { throw new Error("Expected a whitebox box transform target after rotation."); } const scaleSession = createTransformSession({ source: "keyboard", sourcePanelId: "topLeft", operation: "scale", target: scaleTarget }); scaleSession.preview = { kind: "brush", center: { ...brush.center }, rotationDegrees: { x: 0, y: 37.5, z: 12.5 }, size: { x: 3.25, y: 1.75, z: 5.5 } }; store.executeCommand(createCommitTransformSessionCommand(store.getState().document, scaleSession)); expect(store.getState().document.brushes[brush.id]).toMatchObject({ rotationDegrees: { x: 0, y: 37.5, z: 12.5 }, size: { x: 3.25, y: 1.75, z: 5.5 } }); expect(store.undo()).toBe(true); expect(store.getState().document.brushes[brush.id]).toMatchObject({ rotationDegrees: { x: 0, y: 37.5, z: 12.5 }, size: { x: 2.5, y: 2, z: 4 } }); expect(store.undo()).toBe(true); expect(store.getState().document.brushes[brush.id]).toEqual(brush); expect(store.redo()).toBe(true); expect(store.redo()).toBe(true); expect(store.getState().document.brushes[brush.id]).toMatchObject({ rotationDegrees: { x: 0, y: 37.5, z: 12.5 }, size: { x: 3.25, y: 1.75, z: 5.5 } }); }); it("commits a model instance translate/rotate/scale transform with undo and redo", () => { const modelInstance = createModelInstance({ id: "model-instance-main", assetId: modelAsset.id, position: { x: 0, y: 1, z: 0 }, rotationDegrees: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Transform Fixture" }), assets: { [modelAsset.id]: modelAsset }, modelInstances: { [modelInstance.id]: modelInstance } } }); const target = resolveTransformTarget(store.getState().document, { kind: "modelInstances", ids: [modelInstance.id] }).target; if (target === null || target.kind !== "modelInstance") { throw new Error("Expected a model instance transform target."); } const session = createTransformSession({ source: "keyboard", sourcePanelId: "topLeft", operation: "scale", target }); session.preview = { kind: "modelInstance", position: { x: 4, y: 1, z: -2 }, rotationDegrees: { x: 0, y: 90, z: 0 }, scale: { x: 1.5, y: 2, z: 1.5 } }; store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session)); expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({ position: { x: 4, y: 1, z: -2 }, rotationDegrees: { x: 0, y: 90, z: 0 }, scale: { x: 1.5, y: 2, z: 1.5 } }); expect(store.undo()).toBe(true); expect(store.getState().document.modelInstances[modelInstance.id]).toEqual(modelInstance); expect(store.redo()).toBe(true); expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({ position: { x: 4, y: 1, z: -2 }, rotationDegrees: { x: 0, y: 90, z: 0 }, scale: { x: 1.5, y: 2, z: 1.5 } }); }); it("commits a rotatable entity transform with undo and redo", () => { const playerStart = createPlayerStartEntity({ id: "entity-player-start-main", position: { x: 0, y: 0, z: 0 }, yawDegrees: 0 }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Entity Transform Fixture" }), entities: { [playerStart.id]: playerStart } } }); const target = resolveTransformTarget(store.getState().document, { kind: "entities", ids: [playerStart.id] }).target; if (target === null || target.kind !== "entity") { throw new Error("Expected an entity transform target."); } const session = createTransformSession({ source: "keyboard", sourcePanelId: "topLeft", operation: "rotate", target }); session.preview = { kind: "entity", position: { x: 6, y: 0, z: -4 }, rotation: { kind: "yaw", yawDegrees: 90 } }; store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session)); expect(store.getState().document.entities[playerStart.id]).toMatchObject({ position: { x: 6, y: 0, z: -4 }, yawDegrees: 90 }); expect(store.undo()).toBe(true); expect(store.getState().document.entities[playerStart.id]).toEqual(playerStart); expect(store.redo()).toBe(true); expect(store.getState().document.entities[playerStart.id]).toMatchObject({ position: { x: 6, y: 0, z: -4 }, yawDegrees: 90 }); }); });