auto-git:

[change] src/app/App.tsx
 [change] src/assets/starter-environment-assets.ts
 [change] src/document/migrate-scene-document.ts
 [change] src/document/scene-document-validation.ts
 [change] src/document/scene-document.ts
 [change] src/document/world-settings.ts
 [change] src/rendering/world-background-renderer.ts
 [change] src/rendering/world-shader-sky.ts
 [change] src/runtime-three/runtime-host.ts
 [change] src/runtime-three/runtime-project-time.ts
 [change] src/shared-ui/world-background-style.ts
 [change] src/viewport-three/ViewportCanvas.tsx
 [change] src/viewport-three/viewport-host.ts
 [change] tests/domain/runtime-project-time.test.ts
 [change] tests/domain/scene-document-validation.test.ts
 [change] tests/domain/world-settings.test.ts
 [change] tests/serialization/project-document-json.test.ts
 [change] tests/serialization/scene-document-json.test.ts
 [change] tests/unit/world-shader-sky.test.ts
This commit is contained in:
2026-04-22 15:30:37 +02:00
parent a0f8f72c62
commit b2a4e1da7b
19 changed files with 1332 additions and 817 deletions

View File

@@ -183,7 +183,9 @@ describe("runtime project time", () => {
midnight.background.topColorHex
);
expect(dawn.background.topColorHex).not.toBe(noon.background.topColorHex);
expect(midnight.background.topColorHex).not.toBe(noon.background.topColorHex);
expect(midnight.background.topColorHex).not.toBe(
noon.background.topColorHex
);
expect(midnight.background.bottomColorHex).not.toBe(
noon.background.bottomColorHex
);

View File

@@ -28,7 +28,11 @@ import {
createTeleportTargetEntity,
createTriggerVolumeEntity
} from "../../src/entities/entity-instances";
import { createProjectAssetStorageKey, type AudioAssetRecord, type ModelAssetRecord } from "../../src/assets/project-assets";
import {
createProjectAssetStorageKey,
type AudioAssetRecord,
type ModelAssetRecord
} from "../../src/assets/project-assets";
import {
createControlInteractionLink,
createRunSequenceInteractionLink
@@ -199,11 +203,11 @@ describe("validateSceneDocument", () => {
}
};
const soundVolumeAction = document.interactionLinks["link-sound-volume"]
.action as typeof document.interactionLinks["link-sound-volume"]["action"] & {
.action as (typeof document.interactionLinks)["link-sound-volume"]["action"] & {
effect: { volume: number };
};
const ambientColorAction = document.interactionLinks["link-ambient-color"]
.action as typeof document.interactionLinks["link-ambient-color"]["action"] & {
.action as (typeof document.interactionLinks)["link-ambient-color"]["action"] & {
effect: { colorHex: string };
};
soundVolumeAction.effect.volume = Number.NaN;
@@ -289,24 +293,27 @@ describe("validateSceneDocument", () => {
});
const document = createEmptySceneDocument();
document.entities[npc.id] = npc;
document.sequences.sequences["sequence-guide-talk"] = createProjectSequence({
id: "sequence-guide-talk",
title: "Guide Talk",
effects: [
{
stepClass: "impulse",
type: "makeNpcTalk",
npcEntityId: npc.id,
dialogueId: "dialogue-guide"
}
]
});
document.interactionLinks["link-guide-talk"] = createRunSequenceInteractionLink({
id: "link-guide-talk",
sourceEntityId: npc.id,
trigger: "click",
sequenceId: "sequence-guide-talk"
});
document.sequences.sequences["sequence-guide-talk"] = createProjectSequence(
{
id: "sequence-guide-talk",
title: "Guide Talk",
effects: [
{
stepClass: "impulse",
type: "makeNpcTalk",
npcEntityId: npc.id,
dialogueId: "dialogue-guide"
}
]
}
);
document.interactionLinks["link-guide-talk"] =
createRunSequenceInteractionLink({
id: "link-guide-talk",
sourceEntityId: npc.id,
trigger: "click",
sequenceId: "sequence-guide-talk"
});
const validation = validateSceneDocument(document);
@@ -324,32 +331,34 @@ describe("validateSceneDocument", () => {
const document = createEmptySceneDocument();
document.entities[npc.id] = npc;
document.paths[path.id] = path;
document.sequences.sequences["sequence-guard-patrol"] = createProjectSequence({
id: "sequence-guard-patrol",
title: "Guard Patrol",
effects: [
{
stepClass: "held",
type: "controlEffect",
effect: createFollowActorPathControlEffect({
target: createActorControlTargetRef("actor-guard"),
pathId: path.id,
speed: 1,
loop: true,
progressMode: "deriveFromTime"
})
}
]
});
document.scheduler.routines["routine-guard-patrol"] = createProjectScheduleRoutine({
id: "routine-guard-patrol",
title: "Guard Patrol",
target: createActorControlTargetRef("actor-guard"),
startHour: 8,
endHour: 18,
sequenceId: "sequence-guard-patrol",
effects: []
});
document.sequences.sequences["sequence-guard-patrol"] =
createProjectSequence({
id: "sequence-guard-patrol",
title: "Guard Patrol",
effects: [
{
stepClass: "held",
type: "controlEffect",
effect: createFollowActorPathControlEffect({
target: createActorControlTargetRef("actor-guard"),
pathId: path.id,
speed: 1,
loop: true,
progressMode: "deriveFromTime"
})
}
]
});
document.scheduler.routines["routine-guard-patrol"] =
createProjectScheduleRoutine({
id: "routine-guard-patrol",
title: "Guard Patrol",
target: createActorControlTargetRef("actor-guard"),
startHour: 8,
endHour: 18,
sequenceId: "sequence-guard-patrol",
effects: []
});
const validation = validateSceneDocument(document);
@@ -444,31 +453,32 @@ describe("validateSceneDocument", () => {
document.assets[npcModelAsset.id] = npcModelAsset;
document.entities[npc.id] = npc;
document.paths[path.id] = path;
document.scheduler.routines["routine-patrol"] = createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: actorTarget,
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: actorTarget,
active: true
}),
createPlayActorAnimationControlEffect({
target: actorTarget,
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: actorTarget,
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
document.scheduler.routines["routine-patrol"] =
createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: actorTarget,
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: actorTarget,
active: true
}),
createPlayActorAnimationControlEffect({
target: actorTarget,
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: actorTarget,
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
const validation = validateSceneDocument(document);
@@ -548,7 +558,9 @@ describe("validateSceneDocument", () => {
crouch: {
speedMultiplier: 0
}
} as unknown as ReturnType<typeof createPlayerStartEntity>["movementTemplate"],
} as unknown as ReturnType<
typeof createPlayerStartEntity
>["movementTemplate"],
inputBindings: {
keyboard: {
...createPlayerStartInputBindings().keyboard,
@@ -564,7 +576,9 @@ describe("validateSceneDocument", () => {
crouch: "invalidButton",
pauseTime: "invalidButton"
}
} as unknown as ReturnType<typeof createPlayerStartEntity>["inputBindings"],
} as unknown as ReturnType<
typeof createPlayerStartEntity
>["inputBindings"],
collider: {
mode: "capsule",
eyeHeight: 3,
@@ -980,7 +994,9 @@ describe("validateSceneDocument", () => {
width: 512,
height: 256,
hasAlpha: false,
warnings: ["Background images work best as a 2:1 equirectangular panorama."]
warnings: [
"Background images work best as a 2:1 equirectangular panorama."
]
}
};
const pointLight = createPointLightEntity({
@@ -1159,7 +1175,7 @@ describe("validateSceneDocument", () => {
document.world.shaderSky.clouds.coverage = 2;
document.world.timeOfDay.dawn.background = {
mode: "shader"
} as (typeof document.world.timeOfDay.dawn.background);
} as typeof document.world.timeOfDay.dawn.background;
const validation = validateSceneDocument(document);

View File

@@ -21,7 +21,9 @@ describe("world settings helpers", () => {
expect(clone.shaderSky.clouds).not.toBe(source.shaderSky.clouds);
expect(clone.sunLight.direction).not.toBe(source.sunLight.direction);
expect(clone.advancedRendering).not.toBe(source.advancedRendering);
expect(clone.advancedRendering.shadows).not.toBe(source.advancedRendering.shadows);
expect(clone.advancedRendering.shadows).not.toBe(
source.advancedRendering.shadows
);
expect(clone.advancedRendering.whiteboxBevel).not.toBe(
source.advancedRendering.whiteboxBevel
);
@@ -59,7 +61,11 @@ describe("world settings helpers", () => {
environmentIntensity: 0.5
});
const nextImageBackground = changeWorldBackgroundMode(imageBackground, "image", "asset-background-panorama-2");
const nextImageBackground = changeWorldBackgroundMode(
imageBackground,
"image",
"asset-background-panorama-2"
);
expect(nextImageBackground).toEqual({
mode: "image",
@@ -121,7 +127,8 @@ describe("world settings helpers", () => {
expect(areWorldSettingsEqual(left, right)).toBe(false);
right.sunLight.direction.x = left.sunLight.direction.x;
right.advancedRendering.bloom.intensity = right.advancedRendering.bloom.intensity + 0.1;
right.advancedRendering.bloom.intensity =
right.advancedRendering.bloom.intensity + 0.1;
expect(areWorldSettingsEqual(left, right)).toBe(false);
});

View File

@@ -71,9 +71,9 @@ describe("project document JSON", () => {
activeScene.world.shaderSky.clouds.coverage = 0.63;
activeScene.world.shaderSky.clouds.tintHex = "#ece7df";
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual(
document
);
expect(
parseProjectDocumentJson(serializeProjectDocument(document))
).toEqual(document);
});
it("round-trips scene transition sequence effects", () => {
@@ -94,22 +94,23 @@ describe("project document JSON", () => {
});
targetScene.entities[houseEntry.id] = houseEntry;
document.scenes[targetScene.id] = targetScene;
document.sequences.sequences["sequence-enter-house"] = createProjectSequence({
id: "sequence-enter-house",
title: "Enter House",
effects: [
{
stepClass: "impulse",
type: "startSceneTransition",
targetSceneId: targetScene.id,
targetEntryEntityId: houseEntry.id
}
]
});
document.sequences.sequences["sequence-enter-house"] =
createProjectSequence({
id: "sequence-enter-house",
title: "Enter House",
effects: [
{
stepClass: "impulse",
type: "startSceneTransition",
targetSceneId: targetScene.id,
targetEntryEntityId: houseEntry.id
}
]
});
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual(
document
);
expect(
parseProjectDocumentJson(serializeProjectDocument(document))
).toEqual(document);
});
it("round-trips NPC dialogue references in project scenes", () => {
@@ -135,9 +136,9 @@ describe("project document JSON", () => {
});
document.scenes[document.activeSceneId]!.entities[npc.id] = npc;
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual(
document
);
expect(
parseProjectDocumentJson(serializeProjectDocument(document))
).toEqual(document);
});
it("migrates legacy NPC dialogue speaker fields by deriving names from actor ids instead", () => {
@@ -169,9 +170,9 @@ describe("project document JSON", () => {
legacyDocument.version = NPC_ONLY_DIALOGUES_SCENE_DOCUMENT_VERSION;
(
(
(
legacyDocument.scenes as Record<string, unknown>
)[document.activeSceneId] as {
(legacyDocument.scenes as Record<string, unknown>)[
document.activeSceneId
] as {
entities: Record<string, unknown>;
}
).entities[npc.id] as {
@@ -184,8 +185,8 @@ describe("project document JSON", () => {
const migratedDocument = parseProjectDocumentJson(
JSON.stringify(legacyDocument)
);
const migratedNpc = migratedDocument.scenes[migratedDocument.activeSceneId]!
.entities[npc.id];
const migratedNpc =
migratedDocument.scenes[migratedDocument.activeSceneId]!.entities[npc.id];
expect(migratedNpc).toEqual(
expect.objectContaining({
@@ -206,8 +207,7 @@ describe("project document JSON", () => {
})
);
expect(
"speakerName" in
(migratedNpc as typeof npc).dialogues[0]!.lines[0]!
"speakerName" in (migratedNpc as typeof npc).dialogues[0]!.lines[0]!
).toBe(false);
});
@@ -220,9 +220,10 @@ describe("project document JSON", () => {
actorId: "actor-guard"
});
document.scenes[document.activeSceneId]!.entities[npc.id] = npc;
document.scenes[document.activeSceneId]!.paths["path-guard"] = createScenePath({
id: "path-guard"
});
document.scenes[document.activeSceneId]!.paths["path-guard"] =
createScenePath({
id: "path-guard"
});
document.sequences.sequences["sequence-patrol"] = createProjectSequence({
id: "sequence-patrol",
title: "Patrol",
@@ -243,16 +244,21 @@ describe("project document JSON", () => {
const legacyDocument = JSON.parse(
serializeProjectDocument(document)
) as Record<string, unknown>;
legacyDocument.version = NPC_DIALOGUE_LINE_SPEAKER_REMOVED_SCENE_DOCUMENT_VERSION;
legacyDocument.version =
NPC_DIALOGUE_LINE_SPEAKER_REMOVED_SCENE_DOCUMENT_VERSION;
delete (
(
(
legacyDocument.sequences as {
sequences: Record<string, { effects: Array<{ effect?: Record<string, unknown> }> }>;
}
).sequences["sequence-patrol"]!.effects[0]!.effect as Record<string, unknown>
).smoothPath
);
legacyDocument.sequences as {
sequences: Record<
string,
{ effects: Array<{ effect?: Record<string, unknown> }> }
>;
}
).sequences["sequence-patrol"]!.effects[0]!.effect as Record<
string,
unknown
>
).smoothPath;
const migratedDocument = parseProjectDocumentJson(
JSON.stringify(legacyDocument)
@@ -303,8 +309,8 @@ describe("project document JSON", () => {
const migratedDocument = parseProjectDocumentJson(
JSON.stringify(legacyDocument)
);
const migratedNpc = migratedDocument.scenes[migratedDocument.activeSceneId]!
.entities[npc.id];
const migratedNpc =
migratedDocument.scenes[migratedDocument.activeSceneId]!.entities[npc.id];
expect(migratedNpc).toEqual(
expect.objectContaining({
@@ -324,62 +330,68 @@ describe("project document JSON", () => {
actorId: "actor-vendor"
});
document.scenes[document.activeSceneId]!.entities[npc.id] = createNpcEntity({
...npc,
dialogues: [
{
id: "dialogue-market",
title: "Market Greeting",
lines: [
{
id: "dialogue-line-market-1",
text: "Fresh fruit."
}
]
}
],
defaultDialogueId: "dialogue-market"
});
document.sequences.sequences["sequence-market-dialogue"] = createProjectSequence({
id: "sequence-market-dialogue",
title: "Market Greeting Sequence",
steps: [
{
stepClass: "impulse",
type: "makeNpcTalk",
npcEntityId: npc.id,
dialogueId: "dialogue-market"
}
]
});
document.sequences.sequences["sequence-vendor-open"] = createProjectSequence({
id: "sequence-vendor-open",
title: "Vendor Open",
steps: [
{
stepClass: "held",
type: "controlEffect",
effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef("actor-vendor"),
active: true
})
}
]
});
document.scheduler.routines["routine-vendor-open"] = createProjectScheduleRoutine({
id: "routine-vendor-open",
title: "Vendor Open",
target: createActorControlTargetRef("actor-vendor"),
sequenceId: "sequence-vendor-open",
effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef("actor-vendor"),
active: false
})
});
document.scenes[document.activeSceneId]!.entities["entity-trigger-sequence"] =
createTriggerVolumeEntity({
id: "entity-trigger-sequence"
document.scenes[document.activeSceneId]!.entities[npc.id] = createNpcEntity(
{
...npc,
dialogues: [
{
id: "dialogue-market",
title: "Market Greeting",
lines: [
{
id: "dialogue-line-market-1",
text: "Fresh fruit."
}
]
}
],
defaultDialogueId: "dialogue-market"
}
);
document.sequences.sequences["sequence-market-dialogue"] =
createProjectSequence({
id: "sequence-market-dialogue",
title: "Market Greeting Sequence",
steps: [
{
stepClass: "impulse",
type: "makeNpcTalk",
npcEntityId: npc.id,
dialogueId: "dialogue-market"
}
]
});
document.sequences.sequences["sequence-vendor-open"] =
createProjectSequence({
id: "sequence-vendor-open",
title: "Vendor Open",
steps: [
{
stepClass: "held",
type: "controlEffect",
effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef("actor-vendor"),
active: true
})
}
]
});
document.scheduler.routines["routine-vendor-open"] =
createProjectScheduleRoutine({
id: "routine-vendor-open",
title: "Vendor Open",
target: createActorControlTargetRef("actor-vendor"),
sequenceId: "sequence-vendor-open",
effect: createSetActorPresenceControlEffect({
target: createActorControlTargetRef("actor-vendor"),
active: false
})
});
document.scenes[document.activeSceneId]!.entities[
"entity-trigger-sequence"
] = createTriggerVolumeEntity({
id: "entity-trigger-sequence"
});
document.scenes[document.activeSceneId]!.interactionLinks["link-sequence"] =
createRunSequenceInteractionLink({
id: "link-sequence",
@@ -387,9 +399,9 @@ describe("project document JSON", () => {
sequenceId: "sequence-market-dialogue"
});
expect(parseProjectDocumentJson(serializeProjectDocument(document))).toEqual(
document
);
expect(
parseProjectDocumentJson(serializeProjectDocument(document))
).toEqual(document);
});
it("migrates v52 project documents without sequences to an empty project sequence library", () => {
@@ -621,8 +633,8 @@ describe("project document JSON", () => {
lightIntensityFactor: 0.19
}
};
document.scenes["scene-cellar"].paths["path-cellar-patrol"] = createScenePath(
{
document.scenes["scene-cellar"].paths["path-cellar-patrol"] =
createScenePath({
id: "path-cellar-patrol",
name: "Cellar Patrol",
loop: true,
@@ -652,8 +664,7 @@ describe("project document JSON", () => {
}
}
]
}
);
});
const serializedDocument = serializeProjectDocument(document);
@@ -725,31 +736,32 @@ describe("project document JSON", () => {
document.assets[npcModelAsset.id] = npcModelAsset;
document.scenes[document.activeSceneId].entities[npc.id] = npc;
document.scenes[document.activeSceneId].paths[path.id] = path;
document.scheduler.routines["routine-patrol"] = createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: createActorControlTargetRef(npc.actorId),
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: createActorControlTargetRef(npc.actorId),
active: true
}),
createPlayActorAnimationControlEffect({
target: createActorControlTargetRef(npc.actorId),
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: createActorControlTargetRef(npc.actorId),
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
document.scheduler.routines["routine-patrol"] =
createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: createActorControlTargetRef(npc.actorId),
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: createActorControlTargetRef(npc.actorId),
active: true
}),
createPlayActorAnimationControlEffect({
target: createActorControlTargetRef(npc.actorId),
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: createActorControlTargetRef(npc.actorId),
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
expect(
parseProjectDocumentJson(serializeProjectDocument(document))
@@ -765,7 +777,8 @@ describe("project document JSON", () => {
intensity: 1.25
});
document.scenes[document.activeSceneId].entities[pointLight.id] = pointLight;
document.scenes[document.activeSceneId].entities[pointLight.id] =
pointLight;
document.scheduler.routines["routine-night-light"] =
createProjectScheduleRoutine({
id: "routine-night-light",
@@ -910,7 +923,9 @@ describe("project document JSON", () => {
const legacyScene = legacyProject.scenes[legacyProject.activeSceneId];
if (legacyScene === undefined) {
throw new Error("Expected the legacy project to contain an active scene.");
throw new Error(
"Expected the legacy project to contain an active scene."
);
}
const migratedDocument = parseProjectDocumentJson(
@@ -975,9 +990,7 @@ describe("project document JSON", () => {
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world.timeOfDay
.night.background
).toEqual(
createDefaultWorldSettings().timeOfDay.night.background
);
).toEqual(createDefaultWorldSettings().timeOfDay.night.background);
expect(
migratedDocument.scenes[migratedDocument.activeSceneId]?.world
.projectTimeLightingEnabled
@@ -1044,16 +1057,16 @@ describe("project document JSON", () => {
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"
}
);
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 entities", () => {
@@ -1114,7 +1127,9 @@ describe("project document JSON", () => {
})
);
expect(migratedDocument.scenes["scene-main"]?.entities[legacyNpc.id]).toEqual(
expect(
migratedDocument.scenes["scene-main"]?.entities[legacyNpc.id]
).toEqual(
createNpcEntity({
...legacyNpc,
presence: {
@@ -1154,21 +1169,22 @@ describe("project document JSON", () => {
name: "House"
});
document.scenes[targetScene.id] = targetScene;
document.sequences.sequences["sequence-enter-house"] = createProjectSequence({
id: "sequence-enter-house",
title: "Enter House",
effects: [
{
stepClass: "impulse",
type: "startSceneTransition",
targetSceneId: targetScene.id,
targetEntryEntityId: "missing-entry"
}
]
});
document.sequences.sequences["sequence-enter-house"] =
createProjectSequence({
id: "sequence-enter-house",
title: "Enter House",
effects: [
{
stepClass: "impulse",
type: "startSceneTransition",
targetSceneId: targetScene.id,
targetEntryEntityId: "missing-entry"
}
]
});
expect(() =>
parseProjectDocumentJson(JSON.stringify(document))
).toThrow("target entry");
expect(() => parseProjectDocumentJson(JSON.stringify(document))).toThrow(
"target entry"
);
});
});

View File

@@ -6,7 +6,10 @@ import {
createPlayActorAnimationControlEffect,
createSetActorPresenceControlEffect
} from "../../src/controls/control-surface";
import { createBoxBrush, deriveBoxBrushSizeFromGeometry } from "../../src/document/brushes";
import {
createBoxBrush,
deriveBoxBrushSizeFromGeometry
} from "../../src/document/brushes";
import { createScenePath } from "../../src/document/paths";
import { createDefaultProjectTimeSettings } from "../../src/document/project-time-settings";
import { createTerrain } from "../../src/document/terrains";
@@ -69,8 +72,16 @@ import {
} from "../../src/interactions/interaction-links";
import { STARTER_MATERIAL_LIBRARY } from "../../src/materials/starter-material-library";
import { createModelInstance } from "../../src/assets/model-instances";
import { createProjectAssetStorageKey, type AudioAssetRecord, type ImageAssetRecord, type ModelAssetRecord } from "../../src/assets/project-assets";
import { parseSceneDocumentJson, serializeSceneDocument } from "../../src/serialization/scene-document-json";
import {
createProjectAssetStorageKey,
type AudioAssetRecord,
type ImageAssetRecord,
type ModelAssetRecord
} from "../../src/assets/project-assets";
import {
parseSceneDocumentJson,
serializeSceneDocument
} from "../../src/serialization/scene-document-json";
describe("scene document JSON", () => {
it("round-trips the current empty schema", () => {
@@ -125,7 +136,9 @@ describe("scene document JSON", () => {
);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.world.background).toEqual(document.world.background);
expect(migratedDocument.world.background).toEqual(
document.world.background
);
expect(migratedDocument.world.shaderSky.dayTopColorHex).toBe("#335577");
expect(migratedDocument.world.shaderSky.dayBottomColorHex).toBe("#aaccee");
});
@@ -238,9 +251,9 @@ describe("scene document JSON", () => {
})
});
expect(
parseSceneDocumentJson(serializeSceneDocument(document))
).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips actor scheduler animation and follow-path routines in the scene document schema", () => {
@@ -277,35 +290,36 @@ describe("scene document JSON", () => {
document.assets[npcModelAsset.id] = npcModelAsset;
document.entities[npc.id] = npc;
document.paths[path.id] = path;
document.scheduler.routines["routine-patrol"] = createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: actorTarget,
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: actorTarget,
active: true
}),
createPlayActorAnimationControlEffect({
target: actorTarget,
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: actorTarget,
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
document.scheduler.routines["routine-patrol"] =
createProjectScheduleRoutine({
id: "routine-patrol",
title: "Patrolling",
target: actorTarget,
startHour: 9,
endHour: 13,
effects: [
createSetActorPresenceControlEffect({
target: actorTarget,
active: true
}),
createPlayActorAnimationControlEffect({
target: actorTarget,
clipName: "Walk",
loop: true
}),
createFollowActorPathControlEffect({
target: actorTarget,
pathId: path.id,
speed: 2,
loop: false,
progressMode: "deriveFromTime"
})
]
});
expect(
parseSceneDocumentJson(serializeSceneDocument(document))
).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips a document containing a canonical box brush", () => {
@@ -331,7 +345,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips floating-point whitebox box transforms without accidental snapping", () => {
@@ -361,7 +377,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips per-face material and UV state", () => {
@@ -401,7 +419,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips whitebox box volume settings", () => {
@@ -450,7 +470,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("migrates pre-light-volume documents to the current schema version", () => {
@@ -512,11 +534,15 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips authored world environment settings", () => {
const document = createEmptySceneDocument({ name: "World Environment Scene" });
const document = createEmptySceneDocument({
name: "World Environment Scene"
});
document.world.background = {
mode: "verticalGradient",
topColorHex: "#6a87ab",
@@ -536,11 +562,15 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips authored advanced rendering settings", () => {
const document = createEmptySceneDocument({ name: "Advanced Rendering Scene" });
const document = createEmptySceneDocument({
name: "Advanced Rendering Scene"
});
document.world.advancedRendering = {
enabled: true,
shadows: {
@@ -581,7 +611,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("migrates v32 scene documents without whitebox bevel settings to defaults", () => {
@@ -627,7 +659,9 @@ describe("scene document JSON", () => {
waterPath: "quality"
}
},
materials: STARTER_MATERIAL_LIBRARY.reduce<Record<string, (typeof STARTER_MATERIAL_LIBRARY)[number]>>((registry, material) => {
materials: STARTER_MATERIAL_LIBRARY.reduce<
Record<string, (typeof STARTER_MATERIAL_LIBRARY)[number]>
>((registry, material) => {
registry[material.id] = material;
return registry;
}, {}),
@@ -652,7 +686,9 @@ describe("scene document JSON", () => {
interactionLinks: {}
} as unknown);
expect(migratedDocument.world.advancedRendering.waterReflectionMode).toBe("none");
expect(migratedDocument.world.advancedRendering.waterReflectionMode).toBe(
"none"
);
expect(migratedDocument.brushes["brush-water-legacy"]?.volume).toEqual({
mode: "water",
water: expect.objectContaining({
@@ -663,8 +699,11 @@ describe("scene document JSON", () => {
});
it("migrates legacy documents without advanced rendering settings to defaults", () => {
const emptyScene = createEmptySceneDocument({ name: "Legacy Advanced Rendering Scene" });
const { advancedRendering: _advancedRendering, ...legacyWorld } = emptyScene.world;
const emptyScene = createEmptySceneDocument({
name: "Legacy Advanced Rendering Scene"
});
const { advancedRendering: _advancedRendering, ...legacyWorld } =
emptyScene.world;
const migratedDocument = migrateSceneDocument({
version: SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION,
@@ -680,7 +719,9 @@ describe("scene document JSON", () => {
});
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.world.advancedRendering).toEqual(emptyScene.world.advancedRendering);
expect(migratedDocument.world.advancedRendering).toEqual(
emptyScene.world.advancedRendering
);
});
it("round-trips authored local lights and an image background asset", () => {
@@ -743,7 +784,9 @@ describe("scene document JSON", () => {
environmentIntensity: 0.75
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips a document containing an authored PlayerStart entity", () => {
@@ -823,7 +866,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("migrates version 14 documents without entity names", () => {
@@ -920,7 +965,10 @@ describe("scene document JSON", () => {
});
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.modelInstances["model-instance-legacy-collider"].collision).toEqual({
expect(
migratedDocument.modelInstances["model-instance-legacy-collider"]
.collision
).toEqual({
mode: "none",
visible: false
});
@@ -1128,7 +1176,9 @@ describe("scene document JSON", () => {
id: "entity-player-start-legacy-pause-binding"
});
const legacyDocument = {
...createEmptySceneDocument({ name: "Legacy Player Pause Binding Scene" }),
...createEmptySceneDocument({
name: "Legacy Player Pause Binding Scene"
}),
version: NPC_DIALOGUE_REFERENCE_SCENE_DOCUMENT_VERSION,
entities: {
[playerStart.id]: {
@@ -1331,10 +1381,8 @@ describe("scene document JSON", () => {
kind: "responsive"
}
});
const {
directionOnly: _directionOnly,
...legacyJump
} = playerStart.movementTemplate.jump;
const { directionOnly: _directionOnly, ...legacyJump } =
playerStart.movementTemplate.jump;
const legacyDocument = {
...createEmptySceneDocument({
name: "Legacy Player Air Direction Scene"
@@ -1366,7 +1414,9 @@ describe("scene document JSON", () => {
kind: "model",
sourceName: "legacy-authored-state.glb",
mimeType: "model/gltf-binary",
storageKey: createProjectAssetStorageKey("asset-model-authored-state-legacy"),
storageKey: createProjectAssetStorageKey(
"asset-model-authored-state-legacy"
),
byteLength: 2048,
metadata: {
kind: "model",
@@ -1539,7 +1589,9 @@ describe("scene document JSON", () => {
}
};
const roundTripDocument = parseSceneDocumentJson(serializeSceneDocument(document));
const roundTripDocument = parseSceneDocumentJson(
serializeSceneDocument(document)
);
expect(roundTripDocument).toEqual(document);
expect(roundTripDocument.modelInstances).toEqual({});
@@ -1655,7 +1707,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips imported model assets and placed model instances", () => {
@@ -1725,7 +1779,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips authored model-instance collision settings", () => {
@@ -1783,7 +1839,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("round-trips authored static-simple model-instance collision settings", () => {
@@ -1792,7 +1850,9 @@ describe("scene document JSON", () => {
kind: "model",
sourceName: "collision-static-simple.glb",
mimeType: "model/gltf-binary",
storageKey: createProjectAssetStorageKey("asset-model-static-simple-collider"),
storageKey: createProjectAssetStorageKey(
"asset-model-static-simple-collider"
),
byteLength: 64,
metadata: {
kind: "model",
@@ -1832,7 +1892,9 @@ describe("scene document JSON", () => {
}
});
const document = {
...createEmptySceneDocument({ name: "Static Simple Model Collision Scene" }),
...createEmptySceneDocument({
name: "Static Simple Model Collision Scene"
}),
assets: {
[asset.id]: asset
},
@@ -1841,7 +1903,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("migrates version 29 scene documents to preserve existing model collision settings", () => {
@@ -1904,7 +1968,9 @@ describe("scene document JSON", () => {
const migratedDocument = migrateSceneDocument(legacyDocument);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.modelInstances["model-instance-v29-collider"].collision).toEqual({
expect(
migratedDocument.modelInstances["model-instance-v29-collider"].collision
).toEqual({
mode: "dynamic",
visible: true
});
@@ -1957,7 +2023,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("migrates the foundation schema to the current schema version", () => {
@@ -1977,7 +2045,9 @@ describe("scene document JSON", () => {
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));
expect(Object.keys(migratedDocument.materials)).toEqual(
STARTER_MATERIAL_LIBRARY.map((material) => material.id)
);
});
it("migrates NPC foundation entities to include default collider settings", () => {
@@ -2150,7 +2220,9 @@ describe("scene document JSON", () => {
});
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.materialId).toBe(
"starter-amber-grid"
);
expect(migratedDocument.brushes["brush-legacy"].faces.posZ.uv).toEqual({
offset: {
x: 0,
@@ -2284,10 +2356,12 @@ describe("scene document JSON", () => {
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
});
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", () => {
@@ -2395,10 +2469,12 @@ describe("scene document JSON", () => {
});
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.entities["entity-player-start-main"]).toMatchObject({
kind: "playerStart",
yawDegrees: 90
});
expect(migratedDocument.entities["entity-player-start-main"]).toMatchObject(
{
kind: "playerStart",
yawDegrees: 90
}
);
});
it("migrates slice 2.1 documents to the interaction-link schema with empty interaction links", () => {
@@ -2549,8 +2625,12 @@ describe("scene document JSON", () => {
});
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.modelInstances["mi-1"].animationClipName).toBeUndefined();
expect(migratedDocument.modelInstances["mi-1"].animationAutoplay).toBeUndefined();
expect(
migratedDocument.modelInstances["mi-1"].animationClipName
).toBeUndefined();
expect(
migratedDocument.modelInstances["mi-1"].animationAutoplay
).toBeUndefined();
});
it("migrates v12 sound emitters to the current schema version", () => {
@@ -2604,7 +2684,8 @@ describe("scene document JSON", () => {
it("migrates v13 documents without the advanced rendering block to the current schema version", () => {
const emptyScene = createEmptySceneDocument();
const { advancedRendering: _advancedRendering, ...legacyWorld } = emptyScene.world;
const { advancedRendering: _advancedRendering, ...legacyWorld } =
emptyScene.world;
const migratedDocument = migrateSceneDocument({
version: SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION,
@@ -2620,7 +2701,9 @@ describe("scene document JSON", () => {
});
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.world.advancedRendering).toEqual(emptyScene.world.advancedRendering);
expect(migratedDocument.world.advancedRendering).toEqual(
emptyScene.world.advancedRendering
);
});
it("migrates v19 whitebox boxes without volume settings to the current schema version", () => {
@@ -2658,9 +2741,15 @@ describe("scene document JSON", () => {
} as any);
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
expect(migratedDocument.brushes["brush-legacy"].volume).toEqual({ mode: "none" });
expect(migratedDocument.world.advancedRendering.fogPath).toBe("performance");
expect(migratedDocument.world.advancedRendering.waterPath).toBe("performance");
expect(migratedDocument.brushes["brush-legacy"].volume).toEqual({
mode: "none"
});
expect(migratedDocument.world.advancedRendering.fogPath).toBe(
"performance"
);
expect(migratedDocument.world.advancedRendering.waterPath).toBe(
"performance"
);
});
it("round-trips authored playAnimation and stopAnimation interaction links", () => {
@@ -2717,7 +2806,9 @@ describe("scene document JSON", () => {
}
};
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(
document
);
});
it("rejects a v12 document where a playAnimation action has an empty clipName", () => {

View File

@@ -107,9 +107,7 @@ describe("resolveWorldShaderSkyRenderState", () => {
expect(noonSky?.sky.topColorHex).toBe("#88ccff");
expect(noonSky?.sky.bottomColorHex).toBe("#dff3ff");
expect(dawnSky?.sky.topColorHex).not.toBe(noonSky?.sky.topColorHex);
expect(dawnSky?.sky.topColorHex).not.toBe(
midnightSky?.sky.topColorHex
);
expect(dawnSky?.sky.topColorHex).not.toBe(midnightSky?.sky.topColorHex);
expect(midnightSky?.stars.visibility ?? 0).toBeGreaterThan(
dawnSky?.stars.visibility ?? 0
);