339 lines
9.7 KiB
TypeScript
339 lines
9.7 KiB
TypeScript
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)
|
|
).toContain(modelAsset.id);
|
|
|
|
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);
|
|
|
|
if (!("position" in duplicatedEntity)) {
|
|
throw new Error("Expected duplicated entity to expose a position.");
|
|
}
|
|
|
|
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]);
|
|
expect(store.getState().activeSelectionId).toBe(selection.ids[1]);
|
|
|
|
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]
|
|
});
|
|
expect(store.getState().activeSelectionId).toBe(duplicatedBrushB.id);
|
|
});
|
|
});
|