import { describe, expect, it } from "vitest"; import { createModelInstance } from "../../src/assets/model-instances"; import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets"; import { createEditorStore } from "../../src/app/editor-store"; import { createDuplicateSelectionCommand } from "../../src/commands/duplicate-selection-command"; import { createBoxBrush } from "../../src/document/brushes"; import { createEmptySceneDocument } from "../../src/document/scene-document"; import { createNpcEntity, createTriggerVolumeEntity } from "../../src/entities/entity-instances"; const modelAsset = { id: "asset-model-duplicate", kind: "model", sourceName: "duplicate-fixture.gltf", mimeType: "model/gltf+json", storageKey: createProjectAssetStorageKey("asset-model-duplicate"), byteLength: 64, metadata: { kind: "model", format: "gltf", sceneName: "Duplicate Fixture", nodeCount: 1, meshCount: 1, materialNames: [], textureNames: [], animationNames: [], boundingBox: { min: { x: 0, y: 0, z: 0 }, max: { x: 1, y: 1, z: 1 }, size: { x: 1, y: 1, z: 1 } }, warnings: [] } } satisfies ModelAssetRecord; describe("duplicate selection command", () => { it("duplicates one selected whitebox solid with a fresh id and supports undo/redo", () => { const sourceBrush = createBoxBrush({ id: "brush-source", center: { x: 2, y: 1, z: -3 } }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Duplicate Brush" }), brushes: { [sourceBrush.id]: sourceBrush } } }); store.setSelection({ kind: "brushes", ids: [sourceBrush.id] }); store.executeCommand(createDuplicateSelectionCommand()); const selection = store.getState().selection; expect(selection.kind).toBe("brushes"); if (selection.kind !== "brushes") { throw new Error("Expected duplicated brush selection."); } expect(selection.ids).toHaveLength(1); expect(selection.ids[0]).not.toBe(sourceBrush.id); const duplicatedBrush = store.getState().document.brushes[selection.ids[0]]; expect(duplicatedBrush).toBeDefined(); expect(duplicatedBrush.center).toEqual(sourceBrush.center); expect(duplicatedBrush.faces).toEqual(sourceBrush.faces); expect(store.undo()).toBe(true); expect(Object.keys(store.getState().document.brushes)).toEqual([sourceBrush.id]); expect(store.redo()).toBe(true); expect(store.getState().selection).toEqual({ kind: "brushes", ids: [duplicatedBrush.id] }); expect(store.getState().document.brushes[duplicatedBrush.id]).toEqual(duplicatedBrush); }); it("duplicates one selected model instance without duplicating its asset", () => { const sourceModelInstance = createModelInstance({ id: "model-instance-source", assetId: modelAsset.id, position: { x: -4, y: 2, z: 5 }, rotationDegrees: { x: 0, y: 25, z: 0 }, scale: { x: 1.5, y: 1.5, z: 1.5 } }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Duplicate Model Instance" }), assets: { [modelAsset.id]: modelAsset }, modelInstances: { [sourceModelInstance.id]: sourceModelInstance } } }); store.setSelection({ kind: "modelInstances", ids: [sourceModelInstance.id] }); store.executeCommand(createDuplicateSelectionCommand()); const selection = store.getState().selection; expect(selection.kind).toBe("modelInstances"); if (selection.kind !== "modelInstances") { throw new Error("Expected duplicated model instance selection."); } const duplicatedModelInstanceId = selection.ids[0]; expect(duplicatedModelInstanceId).not.toBe(sourceModelInstance.id); const duplicatedModelInstance = store.getState().document.modelInstances[duplicatedModelInstanceId]; expect(duplicatedModelInstance.assetId).toBe(sourceModelInstance.assetId); expect(duplicatedModelInstance.position).toEqual(sourceModelInstance.position); expect(store.getState().document.assets[modelAsset.id]).toEqual(modelAsset); expect(Object.keys(store.getState().document.assets)).toHaveLength(1); expect(store.undo()).toBe(true); expect(store.getState().document.modelInstances[sourceModelInstance.id]).toEqual(sourceModelInstance); expect(store.getState().document.modelInstances[duplicatedModelInstanceId]).toBeUndefined(); expect(store.redo()).toBe(true); expect(store.getState().selection).toEqual({ kind: "modelInstances", ids: [duplicatedModelInstanceId] }); }); it("duplicates one selected entity with a fresh id and selects the duplicate", () => { const sourceEntity = createTriggerVolumeEntity({ id: "entity-source-trigger", position: { x: 8, y: 1, z: -2 }, size: { x: 4, y: 3, z: 2 }, triggerOnEnter: false, triggerOnExit: true }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Duplicate Entity" }), entities: { [sourceEntity.id]: sourceEntity } } }); store.setSelection({ kind: "entities", ids: [sourceEntity.id] }); store.executeCommand(createDuplicateSelectionCommand()); const selection = store.getState().selection; expect(selection.kind).toBe("entities"); if (selection.kind !== "entities") { throw new Error("Expected duplicated entity selection."); } expect(selection.ids).toHaveLength(1); expect(selection.ids[0]).not.toBe(sourceEntity.id); const duplicatedEntity = store.getState().document.entities[selection.ids[0]]; expect(duplicatedEntity).toBeDefined(); expect(duplicatedEntity.kind).toBe(sourceEntity.kind); expect(duplicatedEntity.position).toEqual(sourceEntity.position); expect(store.undo()).toBe(true); expect(store.getState().document.entities[sourceEntity.id]).toEqual(sourceEntity); expect(store.redo()).toBe(true); expect(store.getState().selection).toEqual({ kind: "entities", ids: [duplicatedEntity.id] }); }); it("duplicates NPCs with a fresh stable actor id", () => { const sourceNpc = createNpcEntity({ id: "entity-npc-source", actorId: "actor-town-merchant", position: { x: 3, y: 0, z: -6 }, yawDegrees: 45 }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Duplicate NPC" }), entities: { [sourceNpc.id]: sourceNpc } } }); store.setSelection({ kind: "entities", ids: [sourceNpc.id] }); store.executeCommand(createDuplicateSelectionCommand()); const selection = store.getState().selection; expect(selection.kind).toBe("entities"); if (selection.kind !== "entities") { throw new Error("Expected duplicated NPC selection."); } const duplicatedNpc = store.getState().document.entities[selection.ids[0]]; expect(duplicatedNpc).toMatchObject({ kind: "npc", position: sourceNpc.position, yawDegrees: sourceNpc.yawDegrees }); expect(duplicatedNpc.id).not.toBe(sourceNpc.id); if (duplicatedNpc.kind !== "npc") { throw new Error("Expected duplicated entity to stay an NPC."); } expect(duplicatedNpc.actorId).not.toBe(sourceNpc.actorId); }); it("duplicates multiple selected whitebox solids in one operation", () => { const sourceBrushA = createBoxBrush({ id: "brush-source-a", center: { x: 0, y: 1, z: 0 } }); const sourceBrushB = createBoxBrush({ id: "brush-source-b", center: { x: 6, y: 2, z: -4 } }); const store = createEditorStore({ initialDocument: { ...createEmptySceneDocument({ name: "Duplicate Multi" }), brushes: { [sourceBrushA.id]: sourceBrushA, [sourceBrushB.id]: sourceBrushB } } }); store.setSelection({ kind: "brushes", ids: [sourceBrushA.id, sourceBrushB.id] }); store.executeCommand(createDuplicateSelectionCommand()); const selection = store.getState().selection; expect(selection.kind).toBe("brushes"); if (selection.kind !== "brushes") { throw new Error("Expected duplicated multi-brush selection."); } expect(selection.ids).toHaveLength(2); expect(new Set(selection.ids).size).toBe(2); expect(selection.ids).not.toEqual([sourceBrushA.id, sourceBrushB.id]); const duplicatedBrushA = store.getState().document.brushes[selection.ids[0]]; const duplicatedBrushB = store.getState().document.brushes[selection.ids[1]]; expect(duplicatedBrushA.center).toEqual(sourceBrushA.center); expect(duplicatedBrushB.center).toEqual(sourceBrushB.center); expect(store.undo()).toBe(true); expect(Object.keys(store.getState().document.brushes).sort()).toEqual([sourceBrushA.id, sourceBrushB.id]); expect(store.redo()).toBe(true); expect(store.getState().selection).toEqual({ kind: "brushes", ids: [duplicatedBrushA.id, duplicatedBrushB.id] }); }); });