Add tests for box brush face editing and update existing tests
This commit is contained in:
99
tests/domain/box-brush-face-editing.command.test.ts
Normal file
99
tests/domain/box-brush-face-editing.command.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createEditorStore } from "../../src/app/editor-store";
|
||||
import { createCreateBoxBrushCommand } from "../../src/commands/create-box-brush-command";
|
||||
import { createSetBoxBrushFaceMaterialCommand } from "../../src/commands/set-box-brush-face-material-command";
|
||||
import { createSetBoxBrushFaceUvStateCommand } from "../../src/commands/set-box-brush-face-uv-state-command";
|
||||
|
||||
describe("box brush face editing commands", () => {
|
||||
it("applies a material to one box face and supports undo/redo", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
store.executeCommand(createCreateBoxBrushCommand());
|
||||
|
||||
const createdBrush = Object.values(store.getState().document.brushes)[0];
|
||||
|
||||
store.executeCommand(
|
||||
createSetBoxBrushFaceMaterialCommand({
|
||||
brushId: createdBrush.id,
|
||||
faceId: "posZ",
|
||||
materialId: "starter-amber-grid"
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posZ.materialId).toBe("starter-amber-grid");
|
||||
expect(store.getState().selection).toEqual({
|
||||
kind: "brushFace",
|
||||
brushId: createdBrush.id,
|
||||
faceId: "posZ"
|
||||
});
|
||||
|
||||
expect(store.undo()).toBe(true);
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posZ.materialId).toBeNull();
|
||||
|
||||
expect(store.redo()).toBe(true);
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posZ.materialId).toBe("starter-amber-grid");
|
||||
});
|
||||
|
||||
it("updates face UV state through an undoable command", () => {
|
||||
const store = createEditorStore();
|
||||
|
||||
store.executeCommand(createCreateBoxBrushCommand());
|
||||
|
||||
const createdBrush = Object.values(store.getState().document.brushes)[0];
|
||||
|
||||
store.executeCommand(
|
||||
createSetBoxBrushFaceUvStateCommand({
|
||||
brushId: createdBrush.id,
|
||||
faceId: "posY",
|
||||
uvState: {
|
||||
offset: {
|
||||
x: 0.5,
|
||||
y: -0.25
|
||||
},
|
||||
scale: {
|
||||
x: 0.25,
|
||||
y: 0.5
|
||||
},
|
||||
rotationQuarterTurns: 1,
|
||||
flipU: true,
|
||||
flipV: false
|
||||
},
|
||||
label: "Adjust top face UVs"
|
||||
})
|
||||
);
|
||||
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posY.uv).toEqual({
|
||||
offset: {
|
||||
x: 0.5,
|
||||
y: -0.25
|
||||
},
|
||||
scale: {
|
||||
x: 0.25,
|
||||
y: 0.5
|
||||
},
|
||||
rotationQuarterTurns: 1,
|
||||
flipU: true,
|
||||
flipV: false
|
||||
});
|
||||
|
||||
expect(store.undo()).toBe(true);
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posY.uv).toEqual({
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
rotationQuarterTurns: 0,
|
||||
flipU: false,
|
||||
flipV: false
|
||||
});
|
||||
|
||||
expect(store.redo()).toBe(true);
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posY.uv.rotationQuarterTurns).toBe(1);
|
||||
expect(store.getState().document.brushes[createdBrush.id].faces.posY.uv.flipU).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,22 @@ describe("box brush commands", () => {
|
||||
z: 4
|
||||
});
|
||||
expect(Object.keys(brush.faces)).toEqual(["posX", "negX", "posY", "negY", "posZ", "negZ"]);
|
||||
expect(brush.faces.posX).toEqual({
|
||||
materialId: null,
|
||||
uv: {
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
rotationQuarterTurns: 0,
|
||||
flipU: false,
|
||||
flipV: false
|
||||
}
|
||||
});
|
||||
expect(store.getState().selection).toEqual({
|
||||
kind: "brushes",
|
||||
ids: [brush.id]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SCENE_DOCUMENT_VERSION, createEmptySceneDocument } from "../../src/document/scene-document";
|
||||
import { STARTER_MATERIAL_LIBRARY } from "../../src/materials/starter-material-library";
|
||||
|
||||
describe("createEmptySceneDocument", () => {
|
||||
it("creates a versioned empty scene document", () => {
|
||||
@@ -11,5 +12,6 @@ describe("createEmptySceneDocument", () => {
|
||||
expect(document.brushes).toEqual({});
|
||||
expect(document.entities).toEqual({});
|
||||
expect(document.modelInstances).toEqual({});
|
||||
expect(Object.keys(document.materials)).toEqual(STARTER_MATERIAL_LIBRARY.map((material) => material.id));
|
||||
});
|
||||
});
|
||||
|
||||
78
tests/geometry/box-face-uvs.test.ts
Normal file
78
tests/geometry/box-face-uvs.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { BoxGeometry } from "three";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createBoxBrush } from "../../src/document/brushes";
|
||||
import { applyBoxBrushFaceUvsToGeometry, createFitToFaceBoxBrushFaceUvState, transformProjectedFaceUv } from "../../src/geometry/box-face-uvs";
|
||||
|
||||
describe("box face UV projection", () => {
|
||||
it("fit-to-face produces finite UVs normalized across the target face", () => {
|
||||
const brush = createBoxBrush({
|
||||
size: {
|
||||
x: 4,
|
||||
y: 2,
|
||||
z: 6
|
||||
}
|
||||
});
|
||||
|
||||
brush.faces.posZ.uv = createFitToFaceBoxBrushFaceUvState(brush, "posZ");
|
||||
|
||||
const geometry = new BoxGeometry(brush.size.x, brush.size.y, brush.size.z);
|
||||
applyBoxBrushFaceUvsToGeometry(geometry, brush);
|
||||
|
||||
const uvAttribute = geometry.getAttribute("uv");
|
||||
const indexAttribute = geometry.getIndex();
|
||||
const posZGroup = geometry.groups.find((group) => group.materialIndex === 4);
|
||||
|
||||
expect(indexAttribute).not.toBeNull();
|
||||
expect(posZGroup).toBeDefined();
|
||||
|
||||
const uniqueVertexIndices = new Set<number>();
|
||||
|
||||
for (let indexOffset = posZGroup!.start; indexOffset < posZGroup!.start + posZGroup!.count; indexOffset += 1) {
|
||||
uniqueVertexIndices.add(indexAttribute!.getX(indexOffset));
|
||||
}
|
||||
|
||||
const uvValues = Array.from(uniqueVertexIndices, (vertexIndex) => ({
|
||||
u: uvAttribute.getX(vertexIndex),
|
||||
v: uvAttribute.getY(vertexIndex)
|
||||
}));
|
||||
|
||||
expect(uvValues).toHaveLength(4);
|
||||
expect(uvValues.every((uv) => Number.isFinite(uv.u) && Number.isFinite(uv.v))).toBe(true);
|
||||
expect(Math.min(...uvValues.map((uv) => uv.u))).toBeCloseTo(0);
|
||||
expect(Math.max(...uvValues.map((uv) => uv.u))).toBeCloseTo(1);
|
||||
expect(Math.min(...uvValues.map((uv) => uv.v))).toBeCloseTo(0);
|
||||
expect(Math.max(...uvValues.map((uv) => uv.v))).toBeCloseTo(1);
|
||||
});
|
||||
|
||||
it("applies rotation, scale, and offset deterministically to projected UVs", () => {
|
||||
const transformedUv = transformProjectedFaceUv(
|
||||
{
|
||||
x: 4,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 4,
|
||||
y: 2
|
||||
},
|
||||
{
|
||||
offset: {
|
||||
x: 0.5,
|
||||
y: -0.25
|
||||
},
|
||||
scale: {
|
||||
x: 0.5,
|
||||
y: 1
|
||||
},
|
||||
rotationQuarterTurns: 1,
|
||||
flipU: true,
|
||||
flipV: false
|
||||
}
|
||||
);
|
||||
|
||||
expect(transformedUv).toEqual({
|
||||
x: expect.closeTo(1.5),
|
||||
y: expect.closeTo(1.75)
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { createBoxBrush } from "../../src/document/brushes";
|
||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||
import { migrateSceneDocument } from "../../src/document/migrate-scene-document";
|
||||
import { STARTER_MATERIAL_LIBRARY } from "../../src/materials/starter-material-library";
|
||||
import { parseSceneDocumentJson, serializeSceneDocument } from "../../src/serialization/scene-document-json";
|
||||
|
||||
describe("scene document JSON", () => {
|
||||
@@ -37,12 +38,51 @@ describe("scene document JSON", () => {
|
||||
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
||||
});
|
||||
|
||||
it("round-trips per-face material and UV state", () => {
|
||||
const brush = createBoxBrush({
|
||||
id: "brush-face-room",
|
||||
center: {
|
||||
x: 2,
|
||||
y: 2,
|
||||
z: -1
|
||||
},
|
||||
size: {
|
||||
x: 6,
|
||||
y: 4,
|
||||
z: 8
|
||||
}
|
||||
});
|
||||
|
||||
brush.faces.posX.materialId = "starter-amber-grid";
|
||||
brush.faces.posX.uv = {
|
||||
offset: {
|
||||
x: 0.5,
|
||||
y: -0.25
|
||||
},
|
||||
scale: {
|
||||
x: 0.25,
|
||||
y: 0.5
|
||||
},
|
||||
rotationQuarterTurns: 3,
|
||||
flipU: true,
|
||||
flipV: true
|
||||
};
|
||||
|
||||
const document = {
|
||||
...createEmptySceneDocument({ name: "Face UV Scene" }),
|
||||
brushes: {
|
||||
[brush.id]: brush
|
||||
}
|
||||
};
|
||||
|
||||
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
||||
});
|
||||
|
||||
it("migrates the foundation schema to the current schema version", () => {
|
||||
const migratedDocument = migrateSceneDocument({
|
||||
version: 1,
|
||||
name: "Foundation Scene",
|
||||
world: createEmptySceneDocument().world,
|
||||
materials: {},
|
||||
textures: {},
|
||||
assets: {},
|
||||
brushes: {},
|
||||
@@ -51,9 +91,64 @@ describe("scene document JSON", () => {
|
||||
interactionLinks: {}
|
||||
});
|
||||
|
||||
expect(migratedDocument.version).toBe(2);
|
||||
expect(migratedDocument.version).toBe(3);
|
||||
expect(migratedDocument.brushes).toEqual({});
|
||||
expect(migratedDocument.name).toBe("Foundation Scene");
|
||||
expect(Object.keys(migratedDocument.materials)).toEqual(STARTER_MATERIAL_LIBRARY.map((material) => material.id));
|
||||
});
|
||||
|
||||
it("migrates slice 1.1 box brushes to explicit per-face UV state", () => {
|
||||
const migratedDocument = migrateSceneDocument({
|
||||
version: 2,
|
||||
name: "Legacy Brush Scene",
|
||||
world: createEmptySceneDocument().world,
|
||||
materials: {},
|
||||
textures: {},
|
||||
assets: {},
|
||||
brushes: {
|
||||
"brush-legacy": {
|
||||
id: "brush-legacy",
|
||||
kind: "box",
|
||||
center: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
},
|
||||
size: {
|
||||
x: 4,
|
||||
y: 2,
|
||||
z: 6
|
||||
},
|
||||
faces: {
|
||||
posX: { materialId: null },
|
||||
negX: { materialId: null },
|
||||
posY: { materialId: null },
|
||||
negY: { materialId: null },
|
||||
posZ: { materialId: "starter-amber-grid" },
|
||||
negZ: { materialId: null }
|
||||
}
|
||||
}
|
||||
},
|
||||
modelInstances: {},
|
||||
entities: {},
|
||||
interactionLinks: {}
|
||||
});
|
||||
|
||||
expect(migratedDocument.version).toBe(3);
|
||||
expect(migratedDocument.brushes["brush-legacy"].faces.posZ.materialId).toBe("starter-amber-grid");
|
||||
expect(migratedDocument.brushes["brush-legacy"].faces.posZ.uv).toEqual({
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
rotationQuarterTurns: 0,
|
||||
flipU: false,
|
||||
flipV: false
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects unsupported versions", () => {
|
||||
@@ -62,7 +157,6 @@ describe("scene document JSON", () => {
|
||||
version: 99,
|
||||
name: "Legacy",
|
||||
world: {},
|
||||
materials: {},
|
||||
textures: {},
|
||||
assets: {},
|
||||
brushes: {},
|
||||
|
||||
Reference in New Issue
Block a user