[add] playwright.config.js [add] src/app/App.js [add] src/app/editor-store.js [add] src/app/use-editor-store.js [add] src/assets/audio-assets.js [add] src/assets/gltf-model-import.js [add] src/assets/image-assets.js [add] src/assets/model-instance-labels.js [add] src/assets/model-instance-rendering.js [add] src/assets/model-instances.js [add] src/assets/project-asset-storage.js [add] src/assets/project-assets.js [add] src/commands/brush-command-helpers.js [add] src/commands/command-history.js [add] src/commands/command.js [add] src/commands/commit-transform-session-command.js [add] src/commands/create-box-brush-command.js [add] src/commands/delete-box-brush-command.js [add] src/commands/delete-entity-command.js [add] src/commands/delete-interaction-link-command.js [add] src/commands/delete-model-instance-command.js [add] src/commands/import-audio-asset-command.js [add] src/commands/import-background-image-asset-command.js [add] src/commands/import-model-asset-command.js [add] src/commands/move-box-brush-command.js [add] src/commands/resize-box-brush-command.js [add] src/commands/rotate-box-brush-command.js [add] src/commands/set-box-brush-face-material-command.js [add] src/commands/set-box-brush-face-uv-state-command.js [add] src/commands/set-box-brush-name-command.js [add] src/commands/set-box-brush-transform-command.js [add] src/commands/set-entity-name-command.js [add] src/commands/set-model-instance-name-command.js [add] src/commands/set-player-start-command.js [add] src/commands/set-scene-name-command.js [add] src/commands/set-world-settings-command.js [add] src/commands/upsert-entity-command.js [add] src/commands/upsert-interaction-link-command.js [add] src/commands/upsert-model-instance-command.js [add] src/core/ids.js [add] src/core/selection.js [add] src/core/tool-mode.js [add] src/core/transform-session.js [add] src/core/vector.js [add] src/core/whitebox-selection-feedback.js [add] src/core/whitebox-selection-mode.js [add] src/document/brushes.js [add] src/document/migrate-scene-document.js [add] src/document/scene-document-validation.js [add] src/document/scene-document.js [add] src/document/world-settings.js [add] src/entities/entity-instances.js [add] src/entities/entity-labels.js [add] src/geometry/box-brush-components.js [add] src/geometry/box-brush-mesh.js [add] src/geometry/box-brush.js [add] src/geometry/box-face-uvs.js [add] src/geometry/grid-snapping.js [add] src/geometry/model-instance-collider-debug-mesh.js [add] src/geometry/model-instance-collider-generation.js [add] src/interactions/interaction-links.js [add] src/main.js [add] src/materials/starter-material-library.js [add] src/materials/starter-material-textures.js [add] src/rendering/advanced-rendering.js [add] src/runner-web/RunnerCanvas.js [add] src/runtime-three/first-person-navigation-controller.js [add] src/runtime-three/navigation-controller.js [add] src/runtime-three/orbit-visitor-navigation-controller.js [add] src/runtime-three/player-collision.js [add] src/runtime-three/rapier-collision-world.js [add] src/runtime-three/runtime-audio-system.js [add] src/runtime-three/runtime-host.js [add] src/runtime-three/runtime-interaction-system.js [add] src/runtime-three/runtime-scene-build.js [add] src/runtime-three/runtime-scene-validation.js [add] src/serialization/local-draft-storage.js [add] src/serialization/scene-document-json.js [add] src/shared-ui/HierarchicalMenu.js [add] src/shared-ui/Panel.js [add] src/shared-ui/world-background-style.js [add] src/viewport-three/ViewportCanvas.js [add] src/viewport-three/ViewportPanel.js [add] src/viewport-three/viewport-entity-markers.js [add] src/viewport-three/viewport-focus.js [add] src/viewport-three/viewport-host.js [add] src/viewport-three/viewport-layout.js [add] src/viewport-three/viewport-transient-state.js [add] src/viewport-three/viewport-view-modes.js [add] tests/domain/box-brush-face-editing.command.test.js [add] tests/domain/build-runtime-scene.test.js [add] tests/domain/create-box-brush.command.test.js [add] tests/domain/create-empty-scene-document.test.js [add] tests/domain/editor-store.test.js [add] tests/domain/entity.command.test.js [add] tests/domain/interaction-links.validation.test.js [add] tests/domain/model-import.test.js [add] tests/domain/model-instance.command.test.js [add] tests/domain/player-start.command.test.js [add] tests/domain/rapier-collision-world.test.js [add] tests/domain/runtime-audio-system.test.js [add] tests/domain/runtime-interaction-system.test.js [add] tests/domain/runtime-scene-validation.test.js [add] tests/domain/scene-document-validation.test.js [add] tests/domain/transform-session.command.test.js [add] tests/domain/world-settings.command.test.js [add] tests/domain/world-settings.test.js [add] tests/e2e/app-smoke.e2e.js [add] tests/e2e/box-brush-authoring.e2e.js [add] tests/e2e/entities-foundation.e2e.js [add] tests/e2e/face-material-authoring.e2e.js [add] tests/e2e/first-room-workflow.e2e.js [add] tests/e2e/import-draco-model-asset.e2e.js [add] tests/e2e/import-external-model-asset.e2e.js [add] tests/e2e/import-model-asset.e2e.js [add] tests/e2e/local-lights-and-background.e2e.js [add] tests/e2e/orthographic-views.e2e.js [add] tests/e2e/runner-v1.e2e.js [add] tests/e2e/runtime-click-interaction.e2e.js [add] tests/e2e/runtime-trigger-teleport.e2e.js [add] tests/e2e/viewport-quad-layout.e2e.js [add] tests/e2e/viewport-test-helpers.js [add] tests/e2e/whitebox-component-selection.e2e.js [add] tests/e2e/world-environment.e2e.js [add] tests/geometry/box-brush-geometry.test.js [add] tests/geometry/box-face-uvs.test.js [add] tests/geometry/model-instance-collider-generation.test.js [add] tests/helpers/model-collider-fixtures.js [add] tests/serialization/local-draft-storage.test.js [add] tests/serialization/project-asset-storage.test.js [add] tests/serialization/scene-document-json.test.js [add] tests/setup/vitest.setup.js [add] tests/unit/audio-assets.test.js [add] tests/unit/entity-instances.test.js [add] tests/unit/package-scripts.test.js [add] tests/unit/transform-foundation.integration.test.js [add] tests/unit/viewport-canvas.test.js [add] tests/unit/viewport-entity-markers.test.js [add] tests/unit/viewport-focus.test.js [add] tests/unit/viewport-layout.test.js [add] tests/unit/viewport-view-modes.test.js [add] vite.config.js [add] vitest.config.js
1381 lines
50 KiB
JavaScript
1381 lines
50 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { createBoxBrush } from "../../src/document/brushes";
|
|
import { ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION, ENTITY_NAMES_SCENE_DOCUMENT_VERSION, ENTITY_SYSTEM_FOUNDATION_SCENE_DOCUMENT_VERSION, FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION, IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION, LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION, MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION, PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION, SCENE_DOCUMENT_VERSION, SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION, TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION, WORLD_ENVIRONMENT_SCENE_DOCUMENT_VERSION, createEmptySceneDocument } from "../../src/document/scene-document";
|
|
import { migrateSceneDocument } from "../../src/document/migrate-scene-document";
|
|
import { createPointLightEntity, createInteractableEntity, createPlayerStartEntity, createSoundEmitterEntity, createSpotLightEntity, createTeleportTargetEntity, createTriggerVolumeEntity } from "../../src/entities/entity-instances";
|
|
import { createPlayAnimationInteractionLink, createPlaySoundInteractionLink, createStopAnimationInteractionLink, createStopSoundInteractionLink, createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink } from "../../src/interactions/interaction-links";
|
|
import { STARTER_MATERIAL_LIBRARY } from "../../src/materials/starter-material-library";
|
|
import { createModelInstance } from "../../src/assets/model-instances";
|
|
import { createProjectAssetStorageKey } 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", () => {
|
|
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 floating-point whitebox box transforms without accidental snapping", () => {
|
|
const brush = createBoxBrush({
|
|
id: "brush-float-transform",
|
|
center: {
|
|
x: 1.25,
|
|
y: 1.5,
|
|
z: -0.875
|
|
},
|
|
rotationDegrees: {
|
|
x: 12.5,
|
|
y: 37.5,
|
|
z: -8.25
|
|
},
|
|
size: {
|
|
x: 2.5,
|
|
y: 3.25,
|
|
z: 4.75
|
|
}
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Float Transform 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 authored advanced rendering settings", () => {
|
|
const document = createEmptySceneDocument({ name: "Advanced Rendering Scene" });
|
|
document.world.advancedRendering = {
|
|
enabled: true,
|
|
shadows: {
|
|
enabled: true,
|
|
mapSize: 4096,
|
|
type: "pcf",
|
|
bias: -0.001
|
|
},
|
|
ambientOcclusion: {
|
|
enabled: true,
|
|
intensity: 1.4,
|
|
radius: 0.75,
|
|
samples: 16
|
|
},
|
|
bloom: {
|
|
enabled: true,
|
|
intensity: 1.2,
|
|
threshold: 0.9,
|
|
radius: 0.4
|
|
},
|
|
toneMapping: {
|
|
mode: "acesFilmic",
|
|
exposure: 1.25
|
|
},
|
|
depthOfField: {
|
|
enabled: true,
|
|
focusDistance: 12,
|
|
focalLength: 0.045,
|
|
bokehScale: 1.8
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
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 migratedDocument = migrateSceneDocument({
|
|
version: SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION,
|
|
name: emptyScene.name,
|
|
world: legacyWorld,
|
|
materials: emptyScene.materials,
|
|
textures: emptyScene.textures,
|
|
assets: emptyScene.assets,
|
|
brushes: emptyScene.brushes,
|
|
modelInstances: emptyScene.modelInstances,
|
|
entities: emptyScene.entities,
|
|
interactionLinks: emptyScene.interactionLinks
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.world.advancedRendering).toEqual(emptyScene.world.advancedRendering);
|
|
});
|
|
it("round-trips authored local lights and an image background asset", () => {
|
|
const imageAsset = {
|
|
id: "asset-background-panorama",
|
|
kind: "image",
|
|
sourceName: "skybox-panorama.svg",
|
|
mimeType: "image/svg+xml",
|
|
storageKey: createProjectAssetStorageKey("asset-background-panorama"),
|
|
byteLength: 2048,
|
|
metadata: {
|
|
kind: "image",
|
|
width: 512,
|
|
height: 256,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
};
|
|
const pointLight = createPointLightEntity({
|
|
id: "entity-point-light-main",
|
|
position: {
|
|
x: 2,
|
|
y: 3,
|
|
z: 1
|
|
},
|
|
colorHex: "#ffddaa",
|
|
intensity: 1.5,
|
|
distance: 10
|
|
});
|
|
const spotLight = createSpotLightEntity({
|
|
id: "entity-spot-light-main",
|
|
position: {
|
|
x: -2,
|
|
y: 4,
|
|
z: 0
|
|
},
|
|
direction: {
|
|
x: 0.25,
|
|
y: -1,
|
|
z: 0.15
|
|
},
|
|
colorHex: "#d6e6ff",
|
|
intensity: 2,
|
|
distance: 14,
|
|
angleDegrees: 42
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Local Light and Background Scene" }),
|
|
assets: {
|
|
[imageAsset.id]: imageAsset
|
|
},
|
|
entities: {
|
|
[pointLight.id]: pointLight,
|
|
[spotLight.id]: spotLight
|
|
}
|
|
};
|
|
document.world.background = {
|
|
mode: "image",
|
|
assetId: imageAsset.id,
|
|
environmentIntensity: 0.75
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("round-trips a document containing an authored PlayerStart entity", () => {
|
|
const playerStart = createPlayerStartEntity({
|
|
id: "entity-player-start-main",
|
|
name: "Main Spawn",
|
|
position: {
|
|
x: 4,
|
|
y: 0,
|
|
z: -2
|
|
},
|
|
yawDegrees: 135,
|
|
collider: {
|
|
mode: "box",
|
|
eyeHeight: 1.4,
|
|
capsuleRadius: 0.3,
|
|
capsuleHeight: 1.8,
|
|
boxSize: {
|
|
x: 0.8,
|
|
y: 1.6,
|
|
z: 0.7
|
|
}
|
|
}
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Player Start Scene" }),
|
|
entities: {
|
|
[playerStart.id]: playerStart
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("migrates version 14 documents without entity names", () => {
|
|
const pointLight = createPointLightEntity({
|
|
id: "entity-point-light-legacy",
|
|
position: {
|
|
x: 2,
|
|
y: 3,
|
|
z: 1
|
|
},
|
|
colorHex: "#ffeeaa",
|
|
intensity: 1.75,
|
|
distance: 9
|
|
});
|
|
const legacyDocument = {
|
|
...createEmptySceneDocument({ name: "Legacy Entity Name Scene" }),
|
|
version: 14,
|
|
entities: {
|
|
[pointLight.id]: pointLight
|
|
}
|
|
};
|
|
const migratedDocument = migrateSceneDocument(legacyDocument);
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.entities[pointLight.id]).toEqual(pointLight);
|
|
});
|
|
it("migrates version 15 model instances to include default collider settings", () => {
|
|
const asset = {
|
|
id: "asset-model-legacy-collider",
|
|
kind: "model",
|
|
sourceName: "legacy-collider.glb",
|
|
mimeType: "model/gltf-binary",
|
|
storageKey: createProjectAssetStorageKey("asset-model-legacy-collider"),
|
|
byteLength: 64,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "glb",
|
|
sceneName: "Legacy Collider Scene",
|
|
nodeCount: 1,
|
|
meshCount: 1,
|
|
materialNames: [],
|
|
textureNames: [],
|
|
animationNames: [],
|
|
boundingBox: {
|
|
min: {
|
|
x: -0.5,
|
|
y: 0,
|
|
z: -0.5
|
|
},
|
|
max: {
|
|
x: 0.5,
|
|
y: 1,
|
|
z: 0.5
|
|
},
|
|
size: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 1
|
|
}
|
|
},
|
|
warnings: []
|
|
}
|
|
};
|
|
const migratedDocument = migrateSceneDocument({
|
|
...createEmptySceneDocument({ name: "Legacy Model Collider Scene" }),
|
|
version: ENTITY_NAMES_SCENE_DOCUMENT_VERSION,
|
|
assets: {
|
|
[asset.id]: asset
|
|
},
|
|
modelInstances: {
|
|
"model-instance-legacy-collider": {
|
|
id: "model-instance-legacy-collider",
|
|
kind: "modelInstance",
|
|
assetId: asset.id,
|
|
position: {
|
|
x: 1,
|
|
y: 0,
|
|
z: -2
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 45,
|
|
z: 0
|
|
},
|
|
scale: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 1
|
|
}
|
|
}
|
|
}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.modelInstances["model-instance-legacy-collider"].collision).toEqual({
|
|
mode: "none",
|
|
visible: false
|
|
});
|
|
});
|
|
it("migrates version 17 box brushes to include default whitebox rotation", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
...createEmptySceneDocument({ name: "Legacy Whitebox Transform Scene" }),
|
|
version: PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION,
|
|
brushes: {
|
|
"brush-legacy-room": {
|
|
id: "brush-legacy-room",
|
|
kind: "box",
|
|
center: {
|
|
x: 1.25,
|
|
y: 1.5,
|
|
z: -0.75
|
|
},
|
|
size: {
|
|
x: 2.5,
|
|
y: 3.25,
|
|
z: 4.75
|
|
},
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.brushes["brush-legacy-room"]).toMatchObject({
|
|
center: {
|
|
x: 1.25,
|
|
y: 1.5,
|
|
z: -0.75
|
|
},
|
|
size: {
|
|
x: 2.5,
|
|
y: 3.25,
|
|
z: 4.75
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
}
|
|
});
|
|
});
|
|
it("migrates version 16 Player Start entities to include default collider settings", () => {
|
|
const playerStart = {
|
|
id: "entity-player-start-legacy-collider",
|
|
kind: "playerStart",
|
|
position: {
|
|
x: 1,
|
|
y: 0,
|
|
z: -3
|
|
},
|
|
yawDegrees: 45
|
|
};
|
|
const legacyDocument = {
|
|
...createEmptySceneDocument({ name: "Legacy Player Collider Scene" }),
|
|
version: IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION,
|
|
entities: {
|
|
[playerStart.id]: playerStart
|
|
}
|
|
};
|
|
const migratedDocument = migrateSceneDocument(legacyDocument);
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.entities[playerStart.id]).toEqual(createPlayerStartEntity({
|
|
...playerStart
|
|
}));
|
|
});
|
|
it("round-trips the initial typed entity registry without mixing entities into model instances", () => {
|
|
const playerStart = createPlayerStartEntity({
|
|
id: "entity-player-start-main"
|
|
});
|
|
const audioAsset = {
|
|
id: "asset-audio-main",
|
|
kind: "audio",
|
|
sourceName: "lobby-loop.ogg",
|
|
mimeType: "audio/ogg",
|
|
storageKey: createProjectAssetStorageKey("asset-audio-main"),
|
|
byteLength: 4096,
|
|
metadata: {
|
|
kind: "audio",
|
|
durationSeconds: 4.5,
|
|
channelCount: 2,
|
|
sampleRateHz: 48000,
|
|
warnings: []
|
|
}
|
|
};
|
|
const soundEmitter = createSoundEmitterEntity({
|
|
id: "entity-sound-main",
|
|
position: {
|
|
x: 1,
|
|
y: 2,
|
|
z: 3
|
|
},
|
|
audioAssetId: audioAsset.id,
|
|
volume: 0.6,
|
|
refDistance: 7,
|
|
maxDistance: 18,
|
|
autoplay: true,
|
|
loop: true
|
|
});
|
|
const triggerVolume = createTriggerVolumeEntity({
|
|
id: "entity-trigger-main",
|
|
position: {
|
|
x: 0,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 3,
|
|
z: 4
|
|
}
|
|
});
|
|
const teleportTarget = createTeleportTargetEntity({
|
|
id: "entity-teleport-main",
|
|
position: {
|
|
x: -3,
|
|
y: 0,
|
|
z: 5
|
|
},
|
|
yawDegrees: 180
|
|
});
|
|
const interactable = createInteractableEntity({
|
|
id: "entity-interactable-main",
|
|
position: {
|
|
x: 2,
|
|
y: 1,
|
|
z: -2
|
|
},
|
|
radius: 1.25,
|
|
prompt: "Open Door",
|
|
enabled: true
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Typed Entity Scene" }),
|
|
assets: {
|
|
[audioAsset.id]: audioAsset
|
|
},
|
|
entities: {
|
|
[playerStart.id]: playerStart,
|
|
[soundEmitter.id]: soundEmitter,
|
|
[triggerVolume.id]: triggerVolume,
|
|
[teleportTarget.id]: teleportTarget,
|
|
[interactable.id]: interactable
|
|
}
|
|
};
|
|
const roundTripDocument = parseSceneDocumentJson(serializeSceneDocument(document));
|
|
expect(roundTripDocument).toEqual(document);
|
|
expect(roundTripDocument.modelInstances).toEqual({});
|
|
});
|
|
it("round-trips authored playSound and stopSound interaction links", () => {
|
|
const audioAsset = {
|
|
id: "asset-audio-main",
|
|
kind: "audio",
|
|
sourceName: "lobby-loop.ogg",
|
|
mimeType: "audio/ogg",
|
|
storageKey: createProjectAssetStorageKey("asset-audio-main"),
|
|
byteLength: 4096,
|
|
metadata: {
|
|
kind: "audio",
|
|
durationSeconds: 4.5,
|
|
channelCount: 2,
|
|
sampleRateHz: 48000,
|
|
warnings: []
|
|
}
|
|
};
|
|
const triggerVolume = createTriggerVolumeEntity({
|
|
id: "entity-trigger-main"
|
|
});
|
|
const soundEmitter = createSoundEmitterEntity({
|
|
id: "entity-sound-main",
|
|
audioAssetId: audioAsset.id
|
|
});
|
|
const playLink = createPlaySoundInteractionLink({
|
|
id: "link-play-sound",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "enter",
|
|
targetSoundEmitterId: soundEmitter.id
|
|
});
|
|
const stopLink = createStopSoundInteractionLink({
|
|
id: "link-stop-sound",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "exit",
|
|
targetSoundEmitterId: soundEmitter.id
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Sound Link Scene" }),
|
|
assets: {
|
|
[audioAsset.id]: audioAsset
|
|
},
|
|
entities: {
|
|
[triggerVolume.id]: triggerVolume,
|
|
[soundEmitter.id]: soundEmitter
|
|
},
|
|
interactionLinks: {
|
|
[playLink.id]: playLink,
|
|
[stopLink.id]: stopLink
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("round-trips imported model assets and placed model instances", () => {
|
|
const asset = {
|
|
id: "asset-model-triangle",
|
|
kind: "model",
|
|
sourceName: "tiny-triangle.gltf",
|
|
mimeType: "model/gltf+json",
|
|
storageKey: createProjectAssetStorageKey("asset-model-triangle"),
|
|
byteLength: 36,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "gltf",
|
|
sceneName: "Fixture Triangle Scene",
|
|
nodeCount: 2,
|
|
meshCount: 1,
|
|
materialNames: ["Fixture Material"],
|
|
textureNames: [],
|
|
animationNames: [],
|
|
boundingBox: {
|
|
min: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
max: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 1,
|
|
y: 1,
|
|
z: 0
|
|
}
|
|
},
|
|
warnings: []
|
|
}
|
|
};
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-triangle",
|
|
assetId: asset.id,
|
|
name: "Fixture Triangle",
|
|
position: {
|
|
x: 4,
|
|
y: 2,
|
|
z: -3
|
|
},
|
|
rotationDegrees: {
|
|
x: 0,
|
|
y: 45,
|
|
z: 0
|
|
},
|
|
scale: {
|
|
x: 1.5,
|
|
y: 2,
|
|
z: 1.5
|
|
}
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Imported Asset Scene" }),
|
|
assets: {
|
|
[asset.id]: asset
|
|
},
|
|
modelInstances: {
|
|
[modelInstance.id]: modelInstance
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("round-trips authored model-instance collision settings", () => {
|
|
const asset = {
|
|
id: "asset-model-collider",
|
|
kind: "model",
|
|
sourceName: "collision-test.glb",
|
|
mimeType: "model/gltf-binary",
|
|
storageKey: createProjectAssetStorageKey("asset-model-collider"),
|
|
byteLength: 64,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "glb",
|
|
sceneName: "Collision Test Scene",
|
|
nodeCount: 1,
|
|
meshCount: 1,
|
|
materialNames: [],
|
|
textureNames: [],
|
|
animationNames: [],
|
|
boundingBox: {
|
|
min: {
|
|
x: -1,
|
|
y: 0,
|
|
z: -1
|
|
},
|
|
max: {
|
|
x: 1,
|
|
y: 2,
|
|
z: 1
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 2
|
|
}
|
|
},
|
|
warnings: []
|
|
}
|
|
};
|
|
const modelInstance = createModelInstance({
|
|
id: "model-instance-collider",
|
|
assetId: asset.id,
|
|
collision: {
|
|
mode: "dynamic",
|
|
visible: true
|
|
}
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Model Collision Scene" }),
|
|
assets: {
|
|
[asset.id]: asset
|
|
},
|
|
modelInstances: {
|
|
[modelInstance.id]: modelInstance
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("round-trips canonical interaction links", () => {
|
|
const triggerVolume = createTriggerVolumeEntity({
|
|
id: "entity-trigger-main"
|
|
});
|
|
const interactable = createInteractableEntity({
|
|
id: "entity-interactable-main",
|
|
prompt: "Use Console"
|
|
});
|
|
const teleportTarget = createTeleportTargetEntity({
|
|
id: "entity-teleport-main"
|
|
});
|
|
const brush = createBoxBrush({
|
|
id: "brush-door"
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Interaction Scene" }),
|
|
brushes: {
|
|
[brush.id]: brush
|
|
},
|
|
entities: {
|
|
[triggerVolume.id]: triggerVolume,
|
|
[interactable.id]: interactable,
|
|
[teleportTarget.id]: teleportTarget
|
|
},
|
|
interactionLinks: {
|
|
"link-teleport": createTeleportPlayerInteractionLink({
|
|
id: "link-teleport",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "enter",
|
|
targetEntityId: teleportTarget.id
|
|
}),
|
|
"link-hide-door": createToggleVisibilityInteractionLink({
|
|
id: "link-hide-door",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "exit",
|
|
targetBrushId: brush.id,
|
|
visible: false
|
|
}),
|
|
"link-click-teleport": createTeleportPlayerInteractionLink({
|
|
id: "link-click-teleport",
|
|
sourceEntityId: interactable.id,
|
|
trigger: "click",
|
|
targetEntityId: teleportTarget.id
|
|
})
|
|
}
|
|
};
|
|
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 3.0 documents to the current schema version without changing empty asset collections", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION,
|
|
name: "Imported Asset Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.assets).toEqual({});
|
|
expect(migratedDocument.modelInstances).toEqual({});
|
|
});
|
|
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("migrates slice 3.2 documents with local lights and skyboxes to the current schema version", () => {
|
|
const imageAsset = {
|
|
id: "asset-background-panorama",
|
|
kind: "image",
|
|
sourceName: "skybox-panorama.svg",
|
|
mimeType: "image/svg+xml",
|
|
storageKey: createProjectAssetStorageKey("asset-background-panorama"),
|
|
byteLength: 2048,
|
|
metadata: {
|
|
kind: "image",
|
|
width: 512,
|
|
height: 256,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
};
|
|
const pointLight = createPointLightEntity({
|
|
id: "entity-point-light-main"
|
|
});
|
|
const spotLight = createSpotLightEntity({
|
|
id: "entity-spot-light-main"
|
|
});
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION,
|
|
name: "Local Light Scene",
|
|
world: {
|
|
...createEmptySceneDocument().world,
|
|
background: {
|
|
mode: "image",
|
|
assetId: imageAsset.id
|
|
}
|
|
},
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {
|
|
[imageAsset.id]: imageAsset
|
|
},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
[pointLight.id]: pointLight,
|
|
[spotLight.id]: spotLight
|
|
},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.world.background).toEqual({
|
|
mode: "image",
|
|
assetId: imageAsset.id,
|
|
environmentIntensity: 0.5
|
|
});
|
|
expect(migratedDocument.entities[pointLight.id]).toEqual(pointLight);
|
|
expect(migratedDocument.entities[spotLight.id]).toEqual(spotLight);
|
|
expect(migratedDocument.assets[imageAsset.id]).toEqual(imageAsset);
|
|
});
|
|
it("migrates slice 1.5 documents to the typed-entity schema without changing supported authored entities", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: WORLD_ENVIRONMENT_SCENE_DOCUMENT_VERSION,
|
|
name: "World Environment Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-player-start-main": {
|
|
id: "entity-player-start-main",
|
|
kind: "playerStart",
|
|
position: {
|
|
x: 2,
|
|
y: 0,
|
|
z: -1
|
|
},
|
|
yawDegrees: 90
|
|
}
|
|
},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
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", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: ENTITY_SYSTEM_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
|
name: "Entity Foundation Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-trigger-main": {
|
|
id: "entity-trigger-main",
|
|
kind: "triggerVolume",
|
|
position: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 2
|
|
},
|
|
triggerOnEnter: true,
|
|
triggerOnExit: false
|
|
}
|
|
},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.interactionLinks).toEqual({});
|
|
expect(migratedDocument.entities["entity-trigger-main"]).toMatchObject({
|
|
kind: "triggerVolume"
|
|
});
|
|
});
|
|
it("migrates slice 2.2 documents to the click-capable interaction schema without changing existing links", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
|
name: "Trigger Action Target Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-trigger-main": {
|
|
id: "entity-trigger-main",
|
|
kind: "triggerVolume",
|
|
position: {
|
|
x: 0,
|
|
y: 0,
|
|
z: 0
|
|
},
|
|
size: {
|
|
x: 2,
|
|
y: 2,
|
|
z: 2
|
|
},
|
|
triggerOnEnter: true,
|
|
triggerOnExit: false
|
|
},
|
|
"entity-teleport-main": {
|
|
id: "entity-teleport-main",
|
|
kind: "teleportTarget",
|
|
position: {
|
|
x: 4,
|
|
y: 0,
|
|
z: -2
|
|
},
|
|
yawDegrees: 90
|
|
}
|
|
},
|
|
interactionLinks: {
|
|
"link-teleport": {
|
|
id: "link-teleport",
|
|
sourceEntityId: "entity-trigger-main",
|
|
trigger: "enter",
|
|
action: {
|
|
type: "teleportPlayer",
|
|
targetEntityId: "entity-teleport-main"
|
|
}
|
|
}
|
|
}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.interactionLinks).toEqual({
|
|
"link-teleport": {
|
|
id: "link-teleport",
|
|
sourceEntityId: "entity-trigger-main",
|
|
trigger: "enter",
|
|
action: {
|
|
type: "teleportPlayer",
|
|
targetEntityId: "entity-teleport-main"
|
|
}
|
|
}
|
|
});
|
|
});
|
|
it("migrates v11 documents to v12 with animation fields defaulted to undefined on model instances", () => {
|
|
const asset = {
|
|
id: "asset-model-anim",
|
|
kind: "model",
|
|
sourceName: "animated.glb",
|
|
mimeType: "model/gltf-binary",
|
|
storageKey: createProjectAssetStorageKey("asset-model-anim"),
|
|
byteLength: 1024,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "glb",
|
|
sceneName: null,
|
|
nodeCount: 1,
|
|
meshCount: 1,
|
|
materialNames: [],
|
|
textureNames: [],
|
|
animationNames: ["Walk"],
|
|
boundingBox: null,
|
|
warnings: []
|
|
}
|
|
};
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: 11,
|
|
name: "V11 Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: { [asset.id]: asset },
|
|
brushes: {},
|
|
modelInstances: {
|
|
"mi-1": {
|
|
id: "mi-1",
|
|
kind: "modelInstance",
|
|
assetId: asset.id,
|
|
position: { x: 0, y: 0, z: 0 },
|
|
rotationDegrees: { x: 0, y: 0, z: 0 },
|
|
scale: { x: 1, y: 1, z: 1 }
|
|
}
|
|
},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.modelInstances["mi-1"].animationClipName).toBeUndefined();
|
|
expect(migratedDocument.modelInstances["mi-1"].animationAutoplay).toBeUndefined();
|
|
});
|
|
it("migrates v12 sound emitters to the current schema version", () => {
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION,
|
|
name: "Legacy Sound Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-sound-main": {
|
|
id: "entity-sound-main",
|
|
kind: "soundEmitter",
|
|
position: {
|
|
x: 1,
|
|
y: 2,
|
|
z: 3
|
|
},
|
|
radius: 9,
|
|
gain: 0.4,
|
|
autoplay: true,
|
|
loop: false
|
|
}
|
|
},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.entities["entity-sound-main"]).toEqual({
|
|
id: "entity-sound-main",
|
|
kind: "soundEmitter",
|
|
position: {
|
|
x: 1,
|
|
y: 2,
|
|
z: 3
|
|
},
|
|
audioAssetId: null,
|
|
volume: 0.4,
|
|
refDistance: 9,
|
|
maxDistance: 9,
|
|
autoplay: true,
|
|
loop: false
|
|
});
|
|
});
|
|
it("migrates v13 documents without the advanced rendering block to the current schema version", () => {
|
|
const emptyScene = createEmptySceneDocument();
|
|
const { advancedRendering: _advancedRendering, ...legacyWorld } = emptyScene.world;
|
|
const migratedDocument = migrateSceneDocument({
|
|
version: SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION,
|
|
name: "Legacy Spatial Audio Scene",
|
|
world: legacyWorld,
|
|
materials: emptyScene.materials,
|
|
textures: {},
|
|
assets: {},
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {},
|
|
interactionLinks: {}
|
|
});
|
|
expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION);
|
|
expect(migratedDocument.world.advancedRendering).toEqual(emptyScene.world.advancedRendering);
|
|
});
|
|
it("round-trips authored playAnimation and stopAnimation interaction links", () => {
|
|
const asset = {
|
|
id: "asset-model-anim",
|
|
kind: "model",
|
|
sourceName: "animated.glb",
|
|
mimeType: "model/gltf-binary",
|
|
storageKey: createProjectAssetStorageKey("asset-model-anim"),
|
|
byteLength: 1024,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "glb",
|
|
sceneName: null,
|
|
nodeCount: 1,
|
|
meshCount: 1,
|
|
materialNames: [],
|
|
textureNames: [],
|
|
animationNames: ["Walk", "Run"],
|
|
boundingBox: null,
|
|
warnings: []
|
|
}
|
|
};
|
|
const modelInstance = createModelInstance({
|
|
id: "mi-animated",
|
|
assetId: asset.id,
|
|
animationClipName: "Walk",
|
|
animationAutoplay: true
|
|
});
|
|
const triggerVolume = createTriggerVolumeEntity({
|
|
id: "entity-trigger-main"
|
|
});
|
|
const playLink = createPlayAnimationInteractionLink({
|
|
id: "link-play",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "enter",
|
|
targetModelInstanceId: modelInstance.id,
|
|
clipName: "Walk"
|
|
});
|
|
const stopLink = createStopAnimationInteractionLink({
|
|
id: "link-stop",
|
|
sourceEntityId: triggerVolume.id,
|
|
trigger: "exit",
|
|
targetModelInstanceId: modelInstance.id
|
|
});
|
|
const document = {
|
|
...createEmptySceneDocument({ name: "Animation Scene" }),
|
|
assets: { [asset.id]: asset },
|
|
modelInstances: { [modelInstance.id]: modelInstance },
|
|
entities: { [triggerVolume.id]: triggerVolume },
|
|
interactionLinks: {
|
|
[playLink.id]: playLink,
|
|
[stopLink.id]: stopLink
|
|
}
|
|
};
|
|
expect(parseSceneDocumentJson(serializeSceneDocument(document))).toEqual(document);
|
|
});
|
|
it("rejects a v12 document where a playAnimation action has an empty clipName", () => {
|
|
const asset = {
|
|
id: "asset-model-anim",
|
|
kind: "model",
|
|
sourceName: "animated.glb",
|
|
mimeType: "model/gltf-binary",
|
|
storageKey: createProjectAssetStorageKey("asset-model-anim"),
|
|
byteLength: 1024,
|
|
metadata: {
|
|
kind: "model",
|
|
format: "glb",
|
|
sceneName: null,
|
|
nodeCount: 1,
|
|
meshCount: 1,
|
|
materialNames: [],
|
|
textureNames: [],
|
|
animationNames: [],
|
|
boundingBox: null,
|
|
warnings: []
|
|
}
|
|
};
|
|
expect(() => migrateSceneDocument({
|
|
version: SCENE_DOCUMENT_VERSION,
|
|
name: "Bad Animation Scene",
|
|
world: createEmptySceneDocument().world,
|
|
materials: createEmptySceneDocument().materials,
|
|
textures: {},
|
|
assets: { [asset.id]: asset },
|
|
brushes: {},
|
|
modelInstances: {},
|
|
entities: {
|
|
"entity-trigger-main": {
|
|
id: "entity-trigger-main",
|
|
kind: "triggerVolume",
|
|
position: { x: 0, y: 0, z: 0 },
|
|
size: { x: 2, y: 2, z: 2 },
|
|
triggerOnEnter: true,
|
|
triggerOnExit: false
|
|
}
|
|
},
|
|
interactionLinks: {
|
|
"link-bad-play": {
|
|
id: "link-bad-play",
|
|
sourceEntityId: "entity-trigger-main",
|
|
trigger: "enter",
|
|
action: {
|
|
type: "playAnimation",
|
|
targetModelInstanceId: "mi-animated",
|
|
clipName: ""
|
|
}
|
|
}
|
|
}
|
|
})).toThrow();
|
|
});
|
|
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");
|
|
});
|
|
});
|