auto-git:
[unlink] playwright.config.js [unlink] src/app/App.js [unlink] src/app/editor-store.js [unlink] src/app/use-editor-store.js [unlink] src/assets/audio-assets.js [unlink] src/assets/gltf-model-import.js [unlink] src/assets/image-assets.js [unlink] src/assets/model-instance-labels.js [unlink] src/assets/model-instance-rendering.js [unlink] src/assets/model-instances.js [unlink] src/assets/project-asset-storage.js [unlink] src/assets/project-assets.js [unlink] src/commands/brush-command-helpers.js [unlink] src/commands/command-history.js [unlink] src/commands/command.js [unlink] src/commands/commit-transform-session-command.js [unlink] src/commands/create-box-brush-command.js [unlink] src/commands/delete-box-brush-command.js [unlink] src/commands/delete-entity-command.js [unlink] src/commands/delete-interaction-link-command.js [unlink] src/commands/delete-model-instance-command.js [unlink] src/commands/duplicate-selection-command.js [unlink] src/commands/import-audio-asset-command.js [unlink] src/commands/import-background-image-asset-command.js [unlink] src/commands/import-model-asset-command.js [unlink] src/commands/move-box-brush-command.js [unlink] src/commands/resize-box-brush-command.js [unlink] src/commands/rotate-box-brush-command.js [unlink] src/commands/set-box-brush-face-material-command.js [unlink] src/commands/set-box-brush-face-uv-state-command.js [unlink] src/commands/set-box-brush-name-command.js [unlink] src/commands/set-box-brush-transform-command.js [unlink] src/commands/set-box-brush-volume-settings-command.js [unlink] src/commands/set-entity-name-command.js [unlink] src/commands/set-model-instance-name-command.js [unlink] src/commands/set-player-start-command.js [unlink] src/commands/set-scene-name-command.js [unlink] src/commands/set-world-settings-command.js [unlink] src/commands/upsert-entity-command.js [unlink] src/commands/upsert-interaction-link-command.js [unlink] src/commands/upsert-model-instance-command.js [unlink] src/core/ids.js [unlink] src/core/selection.js [unlink] src/core/tool-mode.js [unlink] src/core/transform-session.js [unlink] src/core/vector.js [unlink] src/core/whitebox-selection-feedback.js [unlink] src/core/whitebox-selection-mode.js [unlink] src/document/brushes.js [unlink] src/document/migrate-scene-document.js [unlink] src/document/scene-document-validation.js [unlink] src/document/scene-document.js [unlink] src/document/world-settings.js [unlink] src/entities/entity-instances.js [unlink] src/entities/entity-labels.js [unlink] src/geometry/box-brush-components.js [unlink] src/geometry/box-brush-mesh.js [unlink] src/geometry/box-brush.js [unlink] src/geometry/box-face-uvs.js [unlink] src/geometry/grid-snapping.js [unlink] src/geometry/model-instance-collider-debug-mesh.js [unlink] src/geometry/model-instance-collider-generation.js [unlink] src/interactions/interaction-links.js [unlink] src/main.js [unlink] src/materials/starter-material-library.js [unlink] src/materials/starter-material-textures.js [unlink] src/rendering/advanced-rendering.js [unlink] src/rendering/fog-material.js [unlink] src/rendering/planar-reflection.js [unlink] src/rendering/water-material.js [unlink] src/runner-web/RunnerCanvas.js [unlink] src/runtime-three/first-person-navigation-controller.js [unlink] src/runtime-three/navigation-controller.js [unlink] src/runtime-three/orbit-visitor-navigation-controller.js [unlink] src/runtime-three/player-collision.js [unlink] src/runtime-three/rapier-collision-world.js [unlink] src/runtime-three/runtime-audio-system.js [unlink] src/runtime-three/runtime-host.js [unlink] src/runtime-three/runtime-interaction-system.js [unlink] src/runtime-three/runtime-scene-build.js [unlink] src/runtime-three/runtime-scene-validation.js [unlink] src/runtime-three/underwater-fog.js [unlink] src/serialization/local-draft-storage.js [unlink] src/serialization/scene-document-json.js [unlink] src/shared-ui/HierarchicalMenu.js [unlink] src/shared-ui/Panel.js [unlink] src/shared-ui/world-background-style.js [unlink] src/viewport-three/ViewportCanvas.js [unlink] src/viewport-three/ViewportPanel.js [unlink] src/viewport-three/viewport-entity-markers.js [unlink] src/viewport-three/viewport-focus.js [unlink] src/viewport-three/viewport-host.js [unlink] src/viewport-three/viewport-layout.js [unlink] src/viewport-three/viewport-transient-state.js [unlink] src/viewport-three/viewport-view-modes.js [unlink] vite.config.js [unlink] vitest.config.js
This commit is contained in:
@@ -1,395 +0,0 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
export const BOX_FACE_IDS = ["posX", "negX", "posY", "negY", "posZ", "negZ"];
|
||||
export const BOX_EDGE_IDS = [
|
||||
"edgeX_negY_negZ",
|
||||
"edgeX_posY_negZ",
|
||||
"edgeX_negY_posZ",
|
||||
"edgeX_posY_posZ",
|
||||
"edgeY_negX_negZ",
|
||||
"edgeY_posX_negZ",
|
||||
"edgeY_negX_posZ",
|
||||
"edgeY_posX_posZ",
|
||||
"edgeZ_negX_negY",
|
||||
"edgeZ_posX_negY",
|
||||
"edgeZ_negX_posY",
|
||||
"edgeZ_posX_posY"
|
||||
];
|
||||
export const BOX_VERTEX_IDS = [
|
||||
"negX_negY_negZ",
|
||||
"posX_negY_negZ",
|
||||
"negX_posY_negZ",
|
||||
"posX_posY_negZ",
|
||||
"negX_negY_posZ",
|
||||
"posX_negY_posZ",
|
||||
"negX_posY_posZ",
|
||||
"posX_posY_posZ"
|
||||
];
|
||||
export const FACE_UV_ROTATION_QUARTER_TURNS = [0, 1, 2, 3];
|
||||
export const BOX_BRUSH_VOLUME_MODES = ["none", "water", "fog"];
|
||||
export const BOX_FACE_LABELS = {
|
||||
posX: "Right",
|
||||
negX: "Left",
|
||||
posY: "Top",
|
||||
negY: "Bottom",
|
||||
posZ: "Front",
|
||||
negZ: "Back"
|
||||
};
|
||||
export const BOX_EDGE_LABELS = {
|
||||
edgeX_negY_negZ: "X Edge (-Y, -Z)",
|
||||
edgeX_posY_negZ: "X Edge (+Y, -Z)",
|
||||
edgeX_negY_posZ: "X Edge (-Y, +Z)",
|
||||
edgeX_posY_posZ: "X Edge (+Y, +Z)",
|
||||
edgeY_negX_negZ: "Y Edge (-X, -Z)",
|
||||
edgeY_posX_negZ: "Y Edge (+X, -Z)",
|
||||
edgeY_negX_posZ: "Y Edge (-X, +Z)",
|
||||
edgeY_posX_posZ: "Y Edge (+X, +Z)",
|
||||
edgeZ_negX_negY: "Z Edge (-X, -Y)",
|
||||
edgeZ_posX_negY: "Z Edge (+X, -Y)",
|
||||
edgeZ_negX_posY: "Z Edge (-X, +Y)",
|
||||
edgeZ_posX_posY: "Z Edge (+X, +Y)"
|
||||
};
|
||||
export const BOX_VERTEX_LABELS = {
|
||||
negX_negY_negZ: "Vertex (-X, -Y, -Z)",
|
||||
posX_negY_negZ: "Vertex (+X, -Y, -Z)",
|
||||
negX_posY_negZ: "Vertex (-X, +Y, -Z)",
|
||||
posX_posY_negZ: "Vertex (+X, +Y, -Z)",
|
||||
negX_negY_posZ: "Vertex (-X, -Y, +Z)",
|
||||
posX_negY_posZ: "Vertex (+X, -Y, +Z)",
|
||||
negX_posY_posZ: "Vertex (-X, +Y, +Z)",
|
||||
posX_posY_posZ: "Vertex (+X, +Y, +Z)"
|
||||
};
|
||||
export const DEFAULT_BOX_BRUSH_CENTER = {
|
||||
x: 0,
|
||||
y: 1,
|
||||
z: 0
|
||||
};
|
||||
export const DEFAULT_BOX_BRUSH_SIZE = {
|
||||
x: 2,
|
||||
y: 2,
|
||||
z: 2
|
||||
};
|
||||
export const DEFAULT_BOX_BRUSH_ROTATION_DEGREES = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
};
|
||||
export const DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT = 6;
|
||||
export const MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT = 24;
|
||||
const DEFAULT_BOX_BRUSH_WATER_SETTINGS = {
|
||||
colorHex: "#4da6d9",
|
||||
surfaceOpacity: 0.55,
|
||||
waveStrength: 0.35,
|
||||
foamContactLimit: DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT,
|
||||
surfaceDisplacementEnabled: false
|
||||
};
|
||||
const DEFAULT_BOX_BRUSH_FOG_SETTINGS = {
|
||||
colorHex: "#9cb7c7",
|
||||
density: 0.08,
|
||||
padding: 0.2
|
||||
};
|
||||
export function normalizeBrushName(name) {
|
||||
if (name === undefined || name === null) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmedName = name.trim();
|
||||
return trimmedName.length === 0 ? undefined : trimmedName;
|
||||
}
|
||||
function cloneVec3(vector) {
|
||||
return {
|
||||
x: vector.x,
|
||||
y: vector.y,
|
||||
z: vector.z
|
||||
};
|
||||
}
|
||||
function cloneBrushFace(face) {
|
||||
return {
|
||||
materialId: face.materialId,
|
||||
uv: cloneFaceUvState(face.uv)
|
||||
};
|
||||
}
|
||||
function cloneBoxBrushGeometryVertex(vertex) {
|
||||
return {
|
||||
x: vertex.x,
|
||||
y: vertex.y,
|
||||
z: vertex.z
|
||||
};
|
||||
}
|
||||
export function cloneBoxBrushGeometry(geometry) {
|
||||
return {
|
||||
vertices: {
|
||||
negX_negY_negZ: cloneBoxBrushGeometryVertex(geometry.vertices.negX_negY_negZ),
|
||||
posX_negY_negZ: cloneBoxBrushGeometryVertex(geometry.vertices.posX_negY_negZ),
|
||||
negX_posY_negZ: cloneBoxBrushGeometryVertex(geometry.vertices.negX_posY_negZ),
|
||||
posX_posY_negZ: cloneBoxBrushGeometryVertex(geometry.vertices.posX_posY_negZ),
|
||||
negX_negY_posZ: cloneBoxBrushGeometryVertex(geometry.vertices.negX_negY_posZ),
|
||||
posX_negY_posZ: cloneBoxBrushGeometryVertex(geometry.vertices.posX_negY_posZ),
|
||||
negX_posY_posZ: cloneBoxBrushGeometryVertex(geometry.vertices.negX_posY_posZ),
|
||||
posX_posY_posZ: cloneBoxBrushGeometryVertex(geometry.vertices.posX_posY_posZ)
|
||||
}
|
||||
};
|
||||
}
|
||||
export function getBoxBrushGeometryLocalBounds(geometry) {
|
||||
const vertices = Object.values(geometry.vertices);
|
||||
const firstVertex = vertices[0];
|
||||
const min = { ...firstVertex };
|
||||
const max = { ...firstVertex };
|
||||
for (const vertex of vertices.slice(1)) {
|
||||
min.x = Math.min(min.x, vertex.x);
|
||||
min.y = Math.min(min.y, vertex.y);
|
||||
min.z = Math.min(min.z, vertex.z);
|
||||
max.x = Math.max(max.x, vertex.x);
|
||||
max.y = Math.max(max.y, vertex.y);
|
||||
max.z = Math.max(max.z, vertex.z);
|
||||
}
|
||||
return {
|
||||
min,
|
||||
max
|
||||
};
|
||||
}
|
||||
export function deriveBoxBrushSizeFromGeometry(geometry) {
|
||||
const bounds = getBoxBrushGeometryLocalBounds(geometry);
|
||||
return {
|
||||
x: bounds.max.x - bounds.min.x,
|
||||
y: bounds.max.y - bounds.min.y,
|
||||
z: bounds.max.z - bounds.min.z
|
||||
};
|
||||
}
|
||||
export function scaleBoxBrushGeometryToSize(geometry, size) {
|
||||
const bounds = getBoxBrushGeometryLocalBounds(geometry);
|
||||
const currentSize = deriveBoxBrushSizeFromGeometry(geometry);
|
||||
if (!hasPositiveBoxSize(currentSize) || !hasPositiveBoxSize(size)) {
|
||||
throw new Error("Box brush geometry size must remain positive on every axis.");
|
||||
}
|
||||
const center = {
|
||||
x: (bounds.min.x + bounds.max.x) * 0.5,
|
||||
y: (bounds.min.y + bounds.max.y) * 0.5,
|
||||
z: (bounds.min.z + bounds.max.z) * 0.5
|
||||
};
|
||||
const scale = {
|
||||
x: size.x / currentSize.x,
|
||||
y: size.y / currentSize.y,
|
||||
z: size.z / currentSize.z
|
||||
};
|
||||
return {
|
||||
vertices: {
|
||||
negX_negY_negZ: {
|
||||
x: center.x + (geometry.vertices.negX_negY_negZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.negX_negY_negZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.negX_negY_negZ.z - center.z) * scale.z
|
||||
},
|
||||
posX_negY_negZ: {
|
||||
x: center.x + (geometry.vertices.posX_negY_negZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.posX_negY_negZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.posX_negY_negZ.z - center.z) * scale.z
|
||||
},
|
||||
negX_posY_negZ: {
|
||||
x: center.x + (geometry.vertices.negX_posY_negZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.negX_posY_negZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.negX_posY_negZ.z - center.z) * scale.z
|
||||
},
|
||||
posX_posY_negZ: {
|
||||
x: center.x + (geometry.vertices.posX_posY_negZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.posX_posY_negZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.posX_posY_negZ.z - center.z) * scale.z
|
||||
},
|
||||
negX_negY_posZ: {
|
||||
x: center.x + (geometry.vertices.negX_negY_posZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.negX_negY_posZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.negX_negY_posZ.z - center.z) * scale.z
|
||||
},
|
||||
posX_negY_posZ: {
|
||||
x: center.x + (geometry.vertices.posX_negY_posZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.posX_negY_posZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.posX_negY_posZ.z - center.z) * scale.z
|
||||
},
|
||||
negX_posY_posZ: {
|
||||
x: center.x + (geometry.vertices.negX_posY_posZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.negX_posY_posZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.negX_posY_posZ.z - center.z) * scale.z
|
||||
},
|
||||
posX_posY_posZ: {
|
||||
x: center.x + (geometry.vertices.posX_posY_posZ.x - center.x) * scale.x,
|
||||
y: center.y + (geometry.vertices.posX_posY_posZ.y - center.y) * scale.y,
|
||||
z: center.z + (geometry.vertices.posX_posY_posZ.z - center.z) * scale.z
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
export function createDefaultBoxBrushGeometry(size = DEFAULT_BOX_BRUSH_SIZE) {
|
||||
const halfSize = {
|
||||
x: size.x * 0.5,
|
||||
y: size.y * 0.5,
|
||||
z: size.z * 0.5
|
||||
};
|
||||
return {
|
||||
vertices: {
|
||||
negX_negY_negZ: { x: -halfSize.x, y: -halfSize.y, z: -halfSize.z },
|
||||
posX_negY_negZ: { x: halfSize.x, y: -halfSize.y, z: -halfSize.z },
|
||||
negX_posY_negZ: { x: -halfSize.x, y: halfSize.y, z: -halfSize.z },
|
||||
posX_posY_negZ: { x: halfSize.x, y: halfSize.y, z: -halfSize.z },
|
||||
negX_negY_posZ: { x: -halfSize.x, y: -halfSize.y, z: halfSize.z },
|
||||
posX_negY_posZ: { x: halfSize.x, y: -halfSize.y, z: halfSize.z },
|
||||
negX_posY_posZ: { x: -halfSize.x, y: halfSize.y, z: halfSize.z },
|
||||
posX_posY_posZ: { x: halfSize.x, y: halfSize.y, z: halfSize.z }
|
||||
}
|
||||
};
|
||||
}
|
||||
export function isBoxFaceId(value) {
|
||||
return typeof value === "string" && BOX_FACE_IDS.some((faceId) => faceId === value);
|
||||
}
|
||||
export function isBoxEdgeId(value) {
|
||||
return typeof value === "string" && BOX_EDGE_IDS.some((edgeId) => edgeId === value);
|
||||
}
|
||||
export function isBoxVertexId(value) {
|
||||
return typeof value === "string" && BOX_VERTEX_IDS.some((vertexId) => vertexId === value);
|
||||
}
|
||||
export function isFaceUvRotationQuarterTurns(value) {
|
||||
return typeof value === "number" && FACE_UV_ROTATION_QUARTER_TURNS.includes(value);
|
||||
}
|
||||
export function isBoxBrushVolumeMode(value) {
|
||||
return typeof value === "string" && BOX_BRUSH_VOLUME_MODES.includes(value);
|
||||
}
|
||||
export function hasPositiveBoxSize(size) {
|
||||
return size.x > 0 && size.y > 0 && size.z > 0;
|
||||
}
|
||||
export function createDefaultFaceUvState() {
|
||||
return {
|
||||
offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1
|
||||
},
|
||||
rotationQuarterTurns: 0,
|
||||
flipU: false,
|
||||
flipV: false
|
||||
};
|
||||
}
|
||||
export function cloneFaceUvState(uv) {
|
||||
return {
|
||||
offset: {
|
||||
...uv.offset
|
||||
},
|
||||
scale: {
|
||||
...uv.scale
|
||||
},
|
||||
rotationQuarterTurns: uv.rotationQuarterTurns,
|
||||
flipU: uv.flipU,
|
||||
flipV: uv.flipV
|
||||
};
|
||||
}
|
||||
export function cloneBoxBrushFaces(faces) {
|
||||
return {
|
||||
posX: cloneBrushFace(faces.posX),
|
||||
negX: cloneBrushFace(faces.negX),
|
||||
posY: cloneBrushFace(faces.posY),
|
||||
negY: cloneBrushFace(faces.negY),
|
||||
posZ: cloneBrushFace(faces.posZ),
|
||||
negZ: cloneBrushFace(faces.negZ)
|
||||
};
|
||||
}
|
||||
export function createDefaultBoxBrushFaces() {
|
||||
return {
|
||||
posX: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
},
|
||||
negX: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
},
|
||||
posY: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
},
|
||||
negY: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
},
|
||||
posZ: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
},
|
||||
negZ: {
|
||||
materialId: null,
|
||||
uv: createDefaultFaceUvState()
|
||||
}
|
||||
};
|
||||
}
|
||||
export function createDefaultBoxBrushWaterSettings() {
|
||||
return {
|
||||
colorHex: DEFAULT_BOX_BRUSH_WATER_SETTINGS.colorHex,
|
||||
surfaceOpacity: DEFAULT_BOX_BRUSH_WATER_SETTINGS.surfaceOpacity,
|
||||
waveStrength: DEFAULT_BOX_BRUSH_WATER_SETTINGS.waveStrength,
|
||||
foamContactLimit: DEFAULT_BOX_BRUSH_WATER_SETTINGS.foamContactLimit,
|
||||
surfaceDisplacementEnabled: DEFAULT_BOX_BRUSH_WATER_SETTINGS.surfaceDisplacementEnabled
|
||||
};
|
||||
}
|
||||
export function createDefaultBoxBrushFogSettings() {
|
||||
return {
|
||||
colorHex: DEFAULT_BOX_BRUSH_FOG_SETTINGS.colorHex,
|
||||
density: DEFAULT_BOX_BRUSH_FOG_SETTINGS.density,
|
||||
padding: DEFAULT_BOX_BRUSH_FOG_SETTINGS.padding
|
||||
};
|
||||
}
|
||||
export function createDefaultBoxBrushVolumeSettings() {
|
||||
return {
|
||||
mode: "none"
|
||||
};
|
||||
}
|
||||
export function cloneBoxBrushVolumeSettings(volume) {
|
||||
switch (volume.mode) {
|
||||
case "none":
|
||||
return {
|
||||
mode: "none"
|
||||
};
|
||||
case "water":
|
||||
return {
|
||||
mode: "water",
|
||||
water: {
|
||||
colorHex: volume.water.colorHex,
|
||||
surfaceOpacity: volume.water.surfaceOpacity,
|
||||
waveStrength: volume.water.waveStrength,
|
||||
foamContactLimit: volume.water.foamContactLimit,
|
||||
surfaceDisplacementEnabled: volume.water.surfaceDisplacementEnabled
|
||||
}
|
||||
};
|
||||
case "fog":
|
||||
return {
|
||||
mode: "fog",
|
||||
fog: {
|
||||
colorHex: volume.fog.colorHex,
|
||||
density: volume.fog.density,
|
||||
padding: volume.fog.padding
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
export function createBoxBrush(overrides = {}) {
|
||||
const center = cloneVec3(overrides.center ?? DEFAULT_BOX_BRUSH_CENTER);
|
||||
const rotationDegrees = cloneVec3(overrides.rotationDegrees ?? DEFAULT_BOX_BRUSH_ROTATION_DEGREES);
|
||||
const fallbackSize = cloneVec3(overrides.size ?? DEFAULT_BOX_BRUSH_SIZE);
|
||||
const geometry = overrides.geometry === undefined ? createDefaultBoxBrushGeometry(fallbackSize) : cloneBoxBrushGeometry(overrides.geometry);
|
||||
const size = deriveBoxBrushSizeFromGeometry(geometry);
|
||||
if (!hasPositiveBoxSize(size)) {
|
||||
throw new Error("Box brush size must remain positive on every axis.");
|
||||
}
|
||||
return {
|
||||
id: overrides.id ?? createOpaqueId("brush"),
|
||||
kind: "box",
|
||||
name: normalizeBrushName(overrides.name),
|
||||
center,
|
||||
rotationDegrees,
|
||||
size,
|
||||
geometry,
|
||||
faces: overrides.faces === undefined ? createDefaultBoxBrushFaces() : cloneBoxBrushFaces(overrides.faces),
|
||||
volume: overrides.volume === undefined ? createDefaultBoxBrushVolumeSettings() : cloneBoxBrushVolumeSettings(overrides.volume),
|
||||
layerId: overrides.layerId,
|
||||
groupId: overrides.groupId
|
||||
};
|
||||
}
|
||||
export function cloneBoxBrush(brush) {
|
||||
return createBoxBrush(brush);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,690 +0,0 @@
|
||||
import {} from "../assets/project-assets";
|
||||
import { isModelInstanceCollisionMode } from "../assets/model-instances";
|
||||
import { isPlayerStartColliderMode, getPlayerStartColliderHeight } from "../entities/entity-instances";
|
||||
import {} from "../interactions/interaction-links";
|
||||
import { BOX_FACE_IDS, BOX_VERTEX_IDS, MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT, hasPositiveBoxSize, isBoxBrushVolumeMode } from "./brushes";
|
||||
import { isAdvancedRenderingShadowMapSize, isAdvancedRenderingShadowType, isBoxVolumeRenderPath, isAdvancedRenderingToneMappingMode, isHexColorString } from "./world-settings";
|
||||
export function createDiagnostic(severity, code, message, path, scope = "document") {
|
||||
return {
|
||||
code,
|
||||
severity,
|
||||
scope,
|
||||
message,
|
||||
path
|
||||
};
|
||||
}
|
||||
function isFiniteNumber(value) {
|
||||
return typeof value === "number" && Number.isFinite(value);
|
||||
}
|
||||
function isFiniteVec3(vector) {
|
||||
return isFiniteNumber(vector.x) && isFiniteNumber(vector.y) && isFiniteNumber(vector.z);
|
||||
}
|
||||
function hasPositiveFiniteVec3(vector) {
|
||||
return isFiniteVec3(vector) && vector.x > 0 && vector.y > 0 && vector.z > 0;
|
||||
}
|
||||
function isNonNegativeFiniteNumber(value) {
|
||||
return isFiniteNumber(value) && value >= 0;
|
||||
}
|
||||
function isPositiveFiniteNumber(value) {
|
||||
return isFiniteNumber(value) && value > 0;
|
||||
}
|
||||
function isPositiveInteger(value) {
|
||||
return isFiniteNumber(value) && Number.isInteger(value) && value > 0;
|
||||
}
|
||||
function isPositiveIntegerInRange(value, max) {
|
||||
return isPositiveInteger(value) && value <= max;
|
||||
}
|
||||
function isBoolean(value) {
|
||||
return typeof value === "boolean";
|
||||
}
|
||||
function hasNonZeroVectorLength(vector) {
|
||||
return vector.x !== 0 || vector.y !== 0 || vector.z !== 0;
|
||||
}
|
||||
function validateWorldSettings(world, document, diagnostics) {
|
||||
if (world.background.mode === "solid") {
|
||||
if (!isHexColorString(world.background.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-color", "Solid world backgrounds must use #RRGGBB colors.", "world.background.colorHex"));
|
||||
}
|
||||
}
|
||||
else if (world.background.mode === "verticalGradient") {
|
||||
if (!isHexColorString(world.background.topColorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-top-color", "Gradient world backgrounds must use #RRGGBB colors for the top color.", "world.background.topColorHex"));
|
||||
}
|
||||
if (!isHexColorString(world.background.bottomColorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-bottom-color", "Gradient world backgrounds must use #RRGGBB colors for the bottom color.", "world.background.bottomColorHex"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (typeof world.background.assetId !== "string" || world.background.assetId.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-asset-id", "World background images must reference a non-empty asset id.", "world.background.assetId"));
|
||||
}
|
||||
else {
|
||||
const backgroundAsset = document.assets[world.background.assetId];
|
||||
if (backgroundAsset === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-world-background-asset", `World background image asset ${world.background.assetId} does not exist.`, "world.background.assetId"));
|
||||
}
|
||||
else if (backgroundAsset.kind !== "image") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-asset-kind", "World background images must reference image assets.", "world.background.assetId"));
|
||||
}
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(world.background.environmentIntensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-background-environment-intensity", "World background environment intensity must be a non-negative finite number.", "world.background.environmentIntensity"));
|
||||
}
|
||||
}
|
||||
if (!isHexColorString(world.ambientLight.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-ambient-color", "World ambient light must use a #RRGGBB color.", "world.ambientLight.colorHex"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(world.ambientLight.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-ambient-intensity", "World ambient light intensity must remain finite and zero or greater.", "world.ambientLight.intensity"));
|
||||
}
|
||||
if (!isHexColorString(world.sunLight.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-sun-color", "World sun color must use a #RRGGBB color.", "world.sunLight.colorHex"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(world.sunLight.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-sun-intensity", "World sun intensity must remain finite and zero or greater.", "world.sunLight.intensity"));
|
||||
}
|
||||
if (!isFiniteVec3(world.sunLight.direction) || !hasNonZeroVectorLength(world.sunLight.direction)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-world-sun-direction", "World sun direction must remain finite and must not be the zero vector.", "world.sunLight.direction"));
|
||||
}
|
||||
const advancedRendering = world.advancedRendering;
|
||||
if (!isBoolean(advancedRendering.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-enabled", "Advanced rendering enabled must be a boolean.", "world.advancedRendering.enabled"));
|
||||
}
|
||||
if (!isBoolean(advancedRendering.shadows.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-shadows-enabled", "Advanced rendering shadow enabled must be a boolean.", "world.advancedRendering.shadows.enabled"));
|
||||
}
|
||||
if (!isAdvancedRenderingShadowMapSize(advancedRendering.shadows.mapSize)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-shadow-map-size", "Advanced rendering shadow map size must be one of 512, 1024, 2048, or 4096.", "world.advancedRendering.shadows.mapSize"));
|
||||
}
|
||||
if (!isAdvancedRenderingShadowType(advancedRendering.shadows.type)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-shadow-type", "Advanced rendering shadow type must be basic, pcf, or pcfSoft.", "world.advancedRendering.shadows.type"));
|
||||
}
|
||||
if (!isFiniteNumber(advancedRendering.shadows.bias)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-shadow-bias", "Advanced rendering shadow bias must be a finite number.", "world.advancedRendering.shadows.bias"));
|
||||
}
|
||||
if (!isBoolean(advancedRendering.ambientOcclusion.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-ao-enabled", "Advanced rendering ambient occlusion enabled must be a boolean.", "world.advancedRendering.ambientOcclusion.enabled"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.ambientOcclusion.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-ao-intensity", "Advanced rendering ambient occlusion intensity must be a non-negative finite number.", "world.advancedRendering.ambientOcclusion.intensity"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.ambientOcclusion.radius)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-ao-radius", "Advanced rendering ambient occlusion radius must be a non-negative finite number.", "world.advancedRendering.ambientOcclusion.radius"));
|
||||
}
|
||||
if (!isPositiveInteger(advancedRendering.ambientOcclusion.samples)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-ao-samples", "Advanced rendering ambient occlusion samples must be a positive integer.", "world.advancedRendering.ambientOcclusion.samples"));
|
||||
}
|
||||
if (!isBoolean(advancedRendering.bloom.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-bloom-enabled", "Advanced rendering bloom enabled must be a boolean.", "world.advancedRendering.bloom.enabled"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.bloom.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-bloom-intensity", "Advanced rendering bloom intensity must be a non-negative finite number.", "world.advancedRendering.bloom.intensity"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.bloom.threshold)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-bloom-threshold", "Advanced rendering bloom threshold must be a non-negative finite number.", "world.advancedRendering.bloom.threshold"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.bloom.radius)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-bloom-radius", "Advanced rendering bloom radius must be a non-negative finite number.", "world.advancedRendering.bloom.radius"));
|
||||
}
|
||||
if (!isAdvancedRenderingToneMappingMode(advancedRendering.toneMapping.mode)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-tone-mapping-mode", "Advanced rendering tone mapping mode must be none, linear, reinhard, cineon, or acesFilmic.", "world.advancedRendering.toneMapping.mode"));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(advancedRendering.toneMapping.exposure)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-tone-mapping-exposure", "Advanced rendering tone mapping exposure must be a positive finite number.", "world.advancedRendering.toneMapping.exposure"));
|
||||
}
|
||||
if (!isBoolean(advancedRendering.depthOfField.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-dof-enabled", "Advanced rendering depth of field enabled must be a boolean.", "world.advancedRendering.depthOfField.enabled"));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(advancedRendering.depthOfField.focusDistance)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-dof-focus-distance", "Advanced rendering depth of field focus distance must be a non-negative finite number.", "world.advancedRendering.depthOfField.focusDistance"));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(advancedRendering.depthOfField.focalLength)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-dof-focal-length", "Advanced rendering depth of field focal length must be a positive finite number.", "world.advancedRendering.depthOfField.focalLength"));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(advancedRendering.depthOfField.bokehScale)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-dof-bokeh-scale", "Advanced rendering depth of field bokeh scale must be a positive finite number.", "world.advancedRendering.depthOfField.bokehScale"));
|
||||
}
|
||||
if (!isBoxVolumeRenderPath(advancedRendering.fogPath)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-fog-path", "Advanced rendering fog path must be performance or quality.", "world.advancedRendering.fogPath"));
|
||||
}
|
||||
if (!isBoxVolumeRenderPath(advancedRendering.waterPath)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-water-path", "Advanced rendering water path must be performance or quality.", "world.advancedRendering.waterPath"));
|
||||
}
|
||||
if (!["none", "world", "all"].includes(advancedRendering.waterReflectionMode)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-advanced-rendering-water-reflection-mode", "Advanced rendering water reflection mode must be none, world, or all.", "world.advancedRendering.waterReflectionMode"));
|
||||
}
|
||||
}
|
||||
function validatePointLightEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-point-light-position", "Point Light position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isHexColorString(entity.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-point-light-color", "Point Light color must use a #RRGGBB color.", `${path}.colorHex`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(entity.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-point-light-intensity", "Point Light intensity must remain finite and zero or greater.", `${path}.intensity`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(entity.distance)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-point-light-distance", "Point Light distance must remain finite and greater than zero.", `${path}.distance`));
|
||||
}
|
||||
}
|
||||
function validateSpotLightEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-position", "Spot Light position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isFiniteVec3(entity.direction) || !hasNonZeroVectorLength(entity.direction)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-direction", "Spot Light direction must remain finite and must not be the zero vector.", `${path}.direction`));
|
||||
}
|
||||
if (!isHexColorString(entity.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-color", "Spot Light color must use a #RRGGBB color.", `${path}.colorHex`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(entity.intensity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-intensity", "Spot Light intensity must remain finite and zero or greater.", `${path}.intensity`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(entity.distance)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-distance", "Spot Light distance must remain finite and greater than zero.", `${path}.distance`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.angleDegrees) || entity.angleDegrees <= 0 || entity.angleDegrees >= 180) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-spot-light-angle", "Spot Light angle must remain a finite degree value between 0 and 180.", `${path}.angleDegrees`));
|
||||
}
|
||||
}
|
||||
function validateProjectAssetBoundingBox(boundingBox, path, diagnostics) {
|
||||
if (boundingBox === null) {
|
||||
return;
|
||||
}
|
||||
if (!isFiniteVec3(boundingBox.min)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-bounding-box-min", "Model asset bounding boxes must have finite minimum coordinates.", `${path}.min`));
|
||||
}
|
||||
if (!isFiniteVec3(boundingBox.max)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-bounding-box-max", "Model asset bounding boxes must have finite maximum coordinates.", `${path}.max`));
|
||||
}
|
||||
if (!isFiniteVec3(boundingBox.size) || boundingBox.size.x < 0 || boundingBox.size.y < 0 || boundingBox.size.z < 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-bounding-box-size", "Model asset bounding boxes must have finite, zero-or-greater size values.", `${path}.size`));
|
||||
}
|
||||
}
|
||||
function validateModelAssetMetadata(metadata, path, diagnostics) {
|
||||
if (metadata.format !== "glb" && metadata.format !== "gltf") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-format", "Model asset format must be glb or gltf.", `${path}.format`));
|
||||
}
|
||||
if (metadata.sceneName !== null && metadata.sceneName.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-scene-name", "Model asset scene names must be non-empty when authored.", `${path}.sceneName`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(metadata.nodeCount)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-node-count", "Model asset node counts must be finite and zero or greater.", `${path}.nodeCount`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(metadata.meshCount)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-mesh-count", "Model asset mesh counts must be finite and zero or greater.", `${path}.meshCount`));
|
||||
}
|
||||
if (!Array.isArray(metadata.materialNames) || metadata.materialNames.some((name) => typeof name !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-material-names", "Model asset material names must be string arrays.", `${path}.materialNames`));
|
||||
}
|
||||
if (!Array.isArray(metadata.textureNames) || metadata.textureNames.some((name) => typeof name !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-texture-names", "Model asset texture names must be string arrays.", `${path}.textureNames`));
|
||||
}
|
||||
if (!Array.isArray(metadata.animationNames) || metadata.animationNames.some((name) => typeof name !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-animation-names", "Model asset animation names must be string arrays.", `${path}.animationNames`));
|
||||
}
|
||||
validateProjectAssetBoundingBox(metadata.boundingBox, `${path}.boundingBox`, diagnostics);
|
||||
if (!Array.isArray(metadata.warnings) || metadata.warnings.some((warning) => typeof warning !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-asset-warnings", "Model asset warnings must be string arrays.", `${path}.warnings`));
|
||||
}
|
||||
}
|
||||
function validateImageAssetMetadata(metadata, path, diagnostics) {
|
||||
if (!isPositiveFiniteNumber(metadata.width)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-image-asset-width", "Image asset width must be finite and greater than zero.", `${path}.width`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(metadata.height)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-image-asset-height", "Image asset height must be finite and greater than zero.", `${path}.height`));
|
||||
}
|
||||
if (!isBoolean(metadata.hasAlpha)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-image-asset-alpha", "Image asset alpha flags must be booleans.", `${path}.hasAlpha`));
|
||||
}
|
||||
if (!Array.isArray(metadata.warnings) || metadata.warnings.some((warning) => typeof warning !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-image-asset-warnings", "Image asset warnings must be string arrays.", `${path}.warnings`));
|
||||
}
|
||||
}
|
||||
function validateAudioAssetMetadata(metadata, path, diagnostics) {
|
||||
if (metadata.durationSeconds !== null && !isNonNegativeFiniteNumber(metadata.durationSeconds)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-audio-asset-duration", "Audio asset durations must be finite and zero or greater when authored.", `${path}.durationSeconds`));
|
||||
}
|
||||
if (metadata.channelCount !== null && !isPositiveFiniteNumber(metadata.channelCount)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-audio-asset-channel-count", "Audio asset channel counts must be finite and greater than zero when authored.", `${path}.channelCount`));
|
||||
}
|
||||
if (metadata.sampleRateHz !== null && !isPositiveFiniteNumber(metadata.sampleRateHz)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-audio-asset-sample-rate", "Audio asset sample rates must be finite and greater than zero when authored.", `${path}.sampleRateHz`));
|
||||
}
|
||||
if (!Array.isArray(metadata.warnings) || metadata.warnings.some((warning) => typeof warning !== "string")) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-audio-asset-warnings", "Audio asset warnings must be string arrays.", `${path}.warnings`));
|
||||
}
|
||||
}
|
||||
function validateProjectAsset(asset, path, diagnostics) {
|
||||
if (asset.sourceName.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-source-name", "Asset source names must be non-empty strings.", `${path}.sourceName`));
|
||||
}
|
||||
if (asset.mimeType.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-mime-type", "Asset mime types must be non-empty strings.", `${path}.mimeType`));
|
||||
}
|
||||
if (asset.storageKey.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-storage-key", "Asset storage keys must be non-empty strings.", `${path}.storageKey`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(asset.byteLength)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-asset-byte-length", "Asset byte lengths must be finite and greater than zero.", `${path}.byteLength`));
|
||||
}
|
||||
switch (asset.kind) {
|
||||
case "model":
|
||||
validateModelAssetMetadata(asset.metadata, `${path}.metadata`, diagnostics);
|
||||
break;
|
||||
case "image":
|
||||
validateImageAssetMetadata(asset.metadata, `${path}.metadata`, diagnostics);
|
||||
break;
|
||||
case "audio":
|
||||
validateAudioAssetMetadata(asset.metadata, `${path}.metadata`, diagnostics);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function validateModelInstance(modelInstance, path, document, diagnostics) {
|
||||
if (modelInstance.name !== undefined && modelInstance.name.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-name", "Model instance names must be non-empty when authored.", `${path}.name`));
|
||||
}
|
||||
if (!isFiniteVec3(modelInstance.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-position", "Model instance positions must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isFiniteVec3(modelInstance.rotationDegrees)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-rotation", "Model instance rotations must remain finite on every axis.", `${path}.rotationDegrees`));
|
||||
}
|
||||
if (!hasPositiveFiniteVec3(modelInstance.scale)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-scale", "Model instance scales must remain finite and positive on every axis.", `${path}.scale`));
|
||||
}
|
||||
if (!isModelInstanceCollisionMode(modelInstance.collision.mode)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-collision-mode", "Model instance collision mode must be one of none, terrain, static, dynamic, or simple.", `${path}.collision.mode`));
|
||||
}
|
||||
if (!isBoolean(modelInstance.collision.visible)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-collision-visibility", "Model instance collision visibility must be a boolean.", `${path}.collision.visible`));
|
||||
}
|
||||
const asset = document.assets[modelInstance.assetId];
|
||||
if (asset === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-model-instance-asset", `Model instance asset ${modelInstance.assetId} does not exist.`, `${path}.assetId`));
|
||||
return;
|
||||
}
|
||||
if (asset.kind !== "model") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-model-instance-asset-kind", "Model instances may only reference model assets.", `${path}.assetId`));
|
||||
}
|
||||
}
|
||||
function validateEntityName(name, path, diagnostics) {
|
||||
if (name !== undefined && name.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-entity-name", "Entity names must be non-empty when authored.", `${path}.name`));
|
||||
}
|
||||
}
|
||||
function validatePlayerStartEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-position", "Player Start position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.yawDegrees)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-yaw", "Player Start yaw must remain a finite number.", `${path}.yawDegrees`));
|
||||
}
|
||||
if (!isPlayerStartColliderMode(entity.collider.mode)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-collider-mode", "Player Start collider mode must be capsule, box, or none.", `${path}.collider.mode`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.collider.eyeHeight) || entity.collider.eyeHeight <= 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-eye-height", "Player Start eye height must remain a finite number greater than zero.", `${path}.collider.eyeHeight`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.collider.capsuleRadius) || entity.collider.capsuleRadius <= 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-capsule-radius", "Player Start capsule radius must remain a finite number greater than zero.", `${path}.collider.capsuleRadius`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.collider.capsuleHeight) || entity.collider.capsuleHeight <= 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-capsule-height", "Player Start capsule height must remain a finite number greater than zero.", `${path}.collider.capsuleHeight`));
|
||||
}
|
||||
if (!isFiniteVec3(entity.collider.boxSize) ||
|
||||
entity.collider.boxSize.x <= 0 ||
|
||||
entity.collider.boxSize.y <= 0 ||
|
||||
entity.collider.boxSize.z <= 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-box-size", "Player Start box size must remain finite and positive on every axis.", `${path}.collider.boxSize`));
|
||||
}
|
||||
if (entity.collider.capsuleHeight < entity.collider.capsuleRadius * 2) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-capsule-proportions", "Player Start capsule height must be at least twice the capsule radius.", `${path}.collider.capsuleHeight`));
|
||||
}
|
||||
const colliderHeight = getPlayerStartColliderHeight(entity.collider);
|
||||
if (colliderHeight !== null && entity.collider.eyeHeight > colliderHeight) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-player-start-eye-height", "Player Start eye height must fit within the authored collider height.", `${path}.collider.eyeHeight`));
|
||||
}
|
||||
}
|
||||
function validateSoundEmitterAudioAsset(entity, path, document, diagnostics, missingSeverity) {
|
||||
if (entity.audioAssetId === null) {
|
||||
diagnostics.push(createDiagnostic(missingSeverity, "missing-sound-emitter-audio-asset", entity.autoplay
|
||||
? "Sound Emitter autoplay requires an assigned audio asset."
|
||||
: "Sound Emitter has no audio asset assigned yet.", `${path}.audioAssetId`));
|
||||
return null;
|
||||
}
|
||||
const asset = document.assets[entity.audioAssetId];
|
||||
if (asset === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-sound-emitter-audio-asset", `Sound Emitter audio asset ${entity.audioAssetId} does not exist.`, `${path}.audioAssetId`));
|
||||
return null;
|
||||
}
|
||||
if (asset.kind !== "audio") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-audio-asset-kind", "Sound Emitter audioAssetId must reference an audio asset.", `${path}.audioAssetId`));
|
||||
return null;
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
function validateSoundEmitterEntity(entity, path, document, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-position", "Sound Emitter position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(entity.volume)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-volume", "Sound Emitter volume must remain finite and zero or greater.", `${path}.volume`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(entity.refDistance)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-ref-distance", "Sound Emitter ref distance must remain finite and greater than zero.", `${path}.refDistance`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(entity.maxDistance)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-max-distance", "Sound Emitter max distance must remain finite and greater than zero.", `${path}.maxDistance`));
|
||||
}
|
||||
if (isPositiveFiniteNumber(entity.refDistance) && isPositiveFiniteNumber(entity.maxDistance) && entity.maxDistance < entity.refDistance) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-distance-order", "Sound Emitter max distance must be greater than or equal to ref distance.", `${path}.maxDistance`));
|
||||
}
|
||||
if (!isBoolean(entity.autoplay)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-autoplay", "Sound Emitter autoplay must remain a boolean.", `${path}.autoplay`));
|
||||
}
|
||||
if (!isBoolean(entity.loop)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-loop", "Sound Emitter loop must remain a boolean.", `${path}.loop`));
|
||||
}
|
||||
validateSoundEmitterAudioAsset(entity, path, document, diagnostics, entity.autoplay ? "error" : "warning");
|
||||
}
|
||||
function validateTriggerVolumeEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-trigger-volume-position", "Trigger Volume position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!hasPositiveFiniteVec3(entity.size)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-trigger-volume-size", "Trigger Volume size must remain finite and positive on every axis.", `${path}.size`));
|
||||
}
|
||||
if (!isBoolean(entity.triggerOnEnter)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-trigger-volume-enter-flag", "Trigger Volume triggerOnEnter must remain a boolean.", `${path}.triggerOnEnter`));
|
||||
}
|
||||
if (!isBoolean(entity.triggerOnExit)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-trigger-volume-exit-flag", "Trigger Volume triggerOnExit must remain a boolean.", `${path}.triggerOnExit`));
|
||||
}
|
||||
}
|
||||
function validateTeleportTargetEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-teleport-target-position", "Teleport Target position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isFiniteNumber(entity.yawDegrees)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-teleport-target-yaw", "Teleport Target yaw must remain a finite number.", `${path}.yawDegrees`));
|
||||
}
|
||||
}
|
||||
function validateInteractableEntity(entity, path, diagnostics) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-interactable-position", "Interactable position must remain finite on every axis.", `${path}.position`));
|
||||
}
|
||||
if (!isPositiveFiniteNumber(entity.radius)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-interactable-radius", "Interactable radius must remain finite and greater than zero.", `${path}.radius`));
|
||||
}
|
||||
if (typeof entity.prompt !== "string" || entity.prompt.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-interactable-prompt", "Interactable prompt must remain a non-empty string.", `${path}.prompt`));
|
||||
}
|
||||
if (!isBoolean(entity.enabled)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-interactable-enabled", "Interactable enabled must remain a boolean.", `${path}.enabled`));
|
||||
}
|
||||
}
|
||||
function validateInteractionLink(link, path, document, diagnostics) {
|
||||
const sourceEntity = document.entities[link.sourceEntityId];
|
||||
if (sourceEntity === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-interaction-source-entity", `Interaction source entity ${link.sourceEntityId} does not exist.`, `${path}.sourceEntityId`));
|
||||
return;
|
||||
}
|
||||
if (sourceEntity.kind !== "triggerVolume" && sourceEntity.kind !== "interactable") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-interaction-source-kind", "Interaction links may only source from Trigger Volume or Interactable entities in the current slice.", `${path}.sourceEntityId`));
|
||||
}
|
||||
if (sourceEntity.kind === "triggerVolume") {
|
||||
if (link.trigger !== "enter" && link.trigger !== "exit") {
|
||||
diagnostics.push(createDiagnostic("error", "unsupported-interaction-trigger", "Trigger Volume links may only use enter or exit triggers.", `${path}.trigger`));
|
||||
}
|
||||
}
|
||||
else if (sourceEntity.kind === "interactable") {
|
||||
if (link.trigger !== "click") {
|
||||
diagnostics.push(createDiagnostic("error", "unsupported-interaction-trigger", "Interactable links may only use the click trigger.", `${path}.trigger`));
|
||||
}
|
||||
}
|
||||
switch (link.action.type) {
|
||||
case "teleportPlayer": {
|
||||
const targetEntity = document.entities[link.action.targetEntityId];
|
||||
if (targetEntity === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-teleport-target-entity", `Teleport target entity ${link.action.targetEntityId} does not exist.`, `${path}.action.targetEntityId`));
|
||||
return;
|
||||
}
|
||||
if (targetEntity.kind !== "teleportTarget") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-teleport-target-kind", "Teleport player actions must target a Teleport Target entity.", `${path}.action.targetEntityId`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "toggleVisibility":
|
||||
if (document.brushes[link.action.targetBrushId] === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-visibility-target-brush", `Visibility target brush ${link.action.targetBrushId} does not exist.`, `${path}.action.targetBrushId`));
|
||||
}
|
||||
if (link.action.visible !== undefined && typeof link.action.visible !== "boolean") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-visibility-action-visible", "Visibility actions must use a boolean visible value when authored.", `${path}.action.visible`));
|
||||
}
|
||||
break;
|
||||
case "playAnimation":
|
||||
{
|
||||
const targetModelInstance = document.modelInstances[link.action.targetModelInstanceId];
|
||||
if (targetModelInstance === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-play-animation-target-instance", `Play animation target model instance ${link.action.targetModelInstanceId} does not exist.`, `${path}.action.targetModelInstanceId`));
|
||||
return;
|
||||
}
|
||||
if (link.action.clipName.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-play-animation-clip-name", "Play animation clip name must be non-empty.", `${path}.action.clipName`));
|
||||
return;
|
||||
}
|
||||
const targetAsset = document.assets[targetModelInstance.assetId];
|
||||
if (targetAsset === undefined || targetAsset.kind !== "model") {
|
||||
return;
|
||||
}
|
||||
if (!targetAsset.metadata.animationNames.includes(link.action.clipName)) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-play-animation-clip", `Play animation clip ${link.action.clipName} does not exist on model asset ${targetAsset.id}.`, `${path}.action.clipName`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "stopAnimation":
|
||||
// Validate that the target model instance exists in the document
|
||||
if (document.modelInstances[link.action.targetModelInstanceId] === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-stop-animation-target-instance", `Stop animation target model instance ${link.action.targetModelInstanceId} does not exist.`, `${path}.action.targetModelInstanceId`));
|
||||
}
|
||||
break;
|
||||
case "playSound":
|
||||
case "stopSound": {
|
||||
const targetEntity = document.entities[link.action.targetSoundEmitterId];
|
||||
if (targetEntity === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-sound-emitter-entity", `Sound emitter entity ${link.action.targetSoundEmitterId} does not exist.`, `${path}.action.targetSoundEmitterId`));
|
||||
break;
|
||||
}
|
||||
if (targetEntity.kind !== "soundEmitter") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-sound-emitter-kind", "Sound playback actions must target a Sound Emitter entity.", `${path}.action.targetSoundEmitterId`));
|
||||
break;
|
||||
}
|
||||
if (targetEntity.audioAssetId === null) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-sound-emitter-audio-asset", "Sound playback actions require a Sound Emitter that references an audio asset.", `${path}.action.targetSoundEmitterId`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
diagnostics.push(createDiagnostic("error", "unsupported-interaction-action", `Unsupported interaction action ${link.action.type}.`, `${path}.action.type`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
function registerAuthoredId(id, path, seenIds, diagnostics) {
|
||||
const previousPath = seenIds.get(id);
|
||||
if (previousPath !== undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "duplicate-authored-id", `Duplicate authored id ${id} is already used at ${previousPath}.`, path));
|
||||
return;
|
||||
}
|
||||
seenIds.set(id, path);
|
||||
}
|
||||
export function formatSceneDiagnostic(diagnostic) {
|
||||
return diagnostic.path === undefined ? diagnostic.message : `${diagnostic.path}: ${diagnostic.message}`;
|
||||
}
|
||||
export function formatSceneDiagnosticSummary(diagnostics, limit = 3) {
|
||||
if (diagnostics.length === 0) {
|
||||
return "No diagnostics.";
|
||||
}
|
||||
const visibleDiagnostics = diagnostics.slice(0, Math.max(1, limit));
|
||||
const summary = visibleDiagnostics.map((diagnostic) => formatSceneDiagnostic(diagnostic)).join("; ");
|
||||
const remainingCount = diagnostics.length - visibleDiagnostics.length;
|
||||
return remainingCount > 0 ? `${summary}; +${remainingCount} more` : summary;
|
||||
}
|
||||
export function validateSceneDocument(document) {
|
||||
const diagnostics = [];
|
||||
const seenIds = new Map();
|
||||
validateWorldSettings(document.world, document, diagnostics);
|
||||
for (const [materialKey, material] of Object.entries(document.materials)) {
|
||||
const path = `materials.${materialKey}`;
|
||||
if (material.id !== materialKey) {
|
||||
diagnostics.push(createDiagnostic("error", "material-id-mismatch", "Material ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(material.id, path, seenIds, diagnostics);
|
||||
}
|
||||
for (const [assetKey, asset] of Object.entries(document.assets)) {
|
||||
const path = `assets.${assetKey}`;
|
||||
if (asset.id !== assetKey) {
|
||||
diagnostics.push(createDiagnostic("error", "asset-id-mismatch", "Asset ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(asset.id, path, seenIds, diagnostics);
|
||||
validateProjectAsset(asset, path, diagnostics);
|
||||
}
|
||||
for (const [brushKey, brush] of Object.entries(document.brushes)) {
|
||||
const path = `brushes.${brushKey}`;
|
||||
if (brush.id !== brushKey) {
|
||||
diagnostics.push(createDiagnostic("error", "brush-id-mismatch", "Brush ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(brush.id, path, seenIds, diagnostics);
|
||||
if (brush.name !== undefined && brush.name.trim().length === 0) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-name", "Box brush names must be non-empty when authored.", `${path}.name`));
|
||||
}
|
||||
if (!isFiniteVec3(brush.center)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-center", "Box brush centers must remain finite on every axis.", `${path}.center`));
|
||||
}
|
||||
if (!isFiniteVec3(brush.rotationDegrees)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-rotation", "Box brush rotations must remain finite on every axis.", `${path}.rotationDegrees`));
|
||||
}
|
||||
if (!isFiniteVec3(brush.size) || !hasPositiveBoxSize(brush.size)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-size", "Box brush sizes must remain finite and positive on every axis.", `${path}.size`));
|
||||
}
|
||||
for (const vertexId of BOX_VERTEX_IDS) {
|
||||
if (!isFiniteVec3(brush.geometry.vertices[vertexId])) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-geometry-vertex", "Box brush geometry vertices must remain finite on every axis.", `${path}.geometry.vertices.${vertexId}`));
|
||||
}
|
||||
}
|
||||
for (const faceId of BOX_FACE_IDS) {
|
||||
const materialId = brush.faces[faceId].materialId;
|
||||
if (materialId !== null && document.materials[materialId] === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "missing-material-ref", `Face material reference ${materialId} does not exist in the document material registry.`, `${path}.faces.${faceId}.materialId`));
|
||||
}
|
||||
}
|
||||
const volume = brush.volume;
|
||||
if (!isBoxBrushVolumeMode(volume?.mode)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-volume-mode", "Box volume mode must be none, water, or fog.", `${path}.volume.mode`));
|
||||
continue;
|
||||
}
|
||||
if (volume.mode === "water") {
|
||||
const water = volume.water;
|
||||
if (water === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-settings", "Water volumes must define water settings.", `${path}.volume.water`));
|
||||
}
|
||||
else {
|
||||
if (typeof water.colorHex !== "string" || !isHexColorString(water.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-color", "Water volume color must use #RRGGBB format.", `${path}.volume.water.colorHex`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(water.surfaceOpacity)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-surface-opacity", "Water surface opacity must be a non-negative finite number.", `${path}.volume.water.surfaceOpacity`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(water.waveStrength)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-wave-strength", "Water wave strength must be a non-negative finite number.", `${path}.volume.water.waveStrength`));
|
||||
}
|
||||
if (!isPositiveIntegerInRange(water.foamContactLimit, MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-foam-contact-limit", `Water foam contact limit must be a positive integer between 1 and ${MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT}.`, `${path}.volume.water.foamContactLimit`));
|
||||
}
|
||||
if (typeof water.surfaceDisplacementEnabled !== "boolean") {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-water-surface-displacement-enabled", "Water surface displacement must be enabled or disabled explicitly.", `${path}.volume.water.surfaceDisplacementEnabled`));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (volume.mode === "fog") {
|
||||
const fog = volume.fog;
|
||||
if (fog === undefined) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-fog-settings", "Fog volumes must define fog settings.", `${path}.volume.fog`));
|
||||
}
|
||||
else {
|
||||
if (typeof fog.colorHex !== "string" || !isHexColorString(fog.colorHex)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-fog-color", "Fog volume color must use #RRGGBB format.", `${path}.volume.fog.colorHex`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(fog.density)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-fog-density", "Fog volume density must be a non-negative finite number.", `${path}.volume.fog.density`));
|
||||
}
|
||||
if (!isNonNegativeFiniteNumber(fog.padding)) {
|
||||
diagnostics.push(createDiagnostic("error", "invalid-box-fog-padding", "Fog volume padding must be a non-negative finite number.", `${path}.volume.fog.padding`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [modelInstanceKey, modelInstance] of Object.entries(document.modelInstances)) {
|
||||
const path = `modelInstances.${modelInstanceKey}`;
|
||||
if (modelInstance.id !== modelInstanceKey) {
|
||||
diagnostics.push(createDiagnostic("error", "model-instance-id-mismatch", "Model instance ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(modelInstance.id, path, seenIds, diagnostics);
|
||||
validateModelInstance(modelInstance, path, document, diagnostics);
|
||||
}
|
||||
for (const [entityKey, entity] of Object.entries(document.entities)) {
|
||||
const path = `entities.${entityKey}`;
|
||||
if (entity.id !== entityKey) {
|
||||
diagnostics.push(createDiagnostic("error", "entity-id-mismatch", "Entity ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(entity.id, path, seenIds, diagnostics);
|
||||
validateEntityName(entity.name, path, diagnostics);
|
||||
switch (entity.kind) {
|
||||
case "pointLight":
|
||||
validatePointLightEntity(entity, path, diagnostics);
|
||||
break;
|
||||
case "spotLight":
|
||||
validateSpotLightEntity(entity, path, diagnostics);
|
||||
break;
|
||||
case "playerStart":
|
||||
validatePlayerStartEntity(entity, path, diagnostics);
|
||||
break;
|
||||
case "soundEmitter":
|
||||
validateSoundEmitterEntity(entity, path, document, diagnostics);
|
||||
break;
|
||||
case "triggerVolume":
|
||||
validateTriggerVolumeEntity(entity, path, diagnostics);
|
||||
break;
|
||||
case "teleportTarget":
|
||||
validateTeleportTargetEntity(entity, path, diagnostics);
|
||||
break;
|
||||
case "interactable":
|
||||
validateInteractableEntity(entity, path, diagnostics);
|
||||
break;
|
||||
default:
|
||||
diagnostics.push(createDiagnostic("error", "unsupported-entity-kind", `Unsupported entity kind ${entity.kind}.`, `${path}.kind`));
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const [linkKey, link] of Object.entries(document.interactionLinks)) {
|
||||
const path = `interactionLinks.${linkKey}`;
|
||||
if (link.id !== linkKey) {
|
||||
diagnostics.push(createDiagnostic("error", "interaction-link-id-mismatch", "Interaction link ids must match their registry key.", `${path}.id`));
|
||||
}
|
||||
registerAuthoredId(link.id, path, seenIds, diagnostics);
|
||||
validateInteractionLink(link, path, document, diagnostics);
|
||||
}
|
||||
return {
|
||||
diagnostics,
|
||||
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error"),
|
||||
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning")
|
||||
};
|
||||
}
|
||||
export function assertSceneDocumentIsValid(document) {
|
||||
const validation = validateSceneDocument(document);
|
||||
if (validation.errors.length > 0) {
|
||||
throw new Error(`Scene document has ${validation.errors.length} validation error(s): ${formatSceneDiagnosticSummary(validation.errors)}`);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { cloneMaterialRegistry, createStarterMaterialRegistry } from "../materials/starter-material-library";
|
||||
import { createDefaultWorldSettings } from "./world-settings";
|
||||
export const SCENE_DOCUMENT_VERSION = 21;
|
||||
export const WATER_SURFACE_DISPLACEMENT_SCENE_DOCUMENT_VERSION = 21;
|
||||
export const WHITEBOX_BOX_VOLUME_SCENE_DOCUMENT_VERSION = 20;
|
||||
export const WHITEBOX_GEOMETRY_SCENE_DOCUMENT_VERSION = 19;
|
||||
export const WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION = 18;
|
||||
export const PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION = 17;
|
||||
export const IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION = 16;
|
||||
export const ENTITY_NAMES_SCENE_DOCUMENT_VERSION = 15;
|
||||
export const SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION = 13;
|
||||
export const ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION = 12;
|
||||
export const LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION = 10;
|
||||
export const MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION = 9;
|
||||
export const FOUNDATION_SCENE_DOCUMENT_VERSION = 1;
|
||||
export const BOX_BRUSH_SCENE_DOCUMENT_VERSION = 2;
|
||||
export const FACE_MATERIALS_SCENE_DOCUMENT_VERSION = 3;
|
||||
export const RUNNER_V1_SCENE_DOCUMENT_VERSION = 4;
|
||||
export const FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION = 5;
|
||||
export const WORLD_ENVIRONMENT_SCENE_DOCUMENT_VERSION = 6;
|
||||
export const ENTITY_SYSTEM_FOUNDATION_SCENE_DOCUMENT_VERSION = 7;
|
||||
export const TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION = 8;
|
||||
export function createEmptySceneDocument(overrides = {}) {
|
||||
return {
|
||||
version: SCENE_DOCUMENT_VERSION,
|
||||
name: overrides.name ?? "Untitled Scene",
|
||||
world: overrides.world ?? createDefaultWorldSettings(),
|
||||
materials: cloneMaterialRegistry(overrides.materials ?? createStarterMaterialRegistry()),
|
||||
textures: {},
|
||||
assets: {},
|
||||
brushes: {},
|
||||
modelInstances: {},
|
||||
entities: {},
|
||||
interactionLinks: {}
|
||||
};
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
import { DEFAULT_SUN_DIRECTION } from "../core/vector";
|
||||
export const ADVANCED_RENDERING_SHADOW_MAP_SIZES = [512, 1024, 2048, 4096];
|
||||
export const ADVANCED_RENDERING_SHADOW_TYPES = ["basic", "pcf", "pcfSoft"];
|
||||
export const ADVANCED_RENDERING_TONE_MAPPING_MODES = ["none", "linear", "reinhard", "cineon", "acesFilmic"];
|
||||
export const BOX_VOLUME_RENDER_PATHS = ["performance", "quality"];
|
||||
export const ADVANCED_RENDERING_WATER_REFLECTION_MODES = ["none", "world", "all"];
|
||||
const DEFAULT_SOLID_BACKGROUND_COLOR = "#2f3947";
|
||||
const DEFAULT_GRADIENT_TOP_COLOR = DEFAULT_SOLID_BACKGROUND_COLOR;
|
||||
const DEFAULT_GRADIENT_BOTTOM_COLOR = "#141a22";
|
||||
const DEFAULT_ADVANCED_RENDERING_SHADOW_MAP_SIZE = 2048;
|
||||
const DEFAULT_ADVANCED_RENDERING_SHADOW_TYPE = "pcfSoft";
|
||||
const DEFAULT_ADVANCED_RENDERING_SHADOW_BIAS = -0.0005;
|
||||
const DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_INTENSITY = 1;
|
||||
const DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_RADIUS = 0.5;
|
||||
const DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_SAMPLES = 8;
|
||||
const DEFAULT_ADVANCED_RENDERING_BLOOM_INTENSITY = 0.75;
|
||||
const DEFAULT_ADVANCED_RENDERING_BLOOM_THRESHOLD = 0.85;
|
||||
const DEFAULT_ADVANCED_RENDERING_BLOOM_RADIUS = 0.35;
|
||||
const DEFAULT_ADVANCED_RENDERING_TONE_MAPPING_MODE = "acesFilmic";
|
||||
const DEFAULT_ADVANCED_RENDERING_TONE_MAPPING_EXPOSURE = 1;
|
||||
const DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_FOCUS_DISTANCE = 10;
|
||||
const DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_FOCAL_LENGTH = 0.03;
|
||||
const DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_BOKEH_SCALE = 1.5;
|
||||
const DEFAULT_BOX_VOLUME_RENDER_PATH = "performance";
|
||||
const DEFAULT_ADVANCED_RENDERING_WATER_REFLECTION_MODE = "none";
|
||||
export function isAdvancedRenderingShadowMapSize(value) {
|
||||
return ADVANCED_RENDERING_SHADOW_MAP_SIZES.includes(value);
|
||||
}
|
||||
export function isAdvancedRenderingShadowType(value) {
|
||||
return ADVANCED_RENDERING_SHADOW_TYPES.includes(value);
|
||||
}
|
||||
export function isAdvancedRenderingToneMappingMode(value) {
|
||||
return ADVANCED_RENDERING_TONE_MAPPING_MODES.includes(value);
|
||||
}
|
||||
export function isBoxVolumeRenderPath(value) {
|
||||
return BOX_VOLUME_RENDER_PATHS.includes(value);
|
||||
}
|
||||
export function isAdvancedRenderingWaterReflectionMode(value) {
|
||||
return ADVANCED_RENDERING_WATER_REFLECTION_MODES.includes(value);
|
||||
}
|
||||
export function createDefaultAdvancedRenderingSettings() {
|
||||
return {
|
||||
enabled: false,
|
||||
shadows: {
|
||||
enabled: false,
|
||||
mapSize: DEFAULT_ADVANCED_RENDERING_SHADOW_MAP_SIZE,
|
||||
type: DEFAULT_ADVANCED_RENDERING_SHADOW_TYPE,
|
||||
bias: DEFAULT_ADVANCED_RENDERING_SHADOW_BIAS
|
||||
},
|
||||
ambientOcclusion: {
|
||||
enabled: false,
|
||||
intensity: DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_INTENSITY,
|
||||
radius: DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_RADIUS,
|
||||
samples: DEFAULT_ADVANCED_RENDERING_AMBIENT_OCCLUSION_SAMPLES
|
||||
},
|
||||
bloom: {
|
||||
enabled: false,
|
||||
intensity: DEFAULT_ADVANCED_RENDERING_BLOOM_INTENSITY,
|
||||
threshold: DEFAULT_ADVANCED_RENDERING_BLOOM_THRESHOLD,
|
||||
radius: DEFAULT_ADVANCED_RENDERING_BLOOM_RADIUS
|
||||
},
|
||||
toneMapping: {
|
||||
mode: DEFAULT_ADVANCED_RENDERING_TONE_MAPPING_MODE,
|
||||
exposure: DEFAULT_ADVANCED_RENDERING_TONE_MAPPING_EXPOSURE
|
||||
},
|
||||
depthOfField: {
|
||||
enabled: false,
|
||||
focusDistance: DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_FOCUS_DISTANCE,
|
||||
focalLength: DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_FOCAL_LENGTH,
|
||||
bokehScale: DEFAULT_ADVANCED_RENDERING_DEPTH_OF_FIELD_BOKEH_SCALE
|
||||
},
|
||||
fogPath: DEFAULT_BOX_VOLUME_RENDER_PATH,
|
||||
waterPath: DEFAULT_BOX_VOLUME_RENDER_PATH,
|
||||
waterReflectionMode: DEFAULT_ADVANCED_RENDERING_WATER_REFLECTION_MODE
|
||||
};
|
||||
}
|
||||
export function createDefaultWorldSettings() {
|
||||
return {
|
||||
background: {
|
||||
mode: "solid",
|
||||
colorHex: DEFAULT_SOLID_BACKGROUND_COLOR
|
||||
},
|
||||
ambientLight: {
|
||||
colorHex: "#f7f1e8",
|
||||
intensity: 1
|
||||
},
|
||||
sunLight: {
|
||||
colorHex: "#fff1d5",
|
||||
intensity: 1.75,
|
||||
direction: {
|
||||
...DEFAULT_SUN_DIRECTION
|
||||
}
|
||||
},
|
||||
advancedRendering: createDefaultAdvancedRenderingSettings()
|
||||
};
|
||||
}
|
||||
export function isHexColorString(value) {
|
||||
return /^#[0-9a-f]{6}$/i.test(value);
|
||||
}
|
||||
export function isWorldBackgroundMode(value) {
|
||||
return value === "solid" || value === "verticalGradient" || value === "image";
|
||||
}
|
||||
export function cloneWorldBackgroundSettings(background) {
|
||||
if (background.mode === "solid") {
|
||||
return {
|
||||
mode: "solid",
|
||||
colorHex: background.colorHex
|
||||
};
|
||||
}
|
||||
if (background.mode === "verticalGradient") {
|
||||
return {
|
||||
mode: "verticalGradient",
|
||||
topColorHex: background.topColorHex,
|
||||
bottomColorHex: background.bottomColorHex
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: "image",
|
||||
assetId: background.assetId,
|
||||
environmentIntensity: background.environmentIntensity
|
||||
};
|
||||
}
|
||||
export function cloneWorldSettings(world) {
|
||||
return {
|
||||
background: cloneWorldBackgroundSettings(world.background),
|
||||
ambientLight: {
|
||||
...world.ambientLight
|
||||
},
|
||||
sunLight: {
|
||||
...world.sunLight,
|
||||
direction: {
|
||||
...world.sunLight.direction
|
||||
}
|
||||
},
|
||||
advancedRendering: cloneAdvancedRenderingSettings(world.advancedRendering)
|
||||
};
|
||||
}
|
||||
export function cloneAdvancedRenderingSettings(settings) {
|
||||
return {
|
||||
enabled: settings.enabled,
|
||||
shadows: {
|
||||
...settings.shadows
|
||||
},
|
||||
ambientOcclusion: {
|
||||
...settings.ambientOcclusion
|
||||
},
|
||||
bloom: {
|
||||
...settings.bloom
|
||||
},
|
||||
toneMapping: {
|
||||
...settings.toneMapping
|
||||
},
|
||||
depthOfField: {
|
||||
...settings.depthOfField
|
||||
},
|
||||
fogPath: settings.fogPath,
|
||||
waterPath: settings.waterPath,
|
||||
waterReflectionMode: settings.waterReflectionMode
|
||||
};
|
||||
}
|
||||
export function areWorldBackgroundSettingsEqual(left, right) {
|
||||
if (left.mode !== right.mode) {
|
||||
return false;
|
||||
}
|
||||
if (left.mode === "solid" && right.mode === "solid") {
|
||||
return left.colorHex === right.colorHex;
|
||||
}
|
||||
if (left.mode === "verticalGradient" && right.mode === "verticalGradient") {
|
||||
return left.topColorHex === right.topColorHex && left.bottomColorHex === right.bottomColorHex;
|
||||
}
|
||||
return left.mode === "image" && right.mode === "image" && left.assetId === right.assetId && left.environmentIntensity === right.environmentIntensity;
|
||||
}
|
||||
export function areWorldSettingsEqual(left, right) {
|
||||
return (areWorldBackgroundSettingsEqual(left.background, right.background) &&
|
||||
left.ambientLight.colorHex === right.ambientLight.colorHex &&
|
||||
left.ambientLight.intensity === right.ambientLight.intensity &&
|
||||
left.sunLight.colorHex === right.sunLight.colorHex &&
|
||||
left.sunLight.intensity === right.sunLight.intensity &&
|
||||
left.sunLight.direction.x === right.sunLight.direction.x &&
|
||||
left.sunLight.direction.y === right.sunLight.direction.y &&
|
||||
left.sunLight.direction.z === right.sunLight.direction.z &&
|
||||
areAdvancedRenderingSettingsEqual(left.advancedRendering, right.advancedRendering));
|
||||
}
|
||||
export function areAdvancedRenderingSettingsEqual(left, right) {
|
||||
return (left.enabled === right.enabled &&
|
||||
left.shadows.enabled === right.shadows.enabled &&
|
||||
left.shadows.mapSize === right.shadows.mapSize &&
|
||||
left.shadows.type === right.shadows.type &&
|
||||
left.shadows.bias === right.shadows.bias &&
|
||||
left.ambientOcclusion.enabled === right.ambientOcclusion.enabled &&
|
||||
left.ambientOcclusion.intensity === right.ambientOcclusion.intensity &&
|
||||
left.ambientOcclusion.radius === right.ambientOcclusion.radius &&
|
||||
left.ambientOcclusion.samples === right.ambientOcclusion.samples &&
|
||||
left.bloom.enabled === right.bloom.enabled &&
|
||||
left.bloom.intensity === right.bloom.intensity &&
|
||||
left.bloom.threshold === right.bloom.threshold &&
|
||||
left.bloom.radius === right.bloom.radius &&
|
||||
left.toneMapping.mode === right.toneMapping.mode &&
|
||||
left.toneMapping.exposure === right.toneMapping.exposure &&
|
||||
left.depthOfField.enabled === right.depthOfField.enabled &&
|
||||
left.depthOfField.focusDistance === right.depthOfField.focusDistance &&
|
||||
left.depthOfField.focalLength === right.depthOfField.focalLength &&
|
||||
left.depthOfField.bokehScale === right.depthOfField.bokehScale &&
|
||||
left.fogPath === right.fogPath &&
|
||||
left.waterPath === right.waterPath &&
|
||||
left.waterReflectionMode === right.waterReflectionMode);
|
||||
}
|
||||
export function changeWorldBackgroundMode(background, mode, imageAssetId) {
|
||||
if (mode === "image") {
|
||||
if (imageAssetId === undefined || imageAssetId.trim().length === 0) {
|
||||
if (background.mode === "image") {
|
||||
return cloneWorldBackgroundSettings(background);
|
||||
}
|
||||
throw new Error("An image asset must be selected to use an image background.");
|
||||
}
|
||||
return {
|
||||
mode: "image",
|
||||
assetId: imageAssetId,
|
||||
environmentIntensity: background.mode === "image" ? background.environmentIntensity : 0.5
|
||||
};
|
||||
}
|
||||
if (background.mode === mode) {
|
||||
return cloneWorldBackgroundSettings(background);
|
||||
}
|
||||
if (mode === "solid") {
|
||||
return {
|
||||
mode: "solid",
|
||||
colorHex: background.mode === "solid"
|
||||
? background.colorHex
|
||||
: background.mode === "verticalGradient"
|
||||
? background.topColorHex
|
||||
: DEFAULT_SOLID_BACKGROUND_COLOR
|
||||
};
|
||||
}
|
||||
if (background.mode === "solid") {
|
||||
return {
|
||||
mode: "verticalGradient",
|
||||
topColorHex: background.colorHex,
|
||||
bottomColorHex: DEFAULT_GRADIENT_BOTTOM_COLOR
|
||||
};
|
||||
}
|
||||
if (background.mode === "verticalGradient") {
|
||||
return {
|
||||
mode: "verticalGradient",
|
||||
topColorHex: background.topColorHex,
|
||||
bottomColorHex: background.bottomColorHex
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode: "verticalGradient",
|
||||
topColorHex: DEFAULT_GRADIENT_TOP_COLOR,
|
||||
bottomColorHex: DEFAULT_GRADIENT_BOTTOM_COLOR
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user