Files
webeditor3d/tests/serialization/project-document-json.test.ts

359 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION,
DEFAULT_PROJECT_NAME,
DEFAULT_SCENE_EDITOR_SNAP_STEP,
PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION,
PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION,
RUNNER_LOADING_SCREEN_SCENE_DOCUMENT_VERSION,
SCENE_DOCUMENT_VERSION,
createEmptyProjectDocument,
createEmptyProjectScene
} from "../../src/document/scene-document";
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
import { createDefaultWorldSettings } from "../../src/document/world-settings";
import {
createSceneEntryEntity,
createSceneExitEntity
} from "../../src/entities/entity-instances";
import {
parseProjectDocumentJson,
serializeProjectDocument
} from "../../src/serialization/scene-document-json";
describe("project document JSON", () => {
it("round-trips the project name and authored scene loading overlay settings", () => {
const cellarEntry = createSceneEntryEntity({
id: "entity-scene-entry-cellar-stairs",
position: {
x: 1,
y: 0,
z: -2
},
yawDegrees: 180
});
const mainExit = createSceneExitEntity({
id: "entity-scene-exit-main-hatch",
position: {
x: 0,
y: 1,
z: 3
},
targetSceneId: "scene-cellar",
targetEntryEntityId: cellarEntry.id
});
const document = {
...createEmptyProjectDocument({
name: "Castle Project",
sceneName: "Entry"
}),
activeSceneId: "scene-cellar",
scenes: {
"scene-main": createEmptyProjectScene({
id: "scene-main",
name: "Entry"
}),
"scene-cellar": createEmptyProjectScene({
id: "scene-cellar",
name: "Cellar",
loadingScreen: {
colorHex: "#233041",
headline: "Descending",
description: "Dust and echoes settle before the next room appears."
}
})
}
};
document.scenes["scene-main"].entities[mainExit.id] = mainExit;
document.scenes["scene-cellar"].entities[cellarEntry.id] = cellarEntry;
document.scenes["scene-main"].editorPreferences = {
...document.scenes["scene-main"].editorPreferences,
whiteboxSelectionMode: "face",
whiteboxSnapEnabled: false,
whiteboxSnapStep: 0.5,
viewportGridVisible: false,
viewportLayoutMode: "quad",
activeViewportPanelId: "bottomRight",
viewportQuadSplit: {
x: 0.42,
y: 0.58
},
viewportPanels: {
...document.scenes["scene-main"].editorPreferences.viewportPanels,
topLeft: {
viewMode: "front",
displayMode: "wireframe"
}
}
};
document.scenes["scene-cellar"].editorPreferences = {
...document.scenes["scene-cellar"].editorPreferences,
whiteboxSelectionMode: "vertex",
whiteboxSnapStep: 2,
viewportLayoutMode: "quad",
activeViewportPanelId: "topRight",
viewportPanels: {
...document.scenes["scene-cellar"].editorPreferences.viewportPanels,
topLeft: {
viewMode: "side",
displayMode: "authoring"
}
}
};
document.scenes["scene-cellar"].world.advancedRendering = {
...document.scenes["scene-cellar"].world.advancedRendering,
enabled: true,
whiteboxBevel: {
enabled: true,
edgeWidth: 0.16,
normalStrength: 0.85
}
};
document.scenes["scene-cellar"].world.projectTimeLightingEnabled = false;
document.assets["asset-night-sky"] = {
id: "asset-night-sky",
kind: "image",
sourceName: "night-sky.png",
mimeType: "image/png",
storageKey: "project-asset:asset-night-sky",
byteLength: 2048,
metadata: {
kind: "image",
width: 1024,
height: 512,
hasAlpha: false,
warnings: []
}
};
document.time = {
startDayNumber: 3,
startTimeOfDayHours: 18.5,
dayLengthMinutes: 16,
sunriseTimeOfDayHours: 5.5,
sunsetTimeOfDayHours: 19.75,
dawnDurationHours: 1.25,
duskDurationHours: 1.75
};
document.scenes["scene-cellar"].world.timeOfDay = {
dawn: {
skyTopColorHex: "#6680bc",
skyBottomColorHex: "#f3b07a",
ambientColorHex: "#ffe0ba",
ambientIntensityFactor: 0.78,
lightColorHex: "#ffd29d",
lightIntensityFactor: 0.82
},
dusk: {
skyTopColorHex: "#313d70",
skyBottomColorHex: "#e27b5e",
ambientColorHex: "#f0bf9f",
ambientIntensityFactor: 0.58,
lightColorHex: "#ff9d79",
lightIntensityFactor: 0.61
},
night: {
background: {
mode: "image",
assetId: "asset-night-sky",
environmentIntensity: 0.42
},
ambientColorHex: "#1a2941",
ambientIntensityFactor: 0.22,
lightColorHex: "#95b0ff",
lightIntensityFactor: 0.19
}
};
const serializedDocument = serializeProjectDocument(document);
expect(parseProjectDocumentJson(serializedDocument)).toEqual(document);
});
it("migrates pre-project-time multi-scene documents to default project time settings", () => {
const migratedDocument = parseProjectDocumentJson(
JSON.stringify({
version: AUTHORED_OBJECT_STATE_SCENE_DOCUMENT_VERSION,
name: "Legacy Project",
activeSceneId: "scene-main",
scenes: {
"scene-main": createEmptyProjectScene({
id: "scene-main",
name: "Legacy Entry"
})
},
materials: createEmptyProjectDocument().materials,
textures: {},
assets: {}
})
);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.time).toEqual(createDefaultProjectTimeSettings());
});
it("migrates legacy project time environment profiles into scene world settings", () => {
const legacyProject = createEmptyProjectDocument({
name: "Legacy Time Project",
sceneName: "Atrium"
});
const legacyScene = legacyProject.scenes[legacyProject.activeSceneId];
if (legacyScene === undefined) {
throw new Error("Expected the legacy project to contain an active scene.");
}
const migratedDocument = parseProjectDocumentJson(
JSON.stringify({
version: PROJECT_TIME_DAY_NIGHT_PROFILE_SCENE_DOCUMENT_VERSION,
name: legacyProject.name,
activeSceneId: legacyProject.activeSceneId,
scenes: {
[legacyScene.id]: {
...legacyScene,
world: {
...legacyScene.world,
projectTimeLightingEnabled: undefined
}
}
},
materials: legacyProject.materials,
textures: legacyProject.textures,
assets: legacyProject.assets,
time: {
startDayNumber: 2,
startTimeOfDayHours: 17.5,
dayLengthMinutes: 20,
sunriseTimeOfDayHours: 6.25,
sunsetTimeOfDayHours: 19.5,
dawnDurationHours: 1.25,
duskDurationHours: 1.75,
dawn: {
...createDefaultProjectTimeSettings().dawn,
ambientIntensityFactor: 0.74
},
dusk: {
...createDefaultProjectTimeSettings().dusk,
lightIntensityFactor: 0.63
},
night: {
...createDefaultProjectTimeSettings().night,
lightIntensityFactor: 0.21
}
}
})
);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.time.startDayNumber).toBe(2);
expect(migratedDocument.time.startTimeOfDayHours).toBe(17.5);
expect(migratedDocument.time.dayLengthMinutes).toBe(20);
expect(migratedDocument.time.sunriseTimeOfDayHours).toBe(6.25);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world.timeOfDay
.dawn.ambientIntensityFactor
).toBe(0.74);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world.timeOfDay
.dusk.lightIntensityFactor
).toBe(0.63);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world.timeOfDay
.night.lightIntensityFactor
).toBe(0.21);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world.timeOfDay
.night.background
).toEqual(
createDefaultWorldSettings().timeOfDay.night.background
);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world
.projectTimeLightingEnabled
).toBe(true);
});
it("migrates pre-project-name multi-scene documents to Untitled Project", () => {
const migratedDocument = parseProjectDocumentJson(
JSON.stringify({
version: PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION,
activeSceneId: "scene-main",
scenes: {
"scene-main": (() => {
const legacyScene = createEmptyProjectScene({
id: "scene-main",
name: "Legacy Entry"
});
return {
...legacyScene,
editorPreferences: undefined
};
})()
},
materials: createEmptyProjectDocument().materials,
textures: {},
assets: {}
})
);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.name).toBe(DEFAULT_PROJECT_NAME);
expect(migratedDocument.scenes["scene-main"]?.name).toBe("Legacy Entry");
expect(migratedDocument.scenes["scene-main"]?.editorPreferences).toMatchObject(
{
whiteboxSelectionMode: "object",
whiteboxSnapEnabled: true,
whiteboxSnapStep: DEFAULT_SCENE_EDITOR_SNAP_STEP,
viewportGridVisible: true,
viewportLayoutMode: "single",
activeViewportPanelId: "topLeft"
}
);
});
it("migrates v23 project documents without Scene Entry and Scene Exit entities", () => {
const legacyScene = createEmptyProjectScene({
id: "scene-main",
name: "Legacy Entry"
});
const migratedDocument = parseProjectDocumentJson(
JSON.stringify({
version: RUNNER_LOADING_SCREEN_SCENE_DOCUMENT_VERSION,
activeSceneId: "scene-main",
scenes: {
"scene-main": legacyScene
},
materials: createEmptyProjectDocument().materials,
textures: {},
assets: {}
})
);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.scenes["scene-main"]?.loadingScreen).toEqual(
legacyScene.loadingScreen
);
});
it("rejects Scene Exit targets that point at a missing Scene Entry", () => {
const document = createEmptyProjectDocument({ sceneName: "Outside" });
const targetScene = createEmptyProjectScene({
id: "scene-house",
name: "House"
});
document.scenes[targetScene.id] = targetScene;
document.scenes["scene-main"].entities["entity-scene-exit-door"] =
createSceneExitEntity({
id: "entity-scene-exit-door",
targetSceneId: targetScene.id,
targetEntryEntityId: "missing-entry"
});
expect(() =>
parseProjectDocumentJson(JSON.stringify(document))
).toThrow("target entry");
});
});