Update scene document migration and validation for model assets
This commit is contained in:
@@ -2,7 +2,7 @@ import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { cloneModelInstance, getModelInstanceKindLabel, type ModelInstance } from "../assets/model-instances";
|
||||
import { cloneProjectAssetRecord, getProjectAssetKindLabel } from "../assets/project-assets";
|
||||
import { getProjectAssetKindLabel } from "../assets/project-assets";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
@@ -95,4 +95,3 @@ export function createUpsertModelInstanceCommand(options: UpsertModelInstanceCom
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { createStarterMaterialRegistry, type MaterialDef, type MaterialPattern } from "../materials/starter-material-library";
|
||||
import {
|
||||
createModelInstance,
|
||||
normalizeModelInstanceName,
|
||||
type ModelInstance
|
||||
} from "../assets/model-instances";
|
||||
import {
|
||||
isProjectAssetKind,
|
||||
type AudioAssetMetadata,
|
||||
type ImageAssetMetadata,
|
||||
type ModelAssetMetadata,
|
||||
type ProjectAssetBoundingBox,
|
||||
type ProjectAssetRecord
|
||||
} from "../assets/project-assets";
|
||||
import {
|
||||
createInteractableEntity,
|
||||
createPlayerStartEntity,
|
||||
@@ -29,6 +42,7 @@ import {
|
||||
FACE_MATERIALS_SCENE_DOCUMENT_VERSION,
|
||||
FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION,
|
||||
FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||
MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION,
|
||||
RUNNER_V1_SCENE_DOCUMENT_VERSION,
|
||||
SCENE_DOCUMENT_VERSION,
|
||||
TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION,
|
||||
@@ -135,6 +149,203 @@ function expectEmptyCollection(value: unknown, label: string): Record<string, ne
|
||||
return {};
|
||||
}
|
||||
|
||||
function readProjectAssetBoundingBox(value: unknown, label: string): ProjectAssetBoundingBox {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
const min = readVec3(value.min, `${label}.min`);
|
||||
const max = readVec3(value.max, `${label}.max`);
|
||||
const size = readVec3(value.size, `${label}.size`);
|
||||
|
||||
if (size.x < 0 || size.y < 0 || size.z < 0) {
|
||||
throw new Error(`${label}.size values must remain zero or greater.`);
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
size
|
||||
};
|
||||
}
|
||||
|
||||
function readModelAssetMetadata(value: unknown, label: string): ModelAssetMetadata {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
const format = expectString(value.format, `${label}.format`);
|
||||
|
||||
if (format !== "glb" && format !== "gltf") {
|
||||
throw new Error(`${label}.format must be glb or gltf.`);
|
||||
}
|
||||
|
||||
const sceneName = value.sceneName === null ? null : expectOptionalString(value.sceneName, `${label}.sceneName`) ?? null;
|
||||
|
||||
return {
|
||||
kind: "model",
|
||||
format,
|
||||
sceneName,
|
||||
nodeCount: expectNonNegativeFiniteNumber(value.nodeCount, `${label}.nodeCount`),
|
||||
meshCount: expectNonNegativeFiniteNumber(value.meshCount, `${label}.meshCount`),
|
||||
materialNames: expectStringArray(value.materialNames, `${label}.materialNames`),
|
||||
textureNames: expectStringArray(value.textureNames, `${label}.textureNames`),
|
||||
animationNames: expectStringArray(value.animationNames, `${label}.animationNames`),
|
||||
boundingBox: value.boundingBox === null ? null : readProjectAssetBoundingBox(value.boundingBox, `${label}.boundingBox`),
|
||||
warnings: expectStringArray(value.warnings, `${label}.warnings`)
|
||||
};
|
||||
}
|
||||
|
||||
function readImageAssetMetadata(value: unknown, label: string): ImageAssetMetadata {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "image",
|
||||
width: expectPositiveFiniteNumber(value.width, `${label}.width`),
|
||||
height: expectPositiveFiniteNumber(value.height, `${label}.height`),
|
||||
hasAlpha: expectBoolean(value.hasAlpha, `${label}.hasAlpha`),
|
||||
warnings: expectStringArray(value.warnings, `${label}.warnings`)
|
||||
};
|
||||
}
|
||||
|
||||
function readAudioAssetMetadata(value: unknown, label: string): AudioAssetMetadata {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "audio",
|
||||
durationSeconds:
|
||||
value.durationSeconds === null
|
||||
? null
|
||||
: expectNonNegativeFiniteNumber(value.durationSeconds, `${label}.durationSeconds`),
|
||||
channelCount:
|
||||
value.channelCount === null ? null : expectPositiveFiniteNumber(value.channelCount, `${label}.channelCount`),
|
||||
sampleRateHz:
|
||||
value.sampleRateHz === null ? null : expectPositiveFiniteNumber(value.sampleRateHz, `${label}.sampleRateHz`),
|
||||
warnings: expectStringArray(value.warnings, `${label}.warnings`)
|
||||
};
|
||||
}
|
||||
|
||||
function readProjectAsset(value: unknown, label: string): ProjectAssetRecord {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
const kind = value.kind;
|
||||
|
||||
if (!isProjectAssetKind(kind)) {
|
||||
throw new Error(`${label}.kind must be model, image, or audio.`);
|
||||
}
|
||||
|
||||
const id = expectString(value.id, `${label}.id`);
|
||||
const sourceName = expectString(value.sourceName, `${label}.sourceName`);
|
||||
const mimeType = expectString(value.mimeType, `${label}.mimeType`);
|
||||
const storageKey = expectString(value.storageKey, `${label}.storageKey`);
|
||||
const byteLength = expectPositiveFiniteNumber(value.byteLength, `${label}.byteLength`);
|
||||
|
||||
switch (kind) {
|
||||
case "model":
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
sourceName,
|
||||
mimeType,
|
||||
storageKey,
|
||||
byteLength,
|
||||
metadata: readModelAssetMetadata(value.metadata, `${label}.metadata`)
|
||||
};
|
||||
case "image":
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
sourceName,
|
||||
mimeType,
|
||||
storageKey,
|
||||
byteLength,
|
||||
metadata: readImageAssetMetadata(value.metadata, `${label}.metadata`)
|
||||
};
|
||||
case "audio":
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
sourceName,
|
||||
mimeType,
|
||||
storageKey,
|
||||
byteLength,
|
||||
metadata: readAudioAssetMetadata(value.metadata, `${label}.metadata`)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function readAssets(value: unknown): SceneDocument["assets"] {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error("assets must be a record.");
|
||||
}
|
||||
|
||||
const assets: SceneDocument["assets"] = {};
|
||||
|
||||
for (const [assetId, assetValue] of Object.entries(value)) {
|
||||
const asset = readProjectAsset(assetValue, `assets.${assetId}`);
|
||||
|
||||
if (asset.id !== assetId) {
|
||||
throw new Error(`assets.${assetId}.id must match the registry key.`);
|
||||
}
|
||||
|
||||
assets[assetId] = asset;
|
||||
}
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
function readModelInstance(value: unknown, label: string, assets: SceneDocument["assets"]): ModelInstance {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
}
|
||||
|
||||
const assetId = expectString(value.assetId, `${label}.assetId`);
|
||||
const asset = assets[assetId];
|
||||
|
||||
if (asset === undefined) {
|
||||
throw new Error(`${label}.assetId references missing asset ${assetId}.`);
|
||||
}
|
||||
|
||||
if (asset.kind !== "model") {
|
||||
throw new Error(`${label}.assetId must reference a model asset.`);
|
||||
}
|
||||
|
||||
return createModelInstance({
|
||||
id: expectString(value.id, `${label}.id`),
|
||||
assetId,
|
||||
name: normalizeModelInstanceName(expectOptionalString(value.name, `${label}.name`)),
|
||||
position: readVec3(value.position, `${label}.position`),
|
||||
rotationDegrees: readVec3(value.rotationDegrees, `${label}.rotationDegrees`),
|
||||
scale: readVec3(value.scale, `${label}.scale`)
|
||||
});
|
||||
}
|
||||
|
||||
function readModelInstances(value: unknown, assets: SceneDocument["assets"]): SceneDocument["modelInstances"] {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error("modelInstances must be a record.");
|
||||
}
|
||||
|
||||
const modelInstances: SceneDocument["modelInstances"] = {};
|
||||
|
||||
for (const [modelInstanceId, modelInstanceValue] of Object.entries(value)) {
|
||||
const modelInstance = readModelInstance(modelInstanceValue, `modelInstances.${modelInstanceId}`, assets);
|
||||
|
||||
if (modelInstance.id !== modelInstanceId) {
|
||||
throw new Error(`modelInstances.${modelInstanceId}.id must match the registry key.`);
|
||||
}
|
||||
|
||||
modelInstances[modelInstanceId] = modelInstance;
|
||||
}
|
||||
|
||||
return modelInstances;
|
||||
}
|
||||
|
||||
function readVec2(value: unknown, label: string) {
|
||||
if (!isRecord(value)) {
|
||||
throw new Error(`${label} must be an object.`);
|
||||
@@ -756,11 +967,29 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
||||
};
|
||||
}
|
||||
|
||||
if (source.version === MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION) {
|
||||
const materials = readMaterialRegistry(source.materials, "materials");
|
||||
|
||||
return {
|
||||
version: SCENE_DOCUMENT_VERSION,
|
||||
name: expectString(source.name, "name"),
|
||||
world: readWorldSettings(source.world),
|
||||
materials,
|
||||
textures: expectEmptyCollection(source.textures, "textures"),
|
||||
assets: expectEmptyCollection(source.assets, "assets"),
|
||||
brushes: readBrushes(source.brushes, materials, false),
|
||||
modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"),
|
||||
entities: readEntities(source.entities),
|
||||
interactionLinks: readInteractionLinks(source.interactionLinks)
|
||||
};
|
||||
}
|
||||
|
||||
if (source.version !== SCENE_DOCUMENT_VERSION) {
|
||||
throw new Error(`Unsupported scene document version: ${String(source.version)}.`);
|
||||
}
|
||||
|
||||
const materials = readMaterialRegistry(source.materials, "materials");
|
||||
const assets = readAssets(source.assets);
|
||||
|
||||
return {
|
||||
version: SCENE_DOCUMENT_VERSION,
|
||||
@@ -768,9 +997,9 @@ export function migrateSceneDocument(source: unknown): SceneDocument {
|
||||
world: readWorldSettings(source.world),
|
||||
materials,
|
||||
textures: expectEmptyCollection(source.textures, "textures"),
|
||||
assets: expectEmptyCollection(source.assets, "assets"),
|
||||
assets,
|
||||
brushes: readBrushes(source.brushes, materials, false),
|
||||
modelInstances: expectEmptyCollection(source.modelInstances, "modelInstances"),
|
||||
modelInstances: readModelInstances(source.modelInstances, assets),
|
||||
entities: readEntities(source.entities),
|
||||
interactionLinks: readInteractionLinks(source.interactionLinks)
|
||||
};
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
type AudioAssetMetadata,
|
||||
type ImageAssetMetadata,
|
||||
type ModelAssetMetadata,
|
||||
type ModelAssetRecord,
|
||||
type ProjectAssetBoundingBox,
|
||||
type ProjectAssetRecord,
|
||||
type ModelInstance,
|
||||
getProjectAssetKindLabel
|
||||
} from "../assets/project-assets";
|
||||
import {
|
||||
type InteractableEntity,
|
||||
type PlayerStartEntity,
|
||||
@@ -158,6 +168,196 @@ function validateWorldSettings(world: WorldSettings, diagnostics: SceneDiagnosti
|
||||
}
|
||||
}
|
||||
|
||||
function validateProjectAssetBoundingBox(
|
||||
boundingBox: ProjectAssetBoundingBox | null,
|
||||
path: string,
|
||||
diagnostics: SceneDiagnostic[]
|
||||
) {
|
||||
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: ModelAssetMetadata, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
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: ImageAssetMetadata, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
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: AudioAssetMetadata, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
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: ProjectAssetRecord, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
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: ModelInstance, path: string, document: SceneDocument, diagnostics: SceneDiagnostic[]) {
|
||||
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`));
|
||||
}
|
||||
|
||||
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 validatePlayerStartEntity(entity: PlayerStartEntity, path: string, diagnostics: SceneDiagnostic[]) {
|
||||
if (!isFiniteVec3(entity.position)) {
|
||||
diagnostics.push(
|
||||
@@ -440,6 +640,17 @@ export function validateSceneDocument(document: SceneDocument): SceneDocumentVal
|
||||
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}`;
|
||||
|
||||
@@ -475,6 +686,19 @@ export function validateSceneDocument(document: SceneDocument): SceneDocumentVal
|
||||
}
|
||||
}
|
||||
|
||||
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}`;
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { Brush } from "./brushes";
|
||||
import type { ModelInstance } from "../assets/model-instances";
|
||||
import type { ProjectAssetRecord } from "../assets/project-assets";
|
||||
import type { EntityInstance } from "../entities/entity-instances";
|
||||
import type { InteractionLink } from "../interactions/interaction-links";
|
||||
import { cloneMaterialRegistry, createStarterMaterialRegistry, type MaterialDef } from "../materials/starter-material-library";
|
||||
import { createDefaultWorldSettings, type WorldSettings } from "./world-settings";
|
||||
|
||||
export const SCENE_DOCUMENT_VERSION = 9 as const;
|
||||
export const SCENE_DOCUMENT_VERSION = 10 as const;
|
||||
export const MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION = 9 as const;
|
||||
export const FOUNDATION_SCENE_DOCUMENT_VERSION = 1 as const;
|
||||
export const BOX_BRUSH_SCENE_DOCUMENT_VERSION = 2 as const;
|
||||
export const FACE_MATERIALS_SCENE_DOCUMENT_VERSION = 3 as const;
|
||||
@@ -20,9 +23,9 @@ export interface SceneDocument {
|
||||
world: WorldSettings;
|
||||
materials: Record<string, MaterialDef>;
|
||||
textures: Record<string, never>;
|
||||
assets: Record<string, never>;
|
||||
assets: Record<string, ProjectAssetRecord>;
|
||||
brushes: Record<string, Brush>;
|
||||
modelInstances: Record<string, never>;
|
||||
modelInstances: Record<string, ModelInstance>;
|
||||
entities: Record<string, EntityInstance>;
|
||||
interactionLinks: Record<string, InteractionLink>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user