@fps-games/engine 0.0.1-beta.5 → 0.0.1-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-resource.d.ts +39 -0
- package/dist/asset-resource.d.ts.map +1 -0
- package/dist/asset-resource.js +121 -0
- package/dist/asset-resource.js.map +1 -0
- package/dist/babylon/game-world-plugin-lifecycle.d.ts +26 -0
- package/dist/babylon/game-world-plugin-lifecycle.d.ts.map +1 -0
- package/dist/babylon/game-world-plugin-lifecycle.js +257 -0
- package/dist/babylon/game-world-plugin-lifecycle.js.map +1 -0
- package/dist/babylon/plugin-contract.d.ts +15 -0
- package/dist/babylon/plugin-contract.d.ts.map +1 -0
- package/dist/babylon/plugin-contract.js +9 -0
- package/dist/babylon/plugin-contract.js.map +1 -0
- package/dist/babylon/renderer/brdf-texture-settlement.d.ts +7 -0
- package/dist/babylon/renderer/brdf-texture-settlement.d.ts.map +1 -0
- package/dist/babylon/renderer/brdf-texture-settlement.js +29 -0
- package/dist/babylon/renderer/brdf-texture-settlement.js.map +1 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.d.ts +7 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.js +357 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.js.map +1 -0
- package/dist/babylon/renderer/environment/index.d.ts +4 -0
- package/dist/babylon/renderer/environment/index.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/index.js +4 -0
- package/dist/babylon/renderer/environment/index.js.map +1 -0
- package/dist/babylon/renderer/environment/profile.d.ts +3 -0
- package/dist/babylon/renderer/environment/profile.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/profile.js +27 -0
- package/dist/babylon/renderer/environment/profile.js.map +1 -0
- package/dist/babylon/renderer/environment/types.d.ts +24 -0
- package/dist/babylon/renderer/environment/types.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/types.js +6 -0
- package/dist/babylon/renderer/environment/types.js.map +1 -0
- package/dist/babylon/renderer/index.d.ts +24 -0
- package/dist/babylon/renderer/index.d.ts.map +1 -0
- package/dist/babylon/renderer/index.js +24 -0
- package/dist/babylon/renderer/index.js.map +1 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.d.ts +17 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.d.ts.map +1 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.js +67 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.js.map +1 -0
- package/dist/babylon/renderer/material-owner-candidates.d.ts +19 -0
- package/dist/babylon/renderer/material-owner-candidates.d.ts.map +1 -0
- package/dist/babylon/renderer/material-owner-candidates.js +212 -0
- package/dist/babylon/renderer/material-owner-candidates.js.map +1 -0
- package/dist/babylon/renderer/materials/index.d.ts +3 -0
- package/dist/babylon/renderer/materials/index.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/index.js +3 -0
- package/dist/babylon/renderer/materials/index.js.map +1 -0
- package/dist/babylon/renderer/materials/material-runtime.d.ts +29 -0
- package/dist/babylon/renderer/materials/material-runtime.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/material-runtime.js +660 -0
- package/dist/babylon/renderer/materials/material-runtime.js.map +1 -0
- package/dist/babylon/renderer/materials/material-slot-owner.d.ts +42 -0
- package/dist/babylon/renderer/materials/material-slot-owner.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/material-slot-owner.js +318 -0
- package/dist/babylon/renderer/materials/material-slot-owner.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts +4 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.js +45 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.d.ts +3 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.js +2 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.d.ts +7 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.js +2 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.js.map +1 -0
- package/dist/babylon/renderer/outlines/index.d.ts +4 -0
- package/dist/babylon/renderer/outlines/index.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/index.js +3 -0
- package/dist/babylon/renderer/outlines/index.js.map +1 -0
- package/dist/babylon/renderer/outlines/runtime-override.d.ts +36 -0
- package/dist/babylon/renderer/outlines/runtime-override.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/runtime-override.js +131 -0
- package/dist/babylon/renderer/outlines/runtime-override.js.map +1 -0
- package/dist/babylon/renderer/outlines/types.d.ts +7 -0
- package/dist/babylon/renderer/outlines/types.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/types.js +2 -0
- package/dist/babylon/renderer/outlines/types.js.map +1 -0
- package/dist/babylon/renderer/placement/index.d.ts +2 -0
- package/dist/babylon/renderer/placement/index.d.ts.map +1 -0
- package/dist/babylon/renderer/placement/index.js +2 -0
- package/dist/babylon/renderer/placement/index.js.map +1 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.d.ts +24 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.d.ts.map +1 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.js +63 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.js.map +1 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts +7 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.js +301 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.js.map +1 -0
- package/dist/babylon/renderer/post-process/index.d.ts +5 -0
- package/dist/babylon/renderer/post-process/index.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/index.js +5 -0
- package/dist/babylon/renderer/post-process/index.js.map +1 -0
- package/dist/babylon/renderer/post-process/profile.d.ts +5 -0
- package/dist/babylon/renderer/post-process/profile.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/profile.js +155 -0
- package/dist/babylon/renderer/post-process/profile.js.map +1 -0
- package/dist/babylon/renderer/post-process/types.d.ts +81 -0
- package/dist/babylon/renderer/post-process/types.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/types.js +24 -0
- package/dist/babylon/renderer/post-process/types.js.map +1 -0
- package/dist/babylon/renderer/post-process/volume-stack.d.ts +4 -0
- package/dist/babylon/renderer/post-process/volume-stack.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/volume-stack.js +105 -0
- package/dist/babylon/renderer/post-process/volume-stack.js.map +1 -0
- package/dist/babylon/renderer/rendering/image-node.d.ts +35 -0
- package/dist/babylon/renderer/rendering/image-node.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/image-node.js +39 -0
- package/dist/babylon/renderer/rendering/image-node.js.map +1 -0
- package/dist/babylon/renderer/rendering/index.d.ts +3 -0
- package/dist/babylon/renderer/rendering/index.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/index.js +3 -0
- package/dist/babylon/renderer/rendering/index.js.map +1 -0
- package/dist/babylon/renderer/rendering/node-rendering.d.ts +19 -0
- package/dist/babylon/renderer/rendering/node-rendering.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/node-rendering.js +66 -0
- package/dist/babylon/renderer/rendering/node-rendering.js.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts +5 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.js +46 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts +5 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.js +402 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts +84 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.js +566 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts +7 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.js +316 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts +5 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.js +1562 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts +14 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.js +14 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts +68 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js +829 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts +148 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js +4529 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts +4 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js +645 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts +61 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js +382 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts +47 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js +285 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts +88 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js +830 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts +3 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.js +60 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts +27 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js +220 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js.map +1 -0
- package/dist/babylon/renderer/shadows/types.d.ts +421 -0
- package/dist/babylon/renderer/shadows/types.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/types.js +127 -0
- package/dist/babylon/renderer/shadows/types.js.map +1 -0
- package/dist/babylon/renderer-controller.d.ts +93 -0
- package/dist/babylon/renderer-controller.d.ts.map +1 -0
- package/dist/babylon/renderer-controller.js +706 -0
- package/dist/babylon/renderer-controller.js.map +1 -0
- package/dist/babylon.d.ts +4 -0
- package/dist/babylon.d.ts.map +1 -1
- package/dist/babylon.js +4 -0
- package/dist/babylon.js.map +1 -1
- package/dist/build-info.json +4 -4
- package/dist/compiled-scene-data.d.ts +23 -0
- package/dist/compiled-scene-data.d.ts.map +1 -0
- package/dist/compiled-scene-data.js +100 -0
- package/dist/compiled-scene-data.js.map +1 -0
- package/dist/ground-decal.d.ts +22 -0
- package/dist/ground-decal.d.ts.map +1 -0
- package/dist/ground-decal.js +48 -0
- package/dist/ground-decal.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/lighting.d.ts.map +1 -1
- package/dist/lighting.js +5 -1
- package/dist/lighting.js.map +1 -1
- package/dist/material-owner.d.ts +128 -0
- package/dist/material-owner.d.ts.map +1 -0
- package/dist/material-owner.js +434 -0
- package/dist/material-owner.js.map +1 -0
- package/dist/material-profile.d.ts +62 -0
- package/dist/material-profile.d.ts.map +1 -0
- package/dist/material-profile.js +132 -0
- package/dist/material-profile.js.map +1 -0
- package/dist/rendering/index.d.ts +14 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +12 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/rendering-profile-types.d.ts +322 -0
- package/dist/rendering/rendering-profile-types.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-types.js +2 -0
- package/dist/rendering/rendering-profile-types.js.map +1 -0
- package/dist/rendering/rendering-profile-validation.d.ts +2 -0
- package/dist/rendering/rendering-profile-validation.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-validation.js +364 -0
- package/dist/rendering/rendering-profile-validation.js.map +1 -0
- package/dist/rendering/rendering-profile-values.d.ts +19 -0
- package/dist/rendering/rendering-profile-values.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-values.js +79 -0
- package/dist/rendering/rendering-profile-values.js.map +1 -0
- package/dist/rendering/rendering-profile.d.ts +15 -0
- package/dist/rendering/rendering-profile.d.ts.map +1 -0
- package/dist/rendering/rendering-profile.js +442 -0
- package/dist/rendering/rendering-profile.js.map +1 -0
- package/dist/rendering/shadow-map-experiment-depth.d.ts +63 -0
- package/dist/rendering/shadow-map-experiment-depth.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment-depth.js +186 -0
- package/dist/rendering/shadow-map-experiment-depth.js.map +1 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.d.ts +94 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.js +407 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.js.map +1 -0
- package/dist/rendering/shadow-map-experiment.d.ts +189 -0
- package/dist/rendering/shadow-map-experiment.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment.js +549 -0
- package/dist/rendering/shadow-map-experiment.js.map +1 -0
- package/dist/rendering/shadow-quality.d.ts +5 -0
- package/dist/rendering/shadow-quality.d.ts.map +1 -0
- package/dist/rendering/shadow-quality.js +83 -0
- package/dist/rendering/shadow-quality.js.map +1 -0
- package/dist/rendering/shadow-runtime.d.ts +59 -0
- package/dist/rendering/shadow-runtime.d.ts.map +1 -0
- package/dist/rendering/shadow-runtime.js +378 -0
- package/dist/rendering/shadow-runtime.js.map +1 -0
- package/dist/rendering/shadow-settings.d.ts +15 -0
- package/dist/rendering/shadow-settings.d.ts.map +1 -0
- package/dist/rendering/shadow-settings.js +129 -0
- package/dist/rendering/shadow-settings.js.map +1 -0
- package/dist/rendering/shadow-types.d.ts +99 -0
- package/dist/rendering/shadow-types.d.ts.map +1 -0
- package/dist/rendering/shadow-types.js +5 -0
- package/dist/rendering/shadow-types.js.map +1 -0
- package/dist/rendering/shadow-validation.d.ts +3 -0
- package/dist/rendering/shadow-validation.d.ts.map +1 -0
- package/dist/rendering/shadow-validation.js +166 -0
- package/dist/rendering/shadow-validation.js.map +1 -0
- package/dist/rendering/value.d.ts +2 -0
- package/dist/rendering/value.d.ts.map +1 -0
- package/dist/rendering/value.js +6 -0
- package/dist/rendering/value.js.map +1 -0
- package/dist/runtime-data-token.d.ts +4 -0
- package/dist/runtime-data-token.d.ts.map +1 -0
- package/dist/runtime-data-token.js +4 -0
- package/dist/runtime-data-token.js.map +1 -0
- package/dist/runtime-data-types.d.ts +83 -0
- package/dist/runtime-data-types.d.ts.map +1 -0
- package/dist/runtime-data-types.js +2 -0
- package/dist/runtime-data-types.js.map +1 -0
- package/dist/runtime-data.d.ts +18 -0
- package/dist/runtime-data.d.ts.map +1 -0
- package/dist/runtime-data.js +301 -0
- package/dist/runtime-data.js.map +1 -0
- package/dist/runtime-scene-resolvers.d.ts +37 -0
- package/dist/runtime-scene-resolvers.d.ts.map +1 -0
- package/dist/runtime-scene-resolvers.js +40 -0
- package/dist/runtime-scene-resolvers.js.map +1 -0
- package/dist/scene-artifact-types.d.ts +383 -0
- package/dist/scene-artifact-types.d.ts.map +1 -0
- package/dist/scene-artifact-types.js +2 -0
- package/dist/scene-artifact-types.js.map +1 -0
- package/dist/scene-artifact-validation.d.ts +19 -0
- package/dist/scene-artifact-validation.d.ts.map +1 -0
- package/dist/scene-artifact-validation.js +1352 -0
- package/dist/scene-artifact-validation.js.map +1 -0
- package/dist/static-shadow-artifact.d.ts +131 -0
- package/dist/static-shadow-artifact.d.ts.map +1 -0
- package/dist/static-shadow-artifact.js +327 -0
- package/dist/static-shadow-artifact.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
import { FPS_RUNTIME_LIGHT_COMPONENT_SCHEMA_VERSION, createFpsRuntimeLightSnapshot, normalizeFpsRuntimeLightDirection, } from './lighting.js';
|
|
2
|
+
import { normalizeSceneMaterialShaderId } from './material-profile.js';
|
|
3
|
+
import { assertEditorShadowSettingsContract } from './rendering/shadow-validation.js';
|
|
4
|
+
import { assertEditorSceneStaticShadowArtifactContract } from './static-shadow-artifact.js';
|
|
5
|
+
export const PLAYABLE_EDITOR_SCENE_COMPILER_ID = '@fps-games/editor.playable-sdk.scene-compiler';
|
|
6
|
+
const PLAYABLE_EDITOR_LEGACY_UNVERSIONED_LIGHT_COMPILER_VERSION = '2';
|
|
7
|
+
const LEGACY_LIGHT_DIRECTION_UNIT_TOLERANCE = 0.0001;
|
|
8
|
+
const PLAYABLE_EDITOR_RUNTIME_SCENE_RULES = {
|
|
9
|
+
nodeKinds: ['group', 'instance', 'transform', 'primitive', 'image'],
|
|
10
|
+
primitiveShapes: ['cube', 'sphere', 'plane', 'capsule'],
|
|
11
|
+
transformTypes: ['plain', 'light', 'camera', 'groundDecal'],
|
|
12
|
+
assetTypes: ['glb', 'texture'],
|
|
13
|
+
materialScopes: ['sharedAsset', 'nodeMaterial'],
|
|
14
|
+
materialModes: ['shared', 'instance'],
|
|
15
|
+
materialShaderIds: ['fps.builtin.pbr', 'fps.builtin.standard'],
|
|
16
|
+
materialAssetSystemPresets: ['default-pbr', 'default-standard'],
|
|
17
|
+
runtimeV3: {
|
|
18
|
+
assetFields: [
|
|
19
|
+
'id', 'guid', 'type', 'url', 'external', 'warmupCount', 'singleton',
|
|
20
|
+
'materialMode', 'defaults', 'materialSlots',
|
|
21
|
+
],
|
|
22
|
+
materialSlotFields: [
|
|
23
|
+
'slotId', 'label', 'sourceSurfaces',
|
|
24
|
+
],
|
|
25
|
+
materialSlotSourceSurfaceFields: [
|
|
26
|
+
'ownerNodePath', 'nodeIndex', 'nodeIndexPath', 'meshIndex', 'primitiveIndex',
|
|
27
|
+
'sourceMaterialIndex', 'sourceMeshName',
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
hierarchy: {
|
|
31
|
+
parentTargets: ['root', 'sceneNode'],
|
|
32
|
+
},
|
|
33
|
+
runtimeOnlyTokens: ['/@fs', 'assetUrl', 'sourceAssetUrl', 'sourceAssetPath'],
|
|
34
|
+
};
|
|
35
|
+
const NODE_KINDS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.nodeKinds);
|
|
36
|
+
const PRIMITIVE_SHAPES = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.primitiveShapes);
|
|
37
|
+
const ASSET_TYPES = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.assetTypes);
|
|
38
|
+
const MATERIAL_MODES = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.materialModes);
|
|
39
|
+
const MATERIAL_SCOPES = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.materialScopes);
|
|
40
|
+
const MATERIAL_ASSET_SYSTEM_PRESETS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.materialAssetSystemPresets);
|
|
41
|
+
const TRANSFORM_TYPES = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.transformTypes);
|
|
42
|
+
const HIERARCHY_PARENT_TARGETS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.hierarchy.parentTargets);
|
|
43
|
+
const RUNTIME_ONLY_TOKENS = PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.runtimeOnlyTokens;
|
|
44
|
+
const RUNTIME_V3_ASSET_FIELDS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.runtimeV3.assetFields);
|
|
45
|
+
const RUNTIME_V3_MATERIAL_SLOT_FIELDS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.runtimeV3.materialSlotFields);
|
|
46
|
+
const RUNTIME_V3_MATERIAL_SLOT_SOURCE_SURFACE_FIELDS = new Set(PLAYABLE_EDITOR_RUNTIME_SCENE_RULES.runtimeV3.materialSlotSourceSurfaceFields);
|
|
47
|
+
function collectPlayableEditorRuntimeSceneValidationErrors(sceneConfig, options = {}) {
|
|
48
|
+
const { allowOrphanSharedMaterials = true, allowOrphanNodeMaterials = true, strictAssetIds = [], strictNodeIds = [], maxErrors = 50, } = options;
|
|
49
|
+
const errorLimit = Number.isInteger(maxErrors) && maxErrors > 0 ? maxErrors : 50;
|
|
50
|
+
const errors = [];
|
|
51
|
+
const strictAssets = new Set(strictAssetIds);
|
|
52
|
+
const strictNodes = new Set(strictNodeIds);
|
|
53
|
+
const add = (path, message) => {
|
|
54
|
+
if (errors.length < errorLimit)
|
|
55
|
+
errors.push({ path, message });
|
|
56
|
+
};
|
|
57
|
+
if (!isRecord(sceneConfig)) {
|
|
58
|
+
add('$', 'scene json must be an object');
|
|
59
|
+
return errors;
|
|
60
|
+
}
|
|
61
|
+
const schemaVersion = sceneConfig.schemaVersion;
|
|
62
|
+
if (schemaVersion !== 2 && schemaVersion !== 3)
|
|
63
|
+
add('$.schemaVersion', 'schemaVersion must be 2 or 3');
|
|
64
|
+
validateGeneratedFrom(sceneConfig.meta?.generatedFrom, '$.meta.generatedFrom', add);
|
|
65
|
+
const allowLegacyUnversionedLights = isLegacyUnversionedLightArtifact(sceneConfig);
|
|
66
|
+
const scene = sceneConfig.scene;
|
|
67
|
+
if (!isRecord(scene)) {
|
|
68
|
+
add('$.scene', 'scene must be an object');
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
if (!nonEmptyString(scene.rootId))
|
|
72
|
+
add('$.scene.rootId', 'rootId must be a non-empty string');
|
|
73
|
+
if (!Array.isArray(scene.assets))
|
|
74
|
+
add('$.scene.assets', 'assets must be an array');
|
|
75
|
+
if (!Array.isArray(scene.nodes))
|
|
76
|
+
add('$.scene.nodes', 'nodes must be an array');
|
|
77
|
+
if (!Array.isArray(scene.materialAssets))
|
|
78
|
+
add('$.scene.materialAssets', 'materialAssets must be an array');
|
|
79
|
+
if (!Array.isArray(scene.materials))
|
|
80
|
+
add('$.scene.materials', 'materials must be an array');
|
|
81
|
+
if (!Array.isArray(scene.textures))
|
|
82
|
+
add('$.scene.textures', 'textures must be an array');
|
|
83
|
+
if (errors.length > 0)
|
|
84
|
+
return errors;
|
|
85
|
+
if (sceneConfig.staticShadows != null) {
|
|
86
|
+
captureSchemaError(() => assertEditorSceneStaticShadowArtifactContract(sceneConfig.staticShadows, '$.staticShadows'), '$.staticShadows', add);
|
|
87
|
+
}
|
|
88
|
+
const assetIds = new Set();
|
|
89
|
+
const assetTypesById = new Map();
|
|
90
|
+
const materialSlotIdsByAssetId = new Map();
|
|
91
|
+
const materialAssetIds = new Set();
|
|
92
|
+
const nodeIds = new Set([scene.rootId]);
|
|
93
|
+
const nodesById = new Map();
|
|
94
|
+
const nodeKinds = new Map();
|
|
95
|
+
const markerNodeIds = new Set();
|
|
96
|
+
scene.assets.forEach((asset, index) => {
|
|
97
|
+
const path = `$.scene.assets[${index}]`;
|
|
98
|
+
if (!isRecord(asset)) {
|
|
99
|
+
add(path, 'asset must be an object');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!nonEmptyString(asset.id))
|
|
103
|
+
add(`${path}.id`, 'asset.id must be a non-empty string');
|
|
104
|
+
else if (assetIds.has(asset.id))
|
|
105
|
+
add(`${path}.id`, `duplicate asset id: ${asset.id}`);
|
|
106
|
+
else {
|
|
107
|
+
assetIds.add(asset.id);
|
|
108
|
+
assetTypesById.set(asset.id, asset.type);
|
|
109
|
+
}
|
|
110
|
+
if (!ASSET_TYPES.has(asset.type))
|
|
111
|
+
add(`${path}.type`, 'asset.type must be glb or texture');
|
|
112
|
+
if (asset.guid != null && !nonEmptyString(asset.guid))
|
|
113
|
+
add(`${path}.guid`, 'asset.guid must be a non-empty string when present');
|
|
114
|
+
if (asset.url != null && !nonEmptyString(asset.url))
|
|
115
|
+
add(`${path}.url`, 'url must be a non-empty string when present');
|
|
116
|
+
if (asset.external != null && !isRecord(asset.external))
|
|
117
|
+
add(`${path}.external`, 'external must be an object when present');
|
|
118
|
+
if (asset.warmupCount != null && (!Number.isInteger(asset.warmupCount) || asset.warmupCount < 0)) {
|
|
119
|
+
add(`${path}.warmupCount`, 'warmupCount must be a non-negative integer when present');
|
|
120
|
+
}
|
|
121
|
+
if (asset.singleton != null && typeof asset.singleton !== 'boolean') {
|
|
122
|
+
add(`${path}.singleton`, 'singleton must be a boolean when present');
|
|
123
|
+
}
|
|
124
|
+
if (asset.materialMode != null && !MATERIAL_MODES.has(asset.materialMode)) {
|
|
125
|
+
add(`${path}.materialMode`, 'materialMode must be shared or instance');
|
|
126
|
+
}
|
|
127
|
+
validateAssetDefaults(asset.defaults, `${path}.defaults`, add);
|
|
128
|
+
if (schemaVersion === 3) {
|
|
129
|
+
for (const field of Object.keys(asset)) {
|
|
130
|
+
if (!RUNTIME_V3_ASSET_FIELDS.has(field)) {
|
|
131
|
+
add(`${path}.${field}`, `runtime schema v3 asset field is not allowed: ${field}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
if (asset.displayName != null && !nonEmptyString(asset.displayName))
|
|
137
|
+
add(`${path}.displayName`, 'displayName must be a non-empty string when present');
|
|
138
|
+
if (asset.category != null && !nonEmptyString(asset.category))
|
|
139
|
+
add(`${path}.category`, 'category must be a non-empty string when present');
|
|
140
|
+
if (asset.metadata != null && !isRecord(asset.metadata))
|
|
141
|
+
add(`${path}.metadata`, 'metadata must be an object when present');
|
|
142
|
+
}
|
|
143
|
+
const materialSlots = schemaVersion === 3
|
|
144
|
+
? asset.materialSlots
|
|
145
|
+
: isRecord(asset.metadata) ? asset.metadata.materialSlots : undefined;
|
|
146
|
+
const materialSlotsPath = schemaVersion === 3 ? `${path}.materialSlots` : `${path}.metadata.materialSlots`;
|
|
147
|
+
const slotIds = new Set();
|
|
148
|
+
if (materialSlots != null) {
|
|
149
|
+
if (!Array.isArray(materialSlots))
|
|
150
|
+
add(materialSlotsPath, 'materialSlots must be an array when present');
|
|
151
|
+
else
|
|
152
|
+
materialSlots.forEach((rawSlot, slotIndex) => {
|
|
153
|
+
const slotPath = `${materialSlotsPath}[${slotIndex}]`;
|
|
154
|
+
if (!isRecord(rawSlot) || !nonEmptyString(rawSlot.slotId)) {
|
|
155
|
+
add(slotPath, schemaVersion === 3
|
|
156
|
+
? 'material slot must contain a stable slotId'
|
|
157
|
+
: 'material slot must contain stable slotId and ownerNodePath');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (schemaVersion !== 3 && !nonEmptyString(rawSlot.ownerNodePath)) {
|
|
161
|
+
add(slotPath, 'material slot must contain stable slotId and ownerNodePath');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (slotIds.has(rawSlot.slotId))
|
|
165
|
+
add(`${slotPath}.slotId`, `duplicate material slot id: ${rawSlot.slotId}`);
|
|
166
|
+
else
|
|
167
|
+
slotIds.add(rawSlot.slotId);
|
|
168
|
+
if (schemaVersion === 3)
|
|
169
|
+
validateRuntimeV3MaterialSlot(rawSlot, slotPath, add);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (nonEmptyString(asset.id))
|
|
173
|
+
materialSlotIdsByAssetId.set(asset.id, slotIds);
|
|
174
|
+
if (strictAssets.has(asset.id))
|
|
175
|
+
assertNoRuntimeOnlyFields(asset, path, add);
|
|
176
|
+
});
|
|
177
|
+
scene.nodes.forEach((node, index) => {
|
|
178
|
+
const path = `$.scene.nodes[${index}]`;
|
|
179
|
+
if (!isRecord(node)) {
|
|
180
|
+
add(path, 'node must be an object');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (nonEmptyString(node.id))
|
|
184
|
+
nodesById.set(node.id, node);
|
|
185
|
+
if (!nonEmptyString(node.id))
|
|
186
|
+
add(`${path}.id`, 'node.id must be a non-empty string');
|
|
187
|
+
else if (nodeIds.has(node.id))
|
|
188
|
+
add(`${path}.id`, `duplicate node id: ${node.id}`);
|
|
189
|
+
else {
|
|
190
|
+
nodeIds.add(node.id);
|
|
191
|
+
if (typeof node.kind === 'string')
|
|
192
|
+
nodeKinds.set(node.id, node.kind);
|
|
193
|
+
if (node.kind === 'transform' && Object.prototype.hasOwnProperty.call(node, 'marker')) {
|
|
194
|
+
markerNodeIds.add(node.id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!NODE_KINDS.has(node.kind))
|
|
198
|
+
add(`${path}.kind`, 'node.kind must be group, instance, transform, primitive, or image');
|
|
199
|
+
});
|
|
200
|
+
const materialAssets = scene.materialAssets;
|
|
201
|
+
materialAssets.forEach((materialAsset, index) => {
|
|
202
|
+
const path = `$.scene.materialAssets[${index}]`;
|
|
203
|
+
if (!isRecord(materialAsset)) {
|
|
204
|
+
add(path, 'material asset must be an object');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!nonEmptyString(materialAsset.id))
|
|
208
|
+
add(`${path}.id`, 'material asset id must be a non-empty string');
|
|
209
|
+
else if (materialAssetIds.has(materialAsset.id))
|
|
210
|
+
add(`${path}.id`, `duplicate material asset id: ${materialAsset.id}`);
|
|
211
|
+
else
|
|
212
|
+
materialAssetIds.add(materialAsset.id);
|
|
213
|
+
if (materialAsset.name != null && !nonEmptyString(materialAsset.name)) {
|
|
214
|
+
add(`${path}.name`, 'material asset name must be a non-empty string when present');
|
|
215
|
+
}
|
|
216
|
+
if (materialAsset.guid != null && !nonEmptyString(materialAsset.guid))
|
|
217
|
+
add(`${path}.guid`, 'material asset guid must be a non-empty string when present');
|
|
218
|
+
if (Object.prototype.hasOwnProperty.call(materialAsset, 'materialKind')) {
|
|
219
|
+
add(`${path}.materialKind`, 'materialKind is not supported; use shaderId');
|
|
220
|
+
}
|
|
221
|
+
if (Object.prototype.hasOwnProperty.call(materialAsset, 'profile')) {
|
|
222
|
+
add(`${path}.profile`, 'profile is not supported; use properties');
|
|
223
|
+
}
|
|
224
|
+
if (!normalizeSceneMaterialShaderId(materialAsset.shaderId)) {
|
|
225
|
+
add(`${path}.shaderId`, 'shaderId must be fps.builtin.pbr or fps.builtin.standard');
|
|
226
|
+
}
|
|
227
|
+
validateArtistMaterialProfile(materialAsset.properties, `${path}.properties`, add);
|
|
228
|
+
for (const field of Object.keys(materialAsset)) {
|
|
229
|
+
if (!['id', 'guid', 'name', 'shaderId', 'properties', 'system', 'origin'].includes(field)) {
|
|
230
|
+
add(`${path}.${field}`, `material asset field is not allowed: ${field}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
validateMaterialAssetSystem(materialAsset.system, `${path}.system`, add);
|
|
234
|
+
validateMaterialAssetOrigin(materialAsset.origin, `${path}.origin`, add);
|
|
235
|
+
validateMaterialAssetSystemKindConsistency(materialAsset, path, add);
|
|
236
|
+
});
|
|
237
|
+
scene.nodes.forEach((node, index) => {
|
|
238
|
+
if (!isRecord(node))
|
|
239
|
+
return;
|
|
240
|
+
const path = `$.scene.nodes[${index}]`;
|
|
241
|
+
const hasExplicitParent = Object.prototype.hasOwnProperty.call(node, 'parentId');
|
|
242
|
+
if (hasExplicitParent && !nonEmptyString(node.parentId)) {
|
|
243
|
+
add(`${path}.parentId`, 'parentId must be a non-empty string when present');
|
|
244
|
+
}
|
|
245
|
+
const parentId = hasExplicitParent ? node.parentId : scene.rootId;
|
|
246
|
+
if (nonEmptyString(parentId)) {
|
|
247
|
+
validateNodeParentTarget(parentId, scene.rootId, nodeIds, `${path}.parentId`, add);
|
|
248
|
+
}
|
|
249
|
+
if (nonEmptyString(node.id)) {
|
|
250
|
+
validateNodeParentCycle(node.id, scene.rootId, nodesById, `${path}.parentId`, add);
|
|
251
|
+
}
|
|
252
|
+
validateRuntimeSourceBinding(node.source, `${path}.source`, add);
|
|
253
|
+
validateTransform(node.transform, `${path}.transform`, add);
|
|
254
|
+
validateNodeRendering(node.rendering, `${path}.rendering`, add);
|
|
255
|
+
if (node.shadow != null) {
|
|
256
|
+
captureSchemaError(() => assertEditorShadowSettingsContract(node.shadow, `${path}.shadow`), `${path}.shadow`, add);
|
|
257
|
+
}
|
|
258
|
+
if (Object.prototype.hasOwnProperty.call(node, 'shadowMode')) {
|
|
259
|
+
add(`${path}.shadowMode`, 'deprecated field; use shadow.mode');
|
|
260
|
+
}
|
|
261
|
+
let instanceAssetId = null;
|
|
262
|
+
if (node.kind === 'instance') {
|
|
263
|
+
if (!isRecord(node.instance))
|
|
264
|
+
add(`${path}.instance`, 'instance node must contain instance object');
|
|
265
|
+
else if (!assetIds.has(node.instance.assetId)) {
|
|
266
|
+
add(`${path}.instance.assetId`, `assetId must reference scene.assets: ${node.instance.assetId}`);
|
|
267
|
+
}
|
|
268
|
+
else if (assetTypesById.get(node.instance.assetId) !== 'glb') {
|
|
269
|
+
add(`${path}.instance.assetId`, 'instance.assetId must reference a glb asset');
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
instanceAssetId = nonEmptyString(node.instance.assetId) ? node.instance.assetId : null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (node.kind === 'primitive') {
|
|
276
|
+
if (!isRecord(node.primitive))
|
|
277
|
+
add(`${path}.primitive`, 'primitive node must contain primitive object');
|
|
278
|
+
else if (!PRIMITIVE_SHAPES.has(node.primitive.shape)) {
|
|
279
|
+
add(`${path}.primitive.shape`, 'primitive.shape must be cube, sphere, plane, or capsule');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (node.kind === 'image') {
|
|
283
|
+
if (!isRecord(node.image))
|
|
284
|
+
add(`${path}.image`, 'image node must contain image object');
|
|
285
|
+
else {
|
|
286
|
+
if (!assetIds.has(node.image.assetId)) {
|
|
287
|
+
add(`${path}.image.assetId`, `assetId must reference scene.assets: ${node.image.assetId}`);
|
|
288
|
+
}
|
|
289
|
+
else if (assetTypesById.get(node.image.assetId) !== 'texture') {
|
|
290
|
+
add(`${path}.image.assetId`, 'image.assetId must reference a texture asset');
|
|
291
|
+
}
|
|
292
|
+
if (!isRecord(node.image.size))
|
|
293
|
+
add(`${path}.image.size`, 'image.size must be an object');
|
|
294
|
+
else {
|
|
295
|
+
if (!positiveFinite(node.image.size.width))
|
|
296
|
+
add(`${path}.image.size.width`, 'image.size.width must be positive');
|
|
297
|
+
if (!positiveFinite(node.image.size.height))
|
|
298
|
+
add(`${path}.image.size.height`, 'image.size.height must be positive');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (node.kind === 'transform' && node.transformType != null && !TRANSFORM_TYPES.has(node.transformType)) {
|
|
303
|
+
add(`${path}.transformType`, 'transformType must be plain, light, camera, or groundDecal');
|
|
304
|
+
}
|
|
305
|
+
const isGroundDecalTransform = node.kind === 'transform' && node.transformType === 'groundDecal';
|
|
306
|
+
if (isGroundDecalTransform) {
|
|
307
|
+
validateGroundDecal(node.groundDecal, `${path}.groundDecal`, add);
|
|
308
|
+
}
|
|
309
|
+
else if (Object.prototype.hasOwnProperty.call(node, 'groundDecal')) {
|
|
310
|
+
add(`${path}.groundDecal`, 'groundDecal is only allowed on transform nodes with transformType groundDecal');
|
|
311
|
+
}
|
|
312
|
+
if (node.kind === 'transform') {
|
|
313
|
+
validateCameraRig(node.camera, `${path}.camera`, add);
|
|
314
|
+
validateSceneLight(node.light, `${path}.light`, add, allowLegacyUnversionedLights);
|
|
315
|
+
}
|
|
316
|
+
if (Object.prototype.hasOwnProperty.call(node, 'marker')) {
|
|
317
|
+
if (node.kind !== 'transform')
|
|
318
|
+
add(`${path}.marker`, 'marker is only allowed on transform nodes');
|
|
319
|
+
else {
|
|
320
|
+
validateSceneMarker(node.marker, `${path}.marker`, add);
|
|
321
|
+
validateSceneMarkerReferences(node.marker, node.id, nodeIds, markerNodeIds, `${path}.marker`, add);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (node.kind === 'instance' || node.kind === 'transform' || node.kind === 'primitive' || node.kind === 'image') {
|
|
325
|
+
validateNodeVisualOverrides(node.overrides, `${path}.overrides`, materialAssetIds, instanceAssetId ? materialSlotIdsByAssetId.get(instanceAssetId) : undefined, schemaVersion === 3 ? 'materialSlots' : 'metadata.materialSlots', add);
|
|
326
|
+
}
|
|
327
|
+
if (strictNodes.has(node.id))
|
|
328
|
+
assertNoRuntimeOnlyFields(node, path, add);
|
|
329
|
+
});
|
|
330
|
+
scene.materials.forEach((material, index) => {
|
|
331
|
+
const path = `$.scene.materials[${index}]`;
|
|
332
|
+
if (!isRecord(material)) {
|
|
333
|
+
add(path, 'material must be an object');
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (!nonEmptyString(material.id))
|
|
337
|
+
add(`${path}.id`, 'material.id must be a non-empty string');
|
|
338
|
+
if (material.scope != null && !MATERIAL_SCOPES.has(material.scope)) {
|
|
339
|
+
add(`${path}.scope`, 'material scope must be sharedAsset or nodeMaterial');
|
|
340
|
+
}
|
|
341
|
+
if (material.scope === 'sharedAsset' && (!nonEmptyString(material.assetId) || !assetIds.has(material.assetId)) && !allowOrphanSharedMaterials) {
|
|
342
|
+
add(`${path}.assetId`, `shared material assetId must reference scene.assets: ${material.assetId}`);
|
|
343
|
+
}
|
|
344
|
+
if (material.scope === 'nodeMaterial') {
|
|
345
|
+
if ((!nonEmptyString(material.nodeId) || !nodeIds.has(material.nodeId)) && !allowOrphanNodeMaterials) {
|
|
346
|
+
add(`${path}.nodeId`, `node material nodeId must reference scene.nodes: ${material.nodeId}`);
|
|
347
|
+
}
|
|
348
|
+
else if (nonEmptyString(material.nodeId) && nodeIds.has(material.nodeId)) {
|
|
349
|
+
const nodeKind = nodeKinds.get(material.nodeId);
|
|
350
|
+
if (nodeKind !== 'instance' && nodeKind !== 'transform' && nodeKind !== 'primitive') {
|
|
351
|
+
add(`${path}.nodeId`, `node material nodeId must reference an instance, transform, or primitive node: ${material.nodeId}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!nonEmptyString(material.materialName))
|
|
356
|
+
add(`${path}.materialName`, 'materialName must be a non-empty string');
|
|
357
|
+
if (!isRecord(material.properties))
|
|
358
|
+
add(`${path}.properties`, 'properties must be an object');
|
|
359
|
+
else
|
|
360
|
+
validateMaterialProperties(material.properties, `${path}.properties`, add);
|
|
361
|
+
});
|
|
362
|
+
return errors;
|
|
363
|
+
}
|
|
364
|
+
export function validatePlayableEditorRuntimeSceneConfig(value, options = {}) {
|
|
365
|
+
const errors = collectPlayableEditorRuntimeSceneValidationErrors(value, options);
|
|
366
|
+
return {
|
|
367
|
+
ok: errors.length === 0,
|
|
368
|
+
errors,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function captureSchemaError(operation, fallbackPath, add) {
|
|
372
|
+
try {
|
|
373
|
+
operation();
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const path = typeof error?.path === 'string'
|
|
377
|
+
? error.path
|
|
378
|
+
: fallbackPath;
|
|
379
|
+
const code = typeof error?.code === 'string'
|
|
380
|
+
? error.code
|
|
381
|
+
: 'schema.invalidValue';
|
|
382
|
+
add(path, code);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function validateNodeVisualOverrides(overrides, path, materialAssetIds, materialSlotIds, materialSlotSource, add) {
|
|
386
|
+
if (overrides == null)
|
|
387
|
+
return;
|
|
388
|
+
if (!isRecord(overrides)) {
|
|
389
|
+
add(path, 'overrides must be an object when present');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
validateNodeMaterialBinding(overrides.materialBinding, `${path}.materialBinding`, materialAssetIds, add);
|
|
393
|
+
validateNodeMaterialBindingMap(overrides.materialSlotBindings, `${path}.materialSlotBindings`, 'slotId', materialAssetIds, materialSlotIds, materialSlotSource, add);
|
|
394
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'childMaterialBindings')) {
|
|
395
|
+
add(`${path}.childMaterialBindings`, 'deprecated field; use materialSlotBindings keyed by slotId');
|
|
396
|
+
}
|
|
397
|
+
if (Object.prototype.hasOwnProperty.call(overrides, 'childMaterials')) {
|
|
398
|
+
add(`${path}.childMaterials`, 'deprecated field; use materialSlotBindings keyed by slotId');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function validateNodeMaterialBindingMap(bindings, path, keyName, materialAssetIds, allowedKeys, materialSlotSource, add) {
|
|
402
|
+
if (bindings == null)
|
|
403
|
+
return;
|
|
404
|
+
if (!isRecord(bindings)) {
|
|
405
|
+
add(path, 'material binding map must be an object when present');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
for (const [key, binding] of Object.entries(bindings)) {
|
|
409
|
+
const bindingPath = `${path}.${key}`;
|
|
410
|
+
if (!nonEmptyString(key))
|
|
411
|
+
add(bindingPath, `${keyName} must be a non-empty string`);
|
|
412
|
+
if (!allowedKeys) {
|
|
413
|
+
add(bindingPath, `${keyName} requires an instance asset with ${materialSlotSource}`);
|
|
414
|
+
}
|
|
415
|
+
else if (!allowedKeys.has(key)) {
|
|
416
|
+
add(bindingPath, `${keyName} must reference asset ${materialSlotSource}`);
|
|
417
|
+
}
|
|
418
|
+
if (binding == null)
|
|
419
|
+
add(bindingPath, 'material binding must be an object');
|
|
420
|
+
else
|
|
421
|
+
validateNodeMaterialBinding(binding, bindingPath, materialAssetIds, add);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function validateNodeMaterialBinding(binding, path, materialAssetIds, add) {
|
|
425
|
+
if (binding == null)
|
|
426
|
+
return;
|
|
427
|
+
if (!isRecord(binding)) {
|
|
428
|
+
add(path, 'material binding must be an object when present');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (binding.materialAssetId != null) {
|
|
432
|
+
if (!nonEmptyString(binding.materialAssetId)) {
|
|
433
|
+
add(`${path}.materialAssetId`, 'materialAssetId must be non-empty when present');
|
|
434
|
+
}
|
|
435
|
+
else if (!materialAssetIds.has(binding.materialAssetId)) {
|
|
436
|
+
add(`${path}.materialAssetId`, `materialAssetId must reference scene.materialAssets: ${binding.materialAssetId}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (binding.override != null)
|
|
440
|
+
validateArtistMaterialProfile(binding.override, `${path}.override`, add);
|
|
441
|
+
}
|
|
442
|
+
function validateArtistMaterialProfile(profile, path, add) {
|
|
443
|
+
if (!isRecord(profile)) {
|
|
444
|
+
add(path, 'material asset profile must be an object');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (profile.lightingModel != null && profile.lightingModel !== 'lit' && profile.lightingModel !== 'unlit') {
|
|
448
|
+
add(`${path}.lightingModel`, 'lightingModel must be lit or unlit');
|
|
449
|
+
}
|
|
450
|
+
if (profile.baseColor != null) {
|
|
451
|
+
if (!isRecord(profile.baseColor))
|
|
452
|
+
add(`${path}.baseColor`, 'baseColor must be an object');
|
|
453
|
+
else {
|
|
454
|
+
validateColor(profile.baseColor.color, `${path}.baseColor.color`, add);
|
|
455
|
+
validateMaterialTextureRef(profile.baseColor.texture, `${path}.baseColor.texture`, add);
|
|
456
|
+
for (const key of ['brightness', 'saturation', 'contrast', 'hue']) {
|
|
457
|
+
if (profile.baseColor[key] != null && !isFiniteNumber(profile.baseColor[key]))
|
|
458
|
+
add(`${path}.baseColor.${key}`, `${key} must be a finite number`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const key of ['metallic', 'roughness']) {
|
|
463
|
+
if (profile[key] != null && !isFiniteNumber(profile[key]))
|
|
464
|
+
add(`${path}.${key}`, `${key} must be a finite number`);
|
|
465
|
+
}
|
|
466
|
+
if (profile.normal != null) {
|
|
467
|
+
if (!isRecord(profile.normal))
|
|
468
|
+
add(`${path}.normal`, 'normal must be an object');
|
|
469
|
+
else {
|
|
470
|
+
validateMaterialTextureRef(profile.normal.texture, `${path}.normal.texture`, add);
|
|
471
|
+
if (profile.normal.strength != null && !isFiniteNumber(profile.normal.strength))
|
|
472
|
+
add(`${path}.normal.strength`, 'normal strength must be a finite number');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (profile.metallicRoughness != null) {
|
|
476
|
+
if (!isRecord(profile.metallicRoughness))
|
|
477
|
+
add(`${path}.metallicRoughness`, 'metallicRoughness must be an object');
|
|
478
|
+
else
|
|
479
|
+
validateMaterialTextureRef(profile.metallicRoughness.texture, `${path}.metallicRoughness.texture`, add);
|
|
480
|
+
}
|
|
481
|
+
if (profile.occlusion != null) {
|
|
482
|
+
if (!isRecord(profile.occlusion))
|
|
483
|
+
add(`${path}.occlusion`, 'occlusion must be an object');
|
|
484
|
+
else {
|
|
485
|
+
validateMaterialTextureRef(profile.occlusion.texture, `${path}.occlusion.texture`, add);
|
|
486
|
+
if (profile.occlusion.strength != null && !isFiniteNumber(profile.occlusion.strength))
|
|
487
|
+
add(`${path}.occlusion.strength`, 'occlusion strength must be a finite number');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (profile.emission != null) {
|
|
491
|
+
if (!isRecord(profile.emission))
|
|
492
|
+
add(`${path}.emission`, 'emission must be an object');
|
|
493
|
+
else {
|
|
494
|
+
validateColor(profile.emission.color, `${path}.emission.color`, add);
|
|
495
|
+
if (profile.emission.intensity != null && !isFiniteNumber(profile.emission.intensity))
|
|
496
|
+
add(`${path}.emission.intensity`, 'emission intensity must be a finite number');
|
|
497
|
+
validateMaterialTextureRef(profile.emission.texture, `${path}.emission.texture`, add);
|
|
498
|
+
validateMaterialTextureRef(profile.emission.maskTexture, `${path}.emission.maskTexture`, add);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (profile.alpha != null) {
|
|
502
|
+
if (!isRecord(profile.alpha))
|
|
503
|
+
add(`${path}.alpha`, 'alpha must be an object');
|
|
504
|
+
else {
|
|
505
|
+
if (profile.alpha.mode != null
|
|
506
|
+
&& profile.alpha.mode !== 'opaque'
|
|
507
|
+
&& profile.alpha.mode !== 'mask'
|
|
508
|
+
&& profile.alpha.mode !== 'blend') {
|
|
509
|
+
add(`${path}.alpha.mode`, 'alpha mode must be opaque, mask, or blend');
|
|
510
|
+
}
|
|
511
|
+
if (profile.alpha.opacity != null && !isFiniteNumber(profile.alpha.opacity))
|
|
512
|
+
add(`${path}.alpha.opacity`, 'alpha opacity must be a finite number');
|
|
513
|
+
if (profile.alpha.cutoff != null && !isFiniteNumber(profile.alpha.cutoff))
|
|
514
|
+
add(`${path}.alpha.cutoff`, 'alpha cutoff must be a finite number');
|
|
515
|
+
validateMaterialTextureRef(profile.alpha.texture, `${path}.alpha.texture`, add);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function validateMaterialTextureRef(texture, path, add) {
|
|
520
|
+
if (texture == null)
|
|
521
|
+
return;
|
|
522
|
+
if (!isRecord(texture)) {
|
|
523
|
+
add(path, 'texture ref must be an object');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (texture.url != null && !nonEmptyString(texture.url))
|
|
527
|
+
add(`${path}.url`, 'texture url must be non-empty when present');
|
|
528
|
+
if (texture.textureAssetId != null && !nonEmptyString(texture.textureAssetId))
|
|
529
|
+
add(`${path}.textureAssetId`, 'textureAssetId must be non-empty when present');
|
|
530
|
+
}
|
|
531
|
+
function validateMaterialAssetSystem(system, path, add) {
|
|
532
|
+
if (system == null)
|
|
533
|
+
return;
|
|
534
|
+
if (!isRecord(system)) {
|
|
535
|
+
add(path, 'material asset system must be an object');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (system.readonly != null && typeof system.readonly !== 'boolean')
|
|
539
|
+
add(`${path}.readonly`, 'readonly must be boolean');
|
|
540
|
+
if (system.preset != null && !MATERIAL_ASSET_SYSTEM_PRESETS.has(system.preset)) {
|
|
541
|
+
add(`${path}.preset`, 'preset must be default-pbr or default-standard');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function validateMaterialAssetOrigin(origin, path, add) {
|
|
545
|
+
if (origin == null)
|
|
546
|
+
return;
|
|
547
|
+
if (!isRecord(origin)) {
|
|
548
|
+
add(path, 'material asset origin must be an object');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (origin.type !== 'imported' && origin.type !== 'created' && origin.type !== 'duplicated' && origin.type !== 'preset') {
|
|
552
|
+
add(`${path}.type`, 'origin type must be imported, created, duplicated, or preset');
|
|
553
|
+
}
|
|
554
|
+
if (origin.sourceAssetGuid != null && !nonEmptyString(origin.sourceAssetGuid))
|
|
555
|
+
add(`${path}.sourceAssetGuid`, 'sourceAssetGuid must be non-empty when present');
|
|
556
|
+
if (origin.sourceAssetId != null && !nonEmptyString(origin.sourceAssetId))
|
|
557
|
+
add(`${path}.sourceAssetId`, 'sourceAssetId must be non-empty when present');
|
|
558
|
+
if (origin.sourceSlotId != null && !nonEmptyString(origin.sourceSlotId))
|
|
559
|
+
add(`${path}.sourceSlotId`, 'sourceSlotId must be non-empty when present');
|
|
560
|
+
if (origin.sourceMaterialIndex != null && !Number.isInteger(origin.sourceMaterialIndex))
|
|
561
|
+
add(`${path}.sourceMaterialIndex`, 'sourceMaterialIndex must be an integer when present');
|
|
562
|
+
if (origin.sourceMaterialName != null && !nonEmptyString(origin.sourceMaterialName))
|
|
563
|
+
add(`${path}.sourceMaterialName`, 'sourceMaterialName must be non-empty when present');
|
|
564
|
+
if (origin.sourceMaterialAssetId != null && !nonEmptyString(origin.sourceMaterialAssetId))
|
|
565
|
+
add(`${path}.sourceMaterialAssetId`, 'sourceMaterialAssetId must be non-empty when present');
|
|
566
|
+
}
|
|
567
|
+
function validateMaterialAssetSystemKindConsistency(materialAsset, path, add) {
|
|
568
|
+
const preset = isRecord(materialAsset.system) ? materialAsset.system.preset : undefined;
|
|
569
|
+
if (preset === 'default-pbr' && materialAsset.shaderId !== 'fps.builtin.pbr') {
|
|
570
|
+
add(`${path}.shaderId`, 'default-pbr material assets must use shaderId fps.builtin.pbr');
|
|
571
|
+
}
|
|
572
|
+
if (preset === 'default-standard' && materialAsset.shaderId !== 'fps.builtin.standard') {
|
|
573
|
+
add(`${path}.shaderId`, 'default-standard material assets must use shaderId fps.builtin.standard');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function validateNodeParentTarget(parentId, rootId, nodeIds, path, add) {
|
|
577
|
+
if (parentId === rootId) {
|
|
578
|
+
if (!HIERARCHY_PARENT_TARGETS.has('root')) {
|
|
579
|
+
add(path, `parentId must point to scene node: ${parentId}`);
|
|
580
|
+
}
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (HIERARCHY_PARENT_TARGETS.has('sceneNode') && nodeIds.has(parentId))
|
|
584
|
+
return;
|
|
585
|
+
const targetDescription = HIERARCHY_PARENT_TARGETS.has('root') && HIERARCHY_PARENT_TARGETS.has('sceneNode')
|
|
586
|
+
? 'root or scene node'
|
|
587
|
+
: HIERARCHY_PARENT_TARGETS.has('sceneNode')
|
|
588
|
+
? 'scene node'
|
|
589
|
+
: 'root';
|
|
590
|
+
add(path, `parentId must point to ${targetDescription}: ${parentId}`);
|
|
591
|
+
}
|
|
592
|
+
function validateNodeParentCycle(nodeId, rootId, nodesById, path, add) {
|
|
593
|
+
if (!nonEmptyString(rootId))
|
|
594
|
+
return;
|
|
595
|
+
const visited = new Set([nodeId]);
|
|
596
|
+
let cursorId = nonEmptyString(nodesById.get(nodeId)?.parentId)
|
|
597
|
+
? nodesById.get(nodeId)?.parentId
|
|
598
|
+
: rootId;
|
|
599
|
+
while (nonEmptyString(cursorId) && cursorId !== rootId) {
|
|
600
|
+
if (visited.has(cursorId)) {
|
|
601
|
+
add(path, `parentId creates a node cycle involving ${nodeId}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
visited.add(cursorId);
|
|
605
|
+
const cursor = nodesById.get(cursorId);
|
|
606
|
+
if (!cursor)
|
|
607
|
+
return;
|
|
608
|
+
cursorId = nonEmptyString(cursor.parentId) ? cursor.parentId : rootId;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function validateRuntimeV3MaterialSlot(slot, path, add) {
|
|
612
|
+
for (const field of Object.keys(slot)) {
|
|
613
|
+
if (!RUNTIME_V3_MATERIAL_SLOT_FIELDS.has(field)) {
|
|
614
|
+
add(`${path}.${field}`, `runtime schema v3 material slot field is not allowed: ${field}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (slot.label != null && !nonEmptyString(slot.label)) {
|
|
618
|
+
add(`${path}.label`, 'label must be a non-empty string when present');
|
|
619
|
+
}
|
|
620
|
+
if (!Array.isArray(slot.sourceSurfaces) || slot.sourceSurfaces.length === 0) {
|
|
621
|
+
add(`${path}.sourceSurfaces`, 'sourceSurfaces must be a non-empty array');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
for (let surfaceIndex = 0; surfaceIndex < slot.sourceSurfaces.length; surfaceIndex += 1) {
|
|
625
|
+
const surfacePath = `${path}.sourceSurfaces[${surfaceIndex}]`;
|
|
626
|
+
const surface = slot.sourceSurfaces[surfaceIndex];
|
|
627
|
+
if (!isRecord(surface)) {
|
|
628
|
+
add(surfacePath, 'source surface must be an object');
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
for (const field of Object.keys(surface)) {
|
|
632
|
+
if (!RUNTIME_V3_MATERIAL_SLOT_SOURCE_SURFACE_FIELDS.has(field)) {
|
|
633
|
+
add(`${surfacePath}.${field}`, `runtime schema v3 material slot source surface field is not allowed: ${field}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (!nonEmptyString(surface.ownerNodePath)) {
|
|
637
|
+
add(`${surfacePath}.ownerNodePath`, 'ownerNodePath must be a non-empty string');
|
|
638
|
+
}
|
|
639
|
+
for (const key of ['meshIndex', 'primitiveIndex']) {
|
|
640
|
+
if (!Number.isInteger(surface[key]) || surface[key] < 0) {
|
|
641
|
+
add(`${surfacePath}.${key}`, `${key} must be a non-negative integer`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const key of ['nodeIndex', 'sourceMaterialIndex']) {
|
|
645
|
+
if (surface[key] != null && (!Number.isInteger(surface[key]) || surface[key] < 0)) {
|
|
646
|
+
add(`${surfacePath}.${key}`, `${key} must be a non-negative integer when present`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (surface.nodeIndexPath != null && (!Array.isArray(surface.nodeIndexPath)
|
|
650
|
+
|| surface.nodeIndexPath.some((value) => !Number.isInteger(value) || value < 0))) {
|
|
651
|
+
add(`${surfacePath}.nodeIndexPath`, 'nodeIndexPath must contain non-negative integers');
|
|
652
|
+
}
|
|
653
|
+
if (!nonEmptyString(surface.sourceMeshName)) {
|
|
654
|
+
add(`${surfacePath}.sourceMeshName`, 'sourceMeshName is required');
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function validateAssetDefaults(defaults, path, add) {
|
|
659
|
+
if (defaults == null)
|
|
660
|
+
return;
|
|
661
|
+
if (!isRecord(defaults)) {
|
|
662
|
+
add(path, 'defaults must be an object');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
validateTransform(defaults.transform, `${path}.transform`, add);
|
|
666
|
+
validateOutline(defaults.outline, `${path}.outline`, add);
|
|
667
|
+
if (defaults.childOutlines != null) {
|
|
668
|
+
if (!isRecord(defaults.childOutlines)) {
|
|
669
|
+
add(`${path}.childOutlines`, 'childOutlines must be an object');
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
for (const [key, outline] of Object.entries(defaults.childOutlines)) {
|
|
673
|
+
validateOutline(outline, `${path}.childOutlines.${key}`, add);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function validateGeneratedFrom(generatedFrom, path, add) {
|
|
679
|
+
if (generatedFrom == null)
|
|
680
|
+
return;
|
|
681
|
+
if (!isRecord(generatedFrom)) {
|
|
682
|
+
add(path, 'generatedFrom must be an object when present');
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
validateAuthoringSourceRef(generatedFrom, path, add);
|
|
686
|
+
if (!nonEmptyString(generatedFrom.compilerId))
|
|
687
|
+
add(`${path}.compilerId`, 'compilerId must be a non-empty string');
|
|
688
|
+
if (!nonEmptyString(generatedFrom.compilerVersion))
|
|
689
|
+
add(`${path}.compilerVersion`, 'compilerVersion must be a non-empty string');
|
|
690
|
+
if (!nonEmptyString(generatedFrom.compiledAt))
|
|
691
|
+
add(`${path}.compiledAt`, 'compiledAt must be a non-empty string');
|
|
692
|
+
}
|
|
693
|
+
function validateRuntimeSourceBinding(source, path, add) {
|
|
694
|
+
if (source == null)
|
|
695
|
+
return;
|
|
696
|
+
if (!isRecord(source)) {
|
|
697
|
+
add(path, 'source must be an object when present');
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
validateAuthoringSourceRef(source, path, add);
|
|
701
|
+
if (source.objectGuid != null && !nonEmptyString(source.objectGuid))
|
|
702
|
+
add(`${path}.objectGuid`, 'objectGuid must be a non-empty string when present');
|
|
703
|
+
if (source.objectId != null && !nonEmptyString(source.objectId))
|
|
704
|
+
add(`${path}.objectId`, 'objectId must be a non-empty string when present');
|
|
705
|
+
if (source.component != null && !nonEmptyString(source.component))
|
|
706
|
+
add(`${path}.component`, 'component must be a non-empty string when present');
|
|
707
|
+
if (source.propertyPath != null && !nonEmptyString(source.propertyPath))
|
|
708
|
+
add(`${path}.propertyPath`, 'propertyPath must be a non-empty string when present');
|
|
709
|
+
}
|
|
710
|
+
function validateAuthoringSourceRef(source, path, add) {
|
|
711
|
+
if (!nonEmptyString(source.sourceId))
|
|
712
|
+
add(`${path}.sourceId`, 'sourceId must be a non-empty string');
|
|
713
|
+
if (!nonEmptyString(source.sourceType))
|
|
714
|
+
add(`${path}.sourceType`, 'sourceType must be a non-empty string');
|
|
715
|
+
if (source.revision != null && (typeof source.revision !== 'number' || !Number.isFinite(source.revision))) {
|
|
716
|
+
add(`${path}.revision`, 'revision must be a finite number when present');
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
export function assertPlayableEditorRuntimeSceneConfig(value, options) {
|
|
720
|
+
const result = validatePlayableEditorRuntimeSceneConfig(value, options);
|
|
721
|
+
if (result.ok)
|
|
722
|
+
return;
|
|
723
|
+
const error = new Error(`scene_json_schema_failed:\n${JSON.stringify(result.errors, null, 2)}`);
|
|
724
|
+
Object.assign(error, {
|
|
725
|
+
code: 'scene_json_schema_failed',
|
|
726
|
+
diagnostics: result.errors,
|
|
727
|
+
});
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
function isRecord(value) {
|
|
731
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
732
|
+
}
|
|
733
|
+
function nonEmptyString(value) {
|
|
734
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
735
|
+
}
|
|
736
|
+
function validateTransform(transform, path, add) {
|
|
737
|
+
if (transform == null)
|
|
738
|
+
return;
|
|
739
|
+
if (!isRecord(transform)) {
|
|
740
|
+
add(path, 'transform must be an object');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
validateVec3(transform.position, `${path}.position`, add);
|
|
744
|
+
validateVec3(transform.rotation, `${path}.rotation`, add);
|
|
745
|
+
validateVec3(transform.rotationDeg, `${path}.rotationDeg`, add);
|
|
746
|
+
if (typeof transform.scale === 'number') {
|
|
747
|
+
if (!Number.isFinite(transform.scale))
|
|
748
|
+
add(`${path}.scale`, 'scale number must be finite');
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
validateVec3(transform.scale, `${path}.scale`, add);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function validateSceneMarker(value, path, add) {
|
|
755
|
+
if (!isRecord(value)) {
|
|
756
|
+
add(path, 'marker must be an object');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
validateAllowedFields(value, [
|
|
760
|
+
'schemaVersion', 'label', 'type', 'kind', 'tags', 'note', 'color',
|
|
761
|
+
'target', 'semanticFrame', 'geometry', 'metadata',
|
|
762
|
+
], path, add);
|
|
763
|
+
if (value.schemaVersion !== 1)
|
|
764
|
+
add(`${path}.schemaVersion`, 'marker.schemaVersion must be 1');
|
|
765
|
+
if (!nonEmptyString(value.label))
|
|
766
|
+
add(`${path}.label`, 'marker.label must be a non-empty string');
|
|
767
|
+
if (!nonEmptyString(value.type))
|
|
768
|
+
add(`${path}.type`, 'marker.type must be a non-empty string');
|
|
769
|
+
if (value.kind != null && !nonEmptyString(value.kind))
|
|
770
|
+
add(`${path}.kind`, 'marker.kind must be a non-empty string when present');
|
|
771
|
+
if (value.tags != null) {
|
|
772
|
+
if (!Array.isArray(value.tags))
|
|
773
|
+
add(`${path}.tags`, 'marker.tags must be an array when present');
|
|
774
|
+
else
|
|
775
|
+
value.tags.forEach((entry, index) => {
|
|
776
|
+
if (typeof entry !== 'string')
|
|
777
|
+
add(`${path}.tags[${index}]`, 'marker tag must be a string');
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
if (value.note != null && typeof value.note !== 'string')
|
|
781
|
+
add(`${path}.note`, 'marker.note must be a string when present');
|
|
782
|
+
if (value.color != null)
|
|
783
|
+
validateMarkerColor(value.color, `${path}.color`, add);
|
|
784
|
+
if (value.target != null)
|
|
785
|
+
validateSceneMarkerTarget(value.target, `${path}.target`, add);
|
|
786
|
+
if (value.semanticFrame != null)
|
|
787
|
+
validateSceneMarkerFrame(value.semanticFrame, `${path}.semanticFrame`, add);
|
|
788
|
+
validateSceneMarkerGeometry(value.geometry, `${path}.geometry`, add);
|
|
789
|
+
if (value.metadata != null && !isRecord(value.metadata))
|
|
790
|
+
add(`${path}.metadata`, 'marker.metadata must be an object when present');
|
|
791
|
+
}
|
|
792
|
+
function validateSceneMarkerReferences(value, markerNodeId, nodeIds, markerNodeIds, path, add) {
|
|
793
|
+
if (!isRecord(value))
|
|
794
|
+
return;
|
|
795
|
+
const targets = [];
|
|
796
|
+
if (value.target != null)
|
|
797
|
+
targets.push({ value: value.target, path: `${path}.target` });
|
|
798
|
+
if (isRecord(value.geometry) && value.geometry.target != null) {
|
|
799
|
+
targets.push({ value: value.geometry.target, path: `${path}.geometry.target` });
|
|
800
|
+
}
|
|
801
|
+
for (const entry of targets) {
|
|
802
|
+
if (!isRecord(entry.value))
|
|
803
|
+
continue;
|
|
804
|
+
if (entry.value.kind !== 'scene-object' && entry.value.kind !== 'marker') {
|
|
805
|
+
add(`${entry.path}.kind`, 'runtime marker target kind must be scene-object or marker');
|
|
806
|
+
}
|
|
807
|
+
if (nonEmptyString(entry.value.id)) {
|
|
808
|
+
if (entry.value.id === markerNodeId) {
|
|
809
|
+
add(`${entry.path}.id`, 'runtime marker target must not reference its marker node');
|
|
810
|
+
}
|
|
811
|
+
else if (!nodeIds.has(entry.value.id)) {
|
|
812
|
+
add(`${entry.path}.id`, `runtime marker target must reference scene.nodes: ${entry.value.id}`);
|
|
813
|
+
}
|
|
814
|
+
else if (entry.value.kind === 'marker' && !markerNodeIds.has(entry.value.id)) {
|
|
815
|
+
add(`${entry.path}.id`, `runtime marker target must reference a marker node: ${entry.value.id}`);
|
|
816
|
+
}
|
|
817
|
+
else if (entry.value.kind === 'scene-object' && markerNodeIds.has(entry.value.id)) {
|
|
818
|
+
add(`${entry.path}.id`, `runtime scene-object target must not reference a marker node: ${entry.value.id}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function validateSceneMarkerTarget(value, path, add) {
|
|
824
|
+
if (!isRecord(value)) {
|
|
825
|
+
add(path, 'marker target must be an object');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
validateAllowedFields(value, ['kind', 'id', 'label', 'path', 'metadata'], path, add);
|
|
829
|
+
if (!nonEmptyString(value.kind))
|
|
830
|
+
add(`${path}.kind`, 'marker target kind must be a non-empty string');
|
|
831
|
+
if (!nonEmptyString(value.id))
|
|
832
|
+
add(`${path}.id`, 'marker target id must be a non-empty string');
|
|
833
|
+
for (const key of ['label', 'path']) {
|
|
834
|
+
if (value[key] != null && typeof value[key] !== 'string')
|
|
835
|
+
add(`${path}.${key}`, `marker target ${key} must be a string when present`);
|
|
836
|
+
}
|
|
837
|
+
if (value.metadata != null && !isRecord(value.metadata))
|
|
838
|
+
add(`${path}.metadata`, 'marker target metadata must be an object when present');
|
|
839
|
+
}
|
|
840
|
+
function validateSceneMarkerFrame(value, path, add) {
|
|
841
|
+
if (!isRecord(value)) {
|
|
842
|
+
add(path, 'marker semanticFrame must be an object');
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
validateAllowedFields(value, ['origin', 'forward', 'right', 'up', 'label', 'description'], path, add);
|
|
846
|
+
if (!isRecord(value.origin))
|
|
847
|
+
add(`${path}.origin`, 'marker semanticFrame.origin must be a vector object');
|
|
848
|
+
else
|
|
849
|
+
validateVec3(value.origin, `${path}.origin`, add);
|
|
850
|
+
for (const key of ['forward', 'right', 'up']) {
|
|
851
|
+
if (value[key] != null)
|
|
852
|
+
validateVec3(value[key], `${path}.${key}`, add);
|
|
853
|
+
}
|
|
854
|
+
for (const key of ['label', 'description']) {
|
|
855
|
+
if (value[key] != null && typeof value[key] !== 'string')
|
|
856
|
+
add(`${path}.${key}`, `marker semanticFrame ${key} must be a string when present`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function validateSceneMarkerGeometry(value, path, add) {
|
|
860
|
+
if (!isRecord(value)) {
|
|
861
|
+
add(path, 'marker.geometry must be an object');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (value.kind === 'box') {
|
|
865
|
+
validateAllowedFields(value, ['kind'], path, add);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (value.kind === 'point') {
|
|
869
|
+
validateAllowedFields(value, ['kind', 'coordinateSpace', 'position', 'offset', 'target'], path, add);
|
|
870
|
+
if (value.coordinateSpace != null && value.coordinateSpace !== 'world' && value.coordinateSpace !== 'local') {
|
|
871
|
+
add(`${path}.coordinateSpace`, 'point marker coordinateSpace must be world or local');
|
|
872
|
+
}
|
|
873
|
+
if (value.position != null)
|
|
874
|
+
validateVec3(value.position, `${path}.position`, add);
|
|
875
|
+
if (value.offset != null)
|
|
876
|
+
validateVec3(value.offset, `${path}.offset`, add);
|
|
877
|
+
if (value.target != null)
|
|
878
|
+
validateSceneMarkerTarget(value.target, `${path}.target`, add);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (value.kind === 'object-bounds') {
|
|
882
|
+
validateAllowedFields(value, ['kind', 'target'], path, add);
|
|
883
|
+
validateSceneMarkerTarget(value.target, `${path}.target`, add);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (value.kind === 'polyhedron') {
|
|
887
|
+
validateAllowedFields(value, ['kind', 'coordinateSpace', 'vertices', 'faces'], path, add);
|
|
888
|
+
if (value.coordinateSpace != null && value.coordinateSpace !== 'world') {
|
|
889
|
+
add(`${path}.coordinateSpace`, 'polyhedron marker coordinateSpace must be world');
|
|
890
|
+
}
|
|
891
|
+
if (!Array.isArray(value.vertices))
|
|
892
|
+
add(`${path}.vertices`, 'polyhedron marker vertices must be an array');
|
|
893
|
+
else
|
|
894
|
+
value.vertices.forEach((entry, index) => {
|
|
895
|
+
const vertexPath = `${path}.vertices[${index}]`;
|
|
896
|
+
if (!isRecord(entry))
|
|
897
|
+
add(vertexPath, 'polyhedron marker vertex must be a vector object');
|
|
898
|
+
else
|
|
899
|
+
validateVec3(entry, vertexPath, add);
|
|
900
|
+
});
|
|
901
|
+
if (value.faces != null) {
|
|
902
|
+
if (!Array.isArray(value.faces))
|
|
903
|
+
add(`${path}.faces`, 'polyhedron marker faces must be an array when present');
|
|
904
|
+
else
|
|
905
|
+
value.faces.forEach((face, faceIndex) => {
|
|
906
|
+
if (!Array.isArray(face))
|
|
907
|
+
add(`${path}.faces[${faceIndex}]`, 'polyhedron marker face must be an array');
|
|
908
|
+
else
|
|
909
|
+
face.forEach((entry, index) => {
|
|
910
|
+
if (!Number.isInteger(entry) || entry < 0)
|
|
911
|
+
add(`${path}.faces[${faceIndex}][${index}]`, 'polyhedron marker index must be a non-negative integer');
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
add(`${path}.kind`, 'marker.geometry.kind must be box, point, object-bounds, or polyhedron');
|
|
918
|
+
}
|
|
919
|
+
function validateMarkerColor(value, path, add) {
|
|
920
|
+
if (!isRecord(value)) {
|
|
921
|
+
add(path, 'marker color must be an object');
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
validateAllowedFields(value, ['r', 'g', 'b'], path, add);
|
|
925
|
+
for (const key of ['r', 'g', 'b']) {
|
|
926
|
+
if (!isUnitRangeNumber(value[key]))
|
|
927
|
+
add(`${path}.${key}`, 'marker color component must be between 0 and 1');
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function validateAllowedFields(value, allowed, path, add) {
|
|
931
|
+
const allowedFields = new Set(allowed);
|
|
932
|
+
for (const key of Object.keys(value)) {
|
|
933
|
+
if (!allowedFields.has(key))
|
|
934
|
+
add(`${path}.${key}`, `field is not allowed: ${key}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function validateVec3(value, path, add) {
|
|
938
|
+
if (value == null)
|
|
939
|
+
return;
|
|
940
|
+
if (!isRecord(value)) {
|
|
941
|
+
add(path, 'value must be a vector object');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
for (const axis of ['x', 'y', 'z']) {
|
|
945
|
+
if (typeof value[axis] !== 'number' || !Number.isFinite(value[axis])) {
|
|
946
|
+
add(`${path}.${axis}`, 'vector component must be a finite number');
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
function validateVec2(value, path, add) {
|
|
951
|
+
if (value == null)
|
|
952
|
+
return;
|
|
953
|
+
if (!isRecord(value)) {
|
|
954
|
+
add(path, 'value must be a vector object');
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
for (const axis of ['x', 'y']) {
|
|
958
|
+
if (typeof value[axis] !== 'number' || !Number.isFinite(value[axis])) {
|
|
959
|
+
add(`${path}.${axis}`, 'vector component must be a finite number');
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function isFiniteNumber(value) {
|
|
964
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
965
|
+
}
|
|
966
|
+
function isPositiveFiniteNumber(value) {
|
|
967
|
+
return isFiniteNumber(value) && value > 0;
|
|
968
|
+
}
|
|
969
|
+
function isUnitRangeNumber(value) {
|
|
970
|
+
return isFiniteNumber(value) && value >= 0 && value <= 1;
|
|
971
|
+
}
|
|
972
|
+
function validateColor(value, path, add) {
|
|
973
|
+
if (value == null)
|
|
974
|
+
return;
|
|
975
|
+
if (!isRecord(value)) {
|
|
976
|
+
add(path, 'color must be an object');
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
for (const key of ['r', 'g', 'b']) {
|
|
980
|
+
if (typeof value[key] !== 'number' || !Number.isFinite(value[key])) {
|
|
981
|
+
add(`${path}.${key}`, 'color component must be a finite number');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function validateCameraRig(value, path, add) {
|
|
986
|
+
if (value == null)
|
|
987
|
+
return;
|
|
988
|
+
if (!isRecord(value)) {
|
|
989
|
+
add(path, 'camera must be an object');
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (value.projection != null && value.projection !== 'orthographic' && value.projection !== 'perspective') {
|
|
993
|
+
add(`${path}.projection`, 'camera projection must be orthographic or perspective');
|
|
994
|
+
}
|
|
995
|
+
for (const key of ['alpha', 'beta']) {
|
|
996
|
+
if (value[key] != null && !isFiniteNumber(value[key])) {
|
|
997
|
+
add(`${path}.${key}`, 'camera property must be a finite number when present');
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
for (const key of ['radius', 'orthoSize']) {
|
|
1001
|
+
if (value[key] != null && !isPositiveFiniteNumber(value[key])) {
|
|
1002
|
+
add(`${path}.${key}`, 'camera property must be a positive finite number when present');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (value.fov != null && !isPositiveFiniteNumber(value.fov))
|
|
1006
|
+
add(`${path}.fov`, 'camera fov must be a positive finite number');
|
|
1007
|
+
validateVec3(value.targetOffset, `${path}.targetOffset`, add);
|
|
1008
|
+
validateVec2(value.targetScreenOffset, `${path}.targetScreenOffset`, add);
|
|
1009
|
+
for (const key of ['minZ', 'maxZ', 'lowerRadiusLimit', 'upperRadiusLimit']) {
|
|
1010
|
+
if (value[key] != null && !isPositiveFiniteNumber(value[key])) {
|
|
1011
|
+
add(`${path}.${key}`, 'camera property must be a positive finite number');
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
for (const key of ['lowerBetaLimit', 'upperBetaLimit']) {
|
|
1015
|
+
if (value[key] != null && !isFiniteNumber(value[key])) {
|
|
1016
|
+
add(`${path}.${key}`, 'camera property must be a finite number');
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (value.inertia != null && !isUnitRangeNumber(value.inertia)) {
|
|
1020
|
+
add(`${path}.inertia`, 'camera inertia must be between 0 and 1');
|
|
1021
|
+
}
|
|
1022
|
+
if (isPositiveFiniteNumber(value.minZ) && isPositiveFiniteNumber(value.maxZ) && value.maxZ <= value.minZ) {
|
|
1023
|
+
add(`${path}.maxZ`, 'camera maxZ must be greater than minZ');
|
|
1024
|
+
}
|
|
1025
|
+
if (isFiniteNumber(value.lowerBetaLimit) && isFiniteNumber(value.upperBetaLimit) && value.upperBetaLimit < value.lowerBetaLimit) {
|
|
1026
|
+
add(`${path}.upperBetaLimit`, 'camera upperBetaLimit must be greater than or equal to lowerBetaLimit');
|
|
1027
|
+
}
|
|
1028
|
+
if (isPositiveFiniteNumber(value.lowerRadiusLimit) && isPositiveFiniteNumber(value.upperRadiusLimit) && value.upperRadiusLimit < value.lowerRadiusLimit) {
|
|
1029
|
+
add(`${path}.upperRadiusLimit`, 'camera upperRadiusLimit must be greater than or equal to lowerRadiusLimit');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function isLegacyUnversionedLightArtifact(sceneConfig) {
|
|
1033
|
+
const generatedFrom = sceneConfig.meta?.generatedFrom;
|
|
1034
|
+
return isRecord(generatedFrom)
|
|
1035
|
+
&& generatedFrom.compilerId === PLAYABLE_EDITOR_SCENE_COMPILER_ID
|
|
1036
|
+
&& generatedFrom.compilerVersion === PLAYABLE_EDITOR_LEGACY_UNVERSIONED_LIGHT_COMPILER_VERSION;
|
|
1037
|
+
}
|
|
1038
|
+
function validateSceneLight(value, path, add, allowLegacyUnversionedLight = false) {
|
|
1039
|
+
if (value == null)
|
|
1040
|
+
return;
|
|
1041
|
+
const validationValue = allowLegacyUnversionedLight
|
|
1042
|
+
&& isRecord(value)
|
|
1043
|
+
&& !Object.prototype.hasOwnProperty.call(value, 'schemaVersion')
|
|
1044
|
+
? createLegacyUnversionedLightValidationView(value, path)
|
|
1045
|
+
: value;
|
|
1046
|
+
try {
|
|
1047
|
+
createFpsRuntimeLightSnapshot(validationValue, path);
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
const failure = error;
|
|
1051
|
+
add(typeof failure.path === 'string' ? failure.path : path, typeof failure.reason === 'string'
|
|
1052
|
+
? failure.reason
|
|
1053
|
+
: typeof failure.message === 'string'
|
|
1054
|
+
? failure.message
|
|
1055
|
+
: 'light does not satisfy the Engine Runtime Light contract');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function createLegacyUnversionedLightValidationView(value, path) {
|
|
1059
|
+
const validationValue = {
|
|
1060
|
+
...value,
|
|
1061
|
+
schemaVersion: FPS_RUNTIME_LIGHT_COMPONENT_SCHEMA_VERSION,
|
|
1062
|
+
};
|
|
1063
|
+
if (value.type !== 'directional' || !isRecord(value.direction))
|
|
1064
|
+
return validationValue;
|
|
1065
|
+
const { x, y, z } = value.direction;
|
|
1066
|
+
if (![x, y, z].every(component => typeof component === 'number' && Number.isFinite(component))) {
|
|
1067
|
+
return validationValue;
|
|
1068
|
+
}
|
|
1069
|
+
const length = Math.hypot(x, y, z);
|
|
1070
|
+
if (length <= 0 || Math.abs(length - 1) > LEGACY_LIGHT_DIRECTION_UNIT_TOLERANCE) {
|
|
1071
|
+
return validationValue;
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
...validationValue,
|
|
1075
|
+
direction: normalizeFpsRuntimeLightDirection(value.direction, `${path}.direction`),
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function validateOutline(value, path, add) {
|
|
1079
|
+
if (value == null)
|
|
1080
|
+
return;
|
|
1081
|
+
if (!isRecord(value)) {
|
|
1082
|
+
add(path, 'outline must be an object');
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (value.renderOutline != null && typeof value.renderOutline !== 'boolean')
|
|
1086
|
+
add(`${path}.renderOutline`, 'renderOutline must be boolean');
|
|
1087
|
+
if (value.outlineWidth != null && (typeof value.outlineWidth !== 'number' || !Number.isFinite(value.outlineWidth))) {
|
|
1088
|
+
add(`${path}.outlineWidth`, 'outlineWidth must be a finite number');
|
|
1089
|
+
}
|
|
1090
|
+
validateColor(value.outlineColor, `${path}.outlineColor`, add);
|
|
1091
|
+
}
|
|
1092
|
+
function validateGroundDecal(value, path, add) {
|
|
1093
|
+
if (value == null) {
|
|
1094
|
+
add(path, 'groundDecal is required when transformType is groundDecal');
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!isRecord(value)) {
|
|
1098
|
+
add(path, 'groundDecal must be an object');
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
validateGroundDecalUi(value, path, add);
|
|
1102
|
+
}
|
|
1103
|
+
function validateGroundDecalUi(value, path, add) {
|
|
1104
|
+
if (!nonEmptyString(value.uiKind))
|
|
1105
|
+
add(`${path}.uiKind`, 'groundDecal UI kind must be non-empty');
|
|
1106
|
+
if (!isRecord(value.size)) {
|
|
1107
|
+
add(`${path}.size`, 'groundDecal.size must be an object');
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
for (const key of ['width', 'depth']) {
|
|
1111
|
+
if (typeof value.size[key] !== 'number' || !Number.isFinite(value.size[key]) || value.size[key] <= 0) {
|
|
1112
|
+
add(`${path}.size.${key}`, 'groundDecal size must be a positive finite number');
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (value.aspectSourceLayerId != null && !nonEmptyString(value.aspectSourceLayerId)) {
|
|
1117
|
+
add(`${path}.aspectSourceLayerId`, 'aspectSourceLayerId must be non-empty when present');
|
|
1118
|
+
}
|
|
1119
|
+
if (value.lockAspectToBorder != null && typeof value.lockAspectToBorder !== 'boolean') {
|
|
1120
|
+
add(`${path}.lockAspectToBorder`, 'lockAspectToBorder must be boolean when present');
|
|
1121
|
+
}
|
|
1122
|
+
if (value.editorPreviewActive != null) {
|
|
1123
|
+
add(`${path}.editorPreviewActive`, 'editorPreviewActive is editor-only and must not appear in runtime scene data');
|
|
1124
|
+
}
|
|
1125
|
+
validateGroundDecalUiMask(value.mask, `${path}.mask`, add);
|
|
1126
|
+
validateGroundDecalUiRendering(value.rendering, `${path}.rendering`, add);
|
|
1127
|
+
if (!Array.isArray(value.layers)) {
|
|
1128
|
+
add(`${path}.layers`, 'groundDecal UI layers must be an array');
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
value.layers.forEach((layer, index) => validateGroundDecalUiLayer(layer, `${path}.layers[${index}]`, add));
|
|
1132
|
+
validateGroundDecalUiLayerIds(value.layers, `${path}.layers`, add);
|
|
1133
|
+
}
|
|
1134
|
+
function validateGroundDecalUiMask(value, path, add) {
|
|
1135
|
+
if (value == null)
|
|
1136
|
+
return;
|
|
1137
|
+
if (!isRecord(value)) {
|
|
1138
|
+
add(path, 'groundDecal UI mask must be an object');
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (value.enabled != null && typeof value.enabled !== 'boolean')
|
|
1142
|
+
add(`${path}.enabled`, 'mask.enabled must be boolean');
|
|
1143
|
+
if (value.source != null && value.source !== 'roundedRect' && value.source !== 'borderAlpha' && value.source !== 'texture') {
|
|
1144
|
+
add(`${path}.source`, 'mask.source must be roundedRect, borderAlpha, or texture');
|
|
1145
|
+
}
|
|
1146
|
+
if (value.textureId != null && !nonEmptyString(value.textureId))
|
|
1147
|
+
add(`${path}.textureId`, 'mask.textureId must be non-empty when present');
|
|
1148
|
+
for (const key of ['cornerRadius', 'padding']) {
|
|
1149
|
+
if (value[key] != null && (typeof value[key] !== 'number' || !Number.isFinite(value[key]) || value[key] < 0)) {
|
|
1150
|
+
add(`${path}.${key}`, `${key} must be a non-negative finite number`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function validateGroundDecalUiRendering(value, path, add) {
|
|
1155
|
+
if (value == null)
|
|
1156
|
+
return;
|
|
1157
|
+
if (!isRecord(value)) {
|
|
1158
|
+
add(path, 'groundDecal UI rendering must be an object');
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
for (const key of ['textureWidth', 'textureHeight']) {
|
|
1162
|
+
if (value[key] != null && (!Number.isInteger(value[key]) || value[key] <= 0)) {
|
|
1163
|
+
add(`${path}.${key}`, `${key} must be a positive integer`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const key of ['alphaIndex', 'diffuseTextureLevel', 'emissiveTextureLevel']) {
|
|
1167
|
+
if (value[key] != null && (typeof value[key] !== 'number' || !Number.isFinite(value[key]))) {
|
|
1168
|
+
add(`${path}.${key}`, `${key} must be a finite number`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function validateGroundDecalUiLayer(value, path, add) {
|
|
1173
|
+
if (!isRecord(value)) {
|
|
1174
|
+
add(path, 'groundDecal UI layer must be an object');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (!nonEmptyString(value.id))
|
|
1178
|
+
add(`${path}.id`, 'layer.id must be non-empty');
|
|
1179
|
+
if (!nonEmptyString(value.role))
|
|
1180
|
+
add(`${path}.role`, 'layer.role must be non-empty');
|
|
1181
|
+
if (value.editorPreviewPercent != null) {
|
|
1182
|
+
add(`${path}.editorPreviewPercent`, 'editorPreviewPercent is editor-only and must not appear in runtime scene data');
|
|
1183
|
+
}
|
|
1184
|
+
if (value.enabled != null && typeof value.enabled !== 'boolean')
|
|
1185
|
+
add(`${path}.enabled`, 'layer.enabled must be boolean');
|
|
1186
|
+
if (typeof value.zOrder !== 'number' || !Number.isFinite(value.zOrder))
|
|
1187
|
+
add(`${path}.zOrder`, 'layer.zOrder must be a finite number');
|
|
1188
|
+
if (value.opacity != null && (typeof value.opacity !== 'number' || !Number.isFinite(value.opacity)))
|
|
1189
|
+
add(`${path}.opacity`, 'layer.opacity must be finite');
|
|
1190
|
+
if (value.rotationDeg != null
|
|
1191
|
+
&& (typeof value.rotationDeg !== 'number'
|
|
1192
|
+
|| !Number.isFinite(value.rotationDeg)
|
|
1193
|
+
|| value.rotationDeg <= -180
|
|
1194
|
+
|| value.rotationDeg > 180)) {
|
|
1195
|
+
add(`${path}.rotationDeg`, 'layer.rotationDeg must be in the range (-180, 180]');
|
|
1196
|
+
}
|
|
1197
|
+
validateGroundDecalUiRect(value.rect, `${path}.rect`, add);
|
|
1198
|
+
if (value.kind === 'texture') {
|
|
1199
|
+
if (value.textureId != null && !nonEmptyString(value.textureId))
|
|
1200
|
+
add(`${path}.textureId`, 'texture layer textureId must be non-empty when present');
|
|
1201
|
+
if (value.imageFit != null && value.imageFit !== 'contain' && value.imageFit !== 'stretch') {
|
|
1202
|
+
add(`${path}.imageFit`, 'texture layer imageFit must be contain or stretch');
|
|
1203
|
+
}
|
|
1204
|
+
validateGroundDecalUiColor(value.tint, `${path}.tint`, add);
|
|
1205
|
+
}
|
|
1206
|
+
else if (value.kind === 'textureProgress') {
|
|
1207
|
+
if (value.textureId != null && !nonEmptyString(value.textureId))
|
|
1208
|
+
add(`${path}.textureId`, 'texture progress layer textureId must be non-empty when present');
|
|
1209
|
+
if (value.imageFit != null && value.imageFit !== 'contain' && value.imageFit !== 'stretch') {
|
|
1210
|
+
add(`${path}.imageFit`, 'texture progress layer imageFit must be contain or stretch');
|
|
1211
|
+
}
|
|
1212
|
+
if (typeof value.value !== 'number' || !Number.isFinite(value.value) || value.value < 0 || value.value > 1) {
|
|
1213
|
+
add(`${path}.value`, 'texture progress value must be between 0 and 1');
|
|
1214
|
+
}
|
|
1215
|
+
if (value.direction != null && !['leftToRight', 'rightToLeft', 'bottomToTop', 'topToBottom'].includes(String(value.direction))) {
|
|
1216
|
+
add(`${path}.direction`, 'texture progress direction is invalid');
|
|
1217
|
+
}
|
|
1218
|
+
validateGroundDecalUiColor(value.tint, `${path}.tint`, add);
|
|
1219
|
+
}
|
|
1220
|
+
else if (value.kind === 'color') {
|
|
1221
|
+
validateGroundDecalUiColor(value.color, `${path}.color`, add);
|
|
1222
|
+
}
|
|
1223
|
+
else if (value.kind === 'progress') {
|
|
1224
|
+
if (typeof value.value !== 'number' || !Number.isFinite(value.value) || value.value < 0 || value.value > 1) {
|
|
1225
|
+
add(`${path}.value`, 'progress value must be between 0 and 1');
|
|
1226
|
+
}
|
|
1227
|
+
if (value.direction != null && !['leftToRight', 'rightToLeft', 'bottomToTop', 'topToBottom'].includes(String(value.direction))) {
|
|
1228
|
+
add(`${path}.direction`, 'progress direction is invalid');
|
|
1229
|
+
}
|
|
1230
|
+
validateGroundDecalUiColor(value.color, `${path}.color`, add);
|
|
1231
|
+
}
|
|
1232
|
+
else if (value.kind === 'text') {
|
|
1233
|
+
validateGroundDecalUiText(value.text, `${path}.text`, add);
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
add(`${path}.kind`, 'layer.kind must be texture, textureProgress, color, progress, or text');
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function validateGroundDecalUiLayerIds(layers, path, add) {
|
|
1240
|
+
const seen = new Map();
|
|
1241
|
+
layers.forEach((layer, index) => {
|
|
1242
|
+
if (!isRecord(layer) || !nonEmptyString(layer.id))
|
|
1243
|
+
return;
|
|
1244
|
+
const previousIndex = seen.get(layer.id);
|
|
1245
|
+
if (previousIndex != null) {
|
|
1246
|
+
add(`${path}[${index}].id`, `layer.id must be unique; duplicate of layers[${previousIndex}]`);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
seen.set(layer.id, index);
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
function validateGroundDecalUiRect(value, path, add) {
|
|
1253
|
+
if (!isRecord(value)) {
|
|
1254
|
+
add(path, 'layer.rect must be an object');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
for (const key of ['x', 'z', 'width', 'depth']) {
|
|
1258
|
+
if (typeof value[key] !== 'number' || !Number.isFinite(value[key]))
|
|
1259
|
+
add(`${path}.${key}`, `rect.${key} must be a finite number`);
|
|
1260
|
+
}
|
|
1261
|
+
for (const key of ['width', 'depth']) {
|
|
1262
|
+
if (typeof value[key] === 'number' && value[key] <= 0)
|
|
1263
|
+
add(`${path}.${key}`, `rect.${key} must be positive`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function validateGroundDecalUiText(value, path, add) {
|
|
1267
|
+
if (!isRecord(value)) {
|
|
1268
|
+
add(path, 'text layer text must be an object');
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (typeof value.value !== 'string')
|
|
1272
|
+
add(`${path}.value`, 'text.value must be a string');
|
|
1273
|
+
if (value.fontFamily != null && typeof value.fontFamily !== 'string')
|
|
1274
|
+
add(`${path}.fontFamily`, 'fontFamily must be a string');
|
|
1275
|
+
if (value.fontSize != null && (typeof value.fontSize !== 'number' || !Number.isFinite(value.fontSize) || value.fontSize <= 0)) {
|
|
1276
|
+
add(`${path}.fontSize`, 'fontSize must be a positive finite number');
|
|
1277
|
+
}
|
|
1278
|
+
if (value.fontWeight != null && typeof value.fontWeight !== 'string')
|
|
1279
|
+
add(`${path}.fontWeight`, 'fontWeight must be a string');
|
|
1280
|
+
validateGroundDecalUiColor(value.color, `${path}.color`, add);
|
|
1281
|
+
validateGroundDecalUiColor(value.strokeColor, `${path}.strokeColor`, add);
|
|
1282
|
+
if (value.strokeWidth != null && (typeof value.strokeWidth !== 'number' || !Number.isFinite(value.strokeWidth) || value.strokeWidth < 0)) {
|
|
1283
|
+
add(`${path}.strokeWidth`, 'strokeWidth must be a non-negative finite number');
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function validateGroundDecalUiColor(value, path, add) {
|
|
1287
|
+
if (value == null)
|
|
1288
|
+
return;
|
|
1289
|
+
if (!isRecord(value)) {
|
|
1290
|
+
add(path, 'color must be an object');
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
for (const key of ['r', 'g', 'b']) {
|
|
1294
|
+
if (typeof value[key] !== 'number' || !Number.isFinite(value[key]))
|
|
1295
|
+
add(`${path}.${key}`, `color.${key} must be finite`);
|
|
1296
|
+
}
|
|
1297
|
+
if (value.a != null && (typeof value.a !== 'number' || !Number.isFinite(value.a)))
|
|
1298
|
+
add(`${path}.a`, 'color.a must be finite');
|
|
1299
|
+
}
|
|
1300
|
+
function validateNodeRendering(value, path, add) {
|
|
1301
|
+
if (value == null)
|
|
1302
|
+
return;
|
|
1303
|
+
if (!isRecord(value)) {
|
|
1304
|
+
add(path, 'rendering must be an object');
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (value.renderingGroupId != null
|
|
1308
|
+
&& value.renderingGroupId !== 0
|
|
1309
|
+
&& value.renderingGroupId !== 1
|
|
1310
|
+
&& value.renderingGroupId !== 2
|
|
1311
|
+
&& value.renderingGroupId !== 3) {
|
|
1312
|
+
add(`${path}.renderingGroupId`, 'renderingGroupId must be 0, 1, 2, or 3');
|
|
1313
|
+
}
|
|
1314
|
+
if (value.alphaIndex != null && (typeof value.alphaIndex !== 'number' || !Number.isFinite(value.alphaIndex))) {
|
|
1315
|
+
add(`${path}.alphaIndex`, 'alphaIndex must be a finite number');
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function positiveFinite(value) {
|
|
1319
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
1320
|
+
}
|
|
1321
|
+
function validateMaterialProperties(value, path, add) {
|
|
1322
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1323
|
+
const childPath = `${path}.${key}`;
|
|
1324
|
+
if (['albedoColor', 'diffuseColor', 'emissiveColor', 'outlineColor'].includes(key))
|
|
1325
|
+
validateColor(child, childPath, add);
|
|
1326
|
+
if (['metallic', 'roughness', 'contrast', 'brightness', 'saturation', 'hue', 'colorDensity', 'alpha', 'alphaCutOff', 'transparencyMode'].includes(key)) {
|
|
1327
|
+
if (typeof child !== 'number' || !Number.isFinite(child))
|
|
1328
|
+
add(childPath, `${key} must be a finite number`);
|
|
1329
|
+
}
|
|
1330
|
+
if (key === 'backFaceCulling' && typeof child !== 'boolean')
|
|
1331
|
+
add(childPath, 'backFaceCulling must be boolean');
|
|
1332
|
+
if (['albedoTexture', 'normalTexture', 'metallicTexture'].includes(key)) {
|
|
1333
|
+
if (!isRecord(child))
|
|
1334
|
+
add(childPath, `${key} must be an object`);
|
|
1335
|
+
else if (child.url != null && !nonEmptyString(child.url))
|
|
1336
|
+
add(`${childPath}.url`, 'texture url must be non-empty when present');
|
|
1337
|
+
}
|
|
1338
|
+
if (['pbr', 'standard'].includes(key)) {
|
|
1339
|
+
if (!isRecord(child))
|
|
1340
|
+
add(childPath, `${key} must be an object`);
|
|
1341
|
+
else
|
|
1342
|
+
validateMaterialProperties(child, childPath, add);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function assertNoRuntimeOnlyFields(value, path, add) {
|
|
1347
|
+
const serialized = JSON.stringify(value);
|
|
1348
|
+
const found = RUNTIME_ONLY_TOKENS.filter((token) => serialized.includes(token));
|
|
1349
|
+
if (found.length > 0)
|
|
1350
|
+
add(path, `contains runtime-only asset fields: ${found.join(', ')}`);
|
|
1351
|
+
}
|
|
1352
|
+
//# sourceMappingURL=scene-artifact-validation.js.map
|