365 lines
10 KiB
TypeScript
365 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { createBoxBrush } from "../../src/document/brushes";
|
|
import { FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION, SCENE_DOCUMENT_VERSION, createEmptySceneDocument } from "../../src/document/scene-document";
|
|
import { migrateSceneDocument } from "../../src/document/migrate-scene-document";
|
|
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
|
|
import { STARTER_MATERIAL_LIBRARY } from "../../src/materials/starter-material-library";
|
|
import { parseSceneDocumentJson, serializeSceneDocument } from "../../src/serialization/scene-document-json";
|
|
|
|
describe("scene document JSON", () => {
|
|
it("round-trips the current empty schema", () => {
|
|
const document = createEmptySceneDocument({ name: "Bootstrap Scene" });
|
|
const serializedDocument = serializeSceneDocument(document);
|
|
|
|
expect(parseSceneDocumentJson(serializedDocument)).toEqual(document);
|
|
});
|
|
|
|
it("round-trips a document containing a canonical box brush", () => {
|
|
const brush = createBoxBrush({
|
|
id: "brush-box-room",
|
|
name: "Entry Room",
|
|
center: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 4,
|
|
y: 2,
|
|
z: 6
|
|
}
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Brush Scene" }),
|
|
brushes: {
|
|
[brush.id]: brush
|
|
}
|
|
};
|
|
|
|
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("round-trips authored world environment settings", () => {
|
|
const document = createEmptySceneDocument({ name: "World Environment Scene" });
|
|
document.world.background = {
|
|
mode: "verticalGradient",
|
|
topColorHex: "#6a87ab",
|
|
bottomColorHex: "#151b23"
|
|
};
|
|
document.world.ambientLight = {
|
|
colorHex: "#d4e2ff",
|
|
intensity: 0.45
|
|
};
|
|
document.world.sunLight = {
|
|
colorHex: "#ffd8a6",
|
|
intensity: 2.25,
|
|
direction: {
|
|
x: -1,
|
|
y: 0.8,
|
|
z: 0.2
|
|
}
|
|
};
|
|
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
|
|
it("round-trips a document containing an authored PlayerStart entity", () => {
|
|
const playerStart = createPlayerStartEntity({
|
|
id: "entity-player-start-main",
|
|
position: {
|
|
x: 4,
|
|
y: 0,
|
|
z: -2
|
|
},
|
|
yawDegrees: 135
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Player Start Scene" }),
|
|
entities: {
|
|
[playerStart.id]: playerStart
|
|
}
|
|
};
|
|
|
|
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: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
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(SCENE_DOCUMENT_VERSION);
|
|
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("migrates slice 1.2 face materials to the PlayerStart-capable schema", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: 3,
|
|
name: "Legacy Face Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.entities).toEqual({});
|
|
});
|
|
|
|
it("migrates runner-v1 documents to authored brush names without changing existing content", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: 4,
|
|
name: "Runner V1 Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {
|
|
"brush-room-shell": {
|
|
id: "brush-room-shell",
|
|
kind: "box",
|
|
center: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 4,
|
|
y: 2,
|
|
z: 6
|
|
},
|
|
faces: {
|
|
posX: { materialId: null, uv: createBoxBrush().faces.posX.uv },
|
|
negX: { materialId: null, uv: createBoxBrush().faces.negX.uv },
|
|
posY: { materialId: null, uv: createBoxBrush().faces.posY.uv },
|
|
negY: { materialId: null, uv: createBoxBrush().faces.negY.uv },
|
|
posZ: { materialId: null, uv: createBoxBrush().faces.posZ.uv },
|
|
negZ: { materialId: null, uv: createBoxBrush().faces.negZ.uv }
|
|
}
|
|
}
|
|
},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-player-start-main": {
|
|
id: "entity-player-start-main",
|
|
kind: "playerStart",
|
|
position: {
|
|
x: 2,
|
|
y: 0,
|
|
z: -2
|
|
},
|
|
yawDegrees: 45
|
|
}
|
|
},
|
|
interactionLinks: {}
|
|
});
|
|
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.brushes["brush-room-shell"].name).toBeUndefined();
|
|
expect(migratedDocument.entities["entity-player-start-main"]).toMatchObject({
|
|
kind: "playerStart",
|
|
yawDegrees: 45
|
|
});
|
|
});
|
|
|
|
it("migrates slice 1.4 documents to the world-environment schema without changing authored solid backgrounds", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION,
|
|
name: "First Room Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.world.background).toEqual({
|
|
mode: "solid",
|
|
colorHex: "#2f3947"
|
|
});
|
|
});
|
|
|
|
it("rejects unsupported versions", () => {
|
|
expect(() =>
|
|
migrateSceneDocument({
|
|
version: 99,
|
|
name: "Legacy",
|
|
world: {},
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
})
|
|
).toThrow("Unsupported scene document version");
|
|
});
|
|
|
|
it("rejects duplicate authored ids after migration and validation", () => {
|
|
expect(() =>
|
|
parseSceneDocumentJson(
|
|
JSON.stringify({
|
|
version: SCENE_DOCUMENT_VERSION,
|
|
name: "Duplicate Id Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {
|
|
"brush-room-shell": {
|
|
id: "shared-id",
|
|
kind: "box",
|
|
center: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 2
|
|
},
|
|
faces: {
|
|
posX: { materialId: null, uv: createBoxBrush().faces.posX.uv },
|
|
negX: { materialId: null, uv: createBoxBrush().faces.negX.uv },
|
|
posY: { materialId: null, uv: createBoxBrush().faces.posY.uv },
|
|
negY: { materialId: null, uv: createBoxBrush().faces.negY.uv },
|
|
posZ: { materialId: null, uv: createBoxBrush().faces.posZ.uv },
|
|
negZ: { materialId: null, uv: createBoxBrush().faces.negZ.uv }
|
|
}
|
|
}
|
|
},
|
|
modelInstances: {},
|
|
entities: {
|
|
"shared-id": {
|
|
id: "shared-id",
|
|
kind: "playerStart",
|
|
position: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
yawDegrees: 0
|
|
}
|
|
},
|
|
interactionLinks: {}
|
|
})
|
|
)
|
|
).toThrow("Duplicate authored id shared-id");
|
|
});
|
|
});
|