@forgeax/engine-assets-runtime 0.1.3 → 0.1.6
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/README.md +442 -175
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/aabb.unit.test.d.ts +2 -0
- package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
- package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
- package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
- package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
- package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
- package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
- package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
- package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
- package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
- package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
- package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
- package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin.test.d.ts +2 -0
- package/dist/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
- package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
- package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
- package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
- package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
- package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
- package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
- package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
- package/dist/aabb.d.ts +3 -0
- package/dist/aabb.d.ts.map +1 -0
- package/dist/asset-registry.d.ts +552 -0
- package/dist/asset-registry.d.ts.map +1 -0
- package/dist/builtin-asset-registry.d.ts +22 -0
- package/dist/builtin-asset-registry.d.ts.map +1 -0
- package/dist/catalog-source.d.ts +2 -1
- package/dist/catalog-source.d.ts.map +1 -1
- package/dist/decode-image-bytes.d.ts +57 -0
- package/dist/decode-image-bytes.d.ts.map +1 -0
- package/dist/dynamic-texture-store.d.ts +89 -0
- package/dist/dynamic-texture-store.d.ts.map +1 -0
- package/dist/errors/asset.d.ts +201 -0
- package/dist/errors/asset.d.ts.map +1 -0
- package/dist/handles.d.ts +141 -0
- package/dist/handles.d.ts.map +1 -0
- package/dist/image-error.d.ts +6 -0
- package/dist/image-error.d.ts.map +1 -0
- package/dist/index.d.ts +32 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +7218 -114
- package/dist/index.mjs.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +317 -3
- package/dist/internal.mjs.map +1 -1
- package/dist/loader-registry.d.ts +63 -0
- package/dist/loader-registry.d.ts.map +1 -0
- package/dist/loaders/inline-pack.d.ts +72 -0
- package/dist/loaders/inline-pack.d.ts.map +1 -0
- package/dist/loaders/mesh-bin.d.ts +21 -0
- package/dist/loaders/mesh-bin.d.ts.map +1 -0
- package/dist/loaders/pack-artifact.d.ts +20 -0
- package/dist/loaders/pack-artifact.d.ts.map +1 -0
- package/dist/material/dependency-graph.d.ts +9 -0
- package/dist/material/dependency-graph.d.ts.map +1 -0
- package/dist/material/generation-cache.d.ts +24 -0
- package/dist/material/generation-cache.d.ts.map +1 -0
- package/dist/material/inspection.d.ts +24 -0
- package/dist/material/inspection.d.ts.map +1 -0
- package/dist/material/loader.d.ts +59 -0
- package/dist/material/loader.d.ts.map +1 -0
- package/dist/material/runtime-shader.d.ts +14 -0
- package/dist/material/runtime-shader.d.ts.map +1 -0
- package/dist/mesh-bin.d.ts +3 -0
- package/dist/mesh-bin.d.ts.map +1 -0
- package/dist/mipmap-generator.d.ts +132 -0
- package/dist/mipmap-generator.d.ts.map +1 -0
- package/dist/payload-validate.d.ts +60 -0
- package/dist/payload-validate.d.ts.map +1 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/registry/artifact-io.d.ts +16 -0
- package/dist/registry/artifact-io.d.ts.map +1 -0
- package/dist/registry/asset-evidence.d.ts +13 -0
- package/dist/registry/asset-evidence.d.ts.map +1 -0
- package/dist/registry/catalog-state.d.ts +47 -0
- package/dist/registry/catalog-state.d.ts.map +1 -0
- package/dist/registry/catalog.d.ts +48 -0
- package/dist/registry/catalog.d.ts.map +1 -0
- package/dist/registry/instantiate.d.ts +153 -0
- package/dist/registry/instantiate.d.ts.map +1 -0
- package/dist/registry/load-by-guid.d.ts +247 -0
- package/dist/registry/load-by-guid.d.ts.map +1 -0
- package/dist/registry/load-state.d.ts +26 -0
- package/dist/registry/load-state.d.ts.map +1 -0
- package/dist/registry/runtime-projection.d.ts +16 -0
- package/dist/registry/runtime-projection.d.ts.map +1 -0
- package/dist/registry/scene-publication-fence.d.ts +38 -0
- package/dist/registry/scene-publication-fence.d.ts.map +1 -0
- package/dist/registry/validate-material.d.ts +77 -0
- package/dist/registry/validate-material.d.ts.map +1 -0
- package/dist/resolve-asset-handle.d.ts +43 -0
- package/dist/resolve-asset-handle.d.ts.map +1 -0
- package/dist/resolve-tileset-runtime.d.ts +23 -0
- package/dist/resolve-tileset-runtime.d.ts.map +1 -0
- package/dist/scene-handle-fields.d.ts +88 -0
- package/dist/scene-handle-fields.d.ts.map +1 -0
- package/dist/scene-payload.d.ts +15 -0
- package/dist/scene-payload.d.ts.map +1 -0
- package/dist/wire-default-loaders.d.ts +29 -0
- package/dist/wire-default-loaders.d.ts.map +1 -0
- package/package.json +22 -9
- package/src/__tests__/aabb.unit.test.ts +97 -0
- package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
- package/src/__tests__/artifact-io.unit.test.ts +127 -0
- package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
- package/src/__tests__/asset-evidence.unit.test.ts +32 -0
- package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
- package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
- package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
- package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
- package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
- package/src/__tests__/asset-registry.unit.test.ts +511 -0
- package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
- package/src/__tests__/catalog-replica.unit.test.ts +196 -0
- package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
- package/src/__tests__/catalog-source.unit.test.ts +126 -0
- package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
- package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
- package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
- package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
- package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
- package/src/__tests__/decode-image-bytes.test.ts +90 -0
- package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
- package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
- package/src/__tests__/errors-asset.unit.test.ts +92 -0
- package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
- package/src/__tests__/instantiate-context.unit.test.ts +65 -0
- package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
- package/src/__tests__/instantiate.unit.test.ts +684 -0
- package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
- package/src/__tests__/loader-registry.unit.test.ts +101 -0
- package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
- package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
- package/src/__tests__/material-load-consumer.test-d.ts +42 -0
- package/src/__tests__/material-ready.integration.test.ts +492 -0
- package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
- package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
- package/src/__tests__/mesh-bin.unit.test.ts +152 -0
- package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
- package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
- package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
- package/src/__tests__/payload-validate.unit.test.ts +285 -0
- package/src/__tests__/plugin.test.ts +55 -0
- package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
- package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
- package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
- package/src/__tests__/registry-load-state.unit.test.ts +35 -0
- package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
- package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
- package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
- package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
- package/src/__tests__/scene-payload.unit.test.ts +151 -0
- package/src/__tests__/scene-publication-fence.test.ts +129 -0
- package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
- package/src/__tests__/static-rig.integration.test.ts +8 -0
- package/src/__tests__/validate-material.unit.test.ts +359 -0
- package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
- package/src/aabb.ts +107 -0
- package/src/asset-registry.ts +2040 -0
- package/src/builtin-asset-registry.ts +164 -0
- package/src/catalog-source.ts +14 -70
- package/src/decode-image-bytes.ts +126 -0
- package/src/dynamic-texture-store.ts +234 -0
- package/src/errors/asset.ts +347 -0
- package/src/handles.ts +180 -0
- package/src/image-error.ts +45 -0
- package/src/index.ts +177 -19
- package/src/internal.ts +2 -0
- package/src/loader-registry.ts +157 -0
- package/src/loaders/inline-pack.ts +837 -0
- package/src/loaders/mesh-bin.ts +304 -0
- package/src/loaders/pack-artifact.ts +665 -0
- package/src/material/dependency-graph.ts +46 -0
- package/src/material/generation-cache.ts +177 -0
- package/src/material/inspection.ts +46 -0
- package/src/material/loader.ts +326 -0
- package/src/material/runtime-shader.ts +133 -0
- package/src/mesh-bin.ts +5 -0
- package/src/mipmap-generator.ts +450 -0
- package/src/payload-validate.ts +573 -0
- package/src/plugin.ts +48 -0
- package/src/registry/artifact-io.ts +259 -0
- package/src/registry/asset-evidence.ts +51 -0
- package/src/registry/catalog-state.ts +409 -0
- package/src/registry/catalog.ts +369 -0
- package/src/registry/instantiate.ts +1056 -0
- package/src/registry/load-by-guid.ts +1657 -0
- package/src/registry/load-state.ts +115 -0
- package/src/registry/runtime-projection.ts +63 -0
- package/src/registry/scene-publication-fence.ts +256 -0
- package/src/registry/validate-material.ts +386 -0
- package/src/resolve-asset-handle.ts +147 -0
- package/src/resolve-tileset-runtime.ts +72 -0
- package/src/scene-handle-fields.ts +221 -0
- package/src/scene-payload.ts +207 -0
- package/src/wire-default-loaders.ts +79 -0
- package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-kind.test.d.ts +0 -2
- package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
- package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
- package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
- package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
- package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
- package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
- package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
- package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
- package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
- package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
- package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
- package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
- package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
- package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime -- scene instantiate collaboration module
|
|
2
|
+
// (feat-20260705-runtime-tier2-decomposition M1 / w6, D-4 + D-1). Free functions
|
|
3
|
+
// taking the AssetRegistry instance as first param; logic byte-preserved from the
|
|
4
|
+
// class body (this. -> registry.). Hosts the SkinJointResolver + PostSpawnHook
|
|
5
|
+
// hook-contract types relocated from scene-instances/post-spawn-resolve-joints.ts
|
|
6
|
+
// (D-1); w9 wires PostSpawnHook into the AssetRegistry constructor.
|
|
7
|
+
|
|
8
|
+
import type { EcsError, EntityHandle, World } from '@forgeax/engine-ecs';
|
|
9
|
+
import type { PackError } from '@forgeax/engine-pack/errors';
|
|
10
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
11
|
+
import { err, ok, type Result } from '@forgeax/engine-rhi';
|
|
12
|
+
import {
|
|
13
|
+
worldDespawnScene,
|
|
14
|
+
worldInstantiateScene,
|
|
15
|
+
worldInstantiateSceneFlat,
|
|
16
|
+
worldSetSceneAssetResolver,
|
|
17
|
+
} from '@forgeax/engine-scene';
|
|
18
|
+
import {
|
|
19
|
+
type Asset,
|
|
20
|
+
type AssetEnvelope,
|
|
21
|
+
AssetError,
|
|
22
|
+
type CatalogEntry,
|
|
23
|
+
type Handle,
|
|
24
|
+
type MountOverride,
|
|
25
|
+
PACK_ERROR_HINTS,
|
|
26
|
+
type SceneAsset,
|
|
27
|
+
type SceneInstanceMount,
|
|
28
|
+
type SkeletonAsset,
|
|
29
|
+
type SkinAsset,
|
|
30
|
+
type TagOf,
|
|
31
|
+
unwrapHandle,
|
|
32
|
+
} from '@forgeax/engine-types';
|
|
33
|
+
import type { AssetRegistry } from '../asset-registry';
|
|
34
|
+
import { resolveAssetHandle } from '../resolve-asset-handle';
|
|
35
|
+
import {
|
|
36
|
+
extractMountOverrideHandleGuids,
|
|
37
|
+
extractSceneEntityHandleGuids,
|
|
38
|
+
} from '../scene-handle-fields';
|
|
39
|
+
import {
|
|
40
|
+
compareScenePublicationFences,
|
|
41
|
+
parseScenePublicationFence,
|
|
42
|
+
type ScenePublicationFence,
|
|
43
|
+
type ScenePublicationFenceError,
|
|
44
|
+
scenePublicationFenceFromCatalog,
|
|
45
|
+
} from './scene-publication-fence';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolver contract consumed by {@link postSpawnResolveJoints} (D-1: relocated
|
|
49
|
+
* here from scene-instances/post-spawn-resolve-joints.ts so the hook contract
|
|
50
|
+
* travels with the instantiate cluster into @forgeax/engine-assets-runtime).
|
|
51
|
+
*/
|
|
52
|
+
export interface SkinJointResolver {
|
|
53
|
+
resolveSkinAsset(skeletonHandleRaw: number): SkinAsset | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function catalogSourceGuidForFence(
|
|
57
|
+
registry: AssetRegistry,
|
|
58
|
+
expected: ScenePublicationFence,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
const entries = catalogEntriesForFence(registry);
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.kind !== 'scene') continue;
|
|
63
|
+
const current = scenePublicationFenceFromCatalog(entries, entry.guid);
|
|
64
|
+
if (current.ok && compareScenePublicationFences(expected, current.value).ok) {
|
|
65
|
+
return entry.guid;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Merge the live Edit Catalog and the Play pack-index projection for fences. */
|
|
72
|
+
function catalogEntriesForFence(registry: AssetRegistry): readonly CatalogEntry[] {
|
|
73
|
+
const byGuid = new Map<string, CatalogEntry>();
|
|
74
|
+
for (const entry of registry.catalogSnapshot()?.entries ?? []) {
|
|
75
|
+
byGuid.set(entry.guid.toLowerCase(), entry);
|
|
76
|
+
}
|
|
77
|
+
for (const [guid, record] of registry.packIndexCache ?? []) {
|
|
78
|
+
if (record.sourcePath === undefined) continue;
|
|
79
|
+
const candidate = { ...record, guid, sourcePath: record.sourcePath } as CatalogEntry;
|
|
80
|
+
const current = byGuid.get(guid.toLowerCase());
|
|
81
|
+
if (
|
|
82
|
+
current === undefined ||
|
|
83
|
+
(current.publication === undefined && candidate.publication !== undefined)
|
|
84
|
+
) {
|
|
85
|
+
byGuid.set(guid.toLowerCase(), candidate);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...byGuid.values()];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Post-spawn hook contract (D-1). A hook runs after instantiate spawns the
|
|
93
|
+
* scene subtree; the shipped implementation is runtime's `postSpawnResolveJoints`
|
|
94
|
+
* (auto-wire Skin.joints). Injected at the sole production assembly point
|
|
95
|
+
* (createRenderer) in w9/w10; when absent, instantiate skips joint wiring.
|
|
96
|
+
*/
|
|
97
|
+
export type PostSpawnHook = (
|
|
98
|
+
world: World,
|
|
99
|
+
resolver: SkinJointResolver,
|
|
100
|
+
root: EntityHandle,
|
|
101
|
+
) => { ok: true } | { ok: false; error: unknown };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Remove the entities and producer grants created by one instantiate call.
|
|
105
|
+
*
|
|
106
|
+
* GUID fields use World interning, so an interned handle is released only when
|
|
107
|
+
* its producer grant is the last live reference. A failed scene has no live
|
|
108
|
+
* holders after `despawnScene`; a handle retained by an unrelated sibling is
|
|
109
|
+
* therefore left untouched. Fresh scene and nested-scene alloc grants are
|
|
110
|
+
* always released after their spawned holders are gone.
|
|
111
|
+
*/
|
|
112
|
+
function rollbackSpawn(
|
|
113
|
+
world: World,
|
|
114
|
+
roots: readonly EntityHandle[],
|
|
115
|
+
allocHandles: readonly number[],
|
|
116
|
+
internedHandles: readonly number[],
|
|
117
|
+
sharedRefBaseline: number,
|
|
118
|
+
): void {
|
|
119
|
+
for (const root of new Set(roots)) {
|
|
120
|
+
void worldDespawnScene(world, root);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const raw of new Set(allocHandles)) {
|
|
124
|
+
const handle = raw as never;
|
|
125
|
+
if (world.sharedRefs.refcount(handle) > 0) void world.sharedRefs.release(handle);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const raw of new Set(internedHandles)) {
|
|
129
|
+
if (world.sharedRefs._liveCount() <= sharedRefBaseline) break;
|
|
130
|
+
const handle = raw as never;
|
|
131
|
+
if (world.sharedRefs.refcount(handle) === 1) void world.sharedRefs.release(handle);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Materialise a `SceneAsset` into an existing `World` and return the
|
|
137
|
+
* synthetic root `Entity` (feat-20260514 w31 sugar wrapper; AC-03 +
|
|
138
|
+
* requirements §IN-3; M3: returns Entity not SceneInstanceId).
|
|
139
|
+
*
|
|
140
|
+
* Before spawning, handle-type component fields (e.g. `assetHandle`,
|
|
141
|
+
* `material`, `skeleton`) containing GUID strings are resolved to fresh
|
|
142
|
+
* user-tier `Handle` numbers via `world.allocSharedRef` (feat-20260614 M8
|
|
143
|
+
* D-19 instantiate-time GUID->handle mint; supersedes the pre-D-17
|
|
144
|
+
* `resolveGuid` map). GUIDs that fail to parse or are not catalogued return
|
|
145
|
+
* `AssetError(code='asset-not-found')` with a hint containing the GUID,
|
|
146
|
+
* node localId, and field name.
|
|
147
|
+
*
|
|
148
|
+
* Errors propagate verbatim through the closed
|
|
149
|
+
* `AssetError | PackError | EcsError` union so AI users that already
|
|
150
|
+
* narrow `loadByGuid<SceneAsset>` results reuse the same `switch
|
|
151
|
+
* (err.code)` exhaustively (charter proposition 3 machine-readable
|
|
152
|
+
* union; plan-strategy §3.3 closed-union transparency).
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* const sceneRes = await engine.assets.loadByGuid<SceneAsset>(roomGuid); // payload (D-17)
|
|
157
|
+
* if (!sceneRes.ok) return;
|
|
158
|
+
* const handle = world.allocSharedRef('SceneAsset', sceneRes.value); // mint column handle
|
|
159
|
+
* const r = engine.assets.instantiate(handle, world);
|
|
160
|
+
* if (!r.ok) {
|
|
161
|
+
* switch (r.error.code) {
|
|
162
|
+
* case 'asset-not-found':
|
|
163
|
+
* case 'pack-cyclic-reference':
|
|
164
|
+
* // ... AssetErrorCode | PackErrorCode | EcsErrorCode exhaustive
|
|
165
|
+
* }
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function instantiate<T extends SceneAsset>(
|
|
170
|
+
registry: AssetRegistry,
|
|
171
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
172
|
+
world: World,
|
|
173
|
+
parent?: EntityHandle,
|
|
174
|
+
expectedPublication?: ScenePublicationFence,
|
|
175
|
+
): Result<EntityHandle, AssetError | PackError | EcsError | ScenePublicationFenceError> {
|
|
176
|
+
const sharedRefBaseline = world.sharedRefs._liveCount();
|
|
177
|
+
const allocHandles: number[] = [];
|
|
178
|
+
const internedHandles: number[] = [];
|
|
179
|
+
// feat-20260614 M8 (D-15 / D-17): resolve the SceneAsset payload from the
|
|
180
|
+
// handle through the two-tier `resolveAssetHandle` (builtin / user-tier
|
|
181
|
+
// world.sharedRefs) -- the registry holds no handle->payload map. Scene
|
|
182
|
+
// GUID-type component fields are then resolved to fresh user-tier handles
|
|
183
|
+
// via `world.allocSharedRef` (instantiate-time GUID->handle mint). When the
|
|
184
|
+
// handle does not resolve to a scene payload, fall through to the ecs-only
|
|
185
|
+
// path (an externally-resolved SceneAssetResolver handle).
|
|
186
|
+
let instantiateResult: Result<EntityHandle, AssetError | PackError | EcsError>;
|
|
187
|
+
const sceneRes0 = resolveAssetHandle<SceneAsset>(
|
|
188
|
+
world,
|
|
189
|
+
handle as unknown as Handle<string, 'shared'>,
|
|
190
|
+
);
|
|
191
|
+
const sceneAsset = sceneRes0.ok ? sceneRes0.value : undefined;
|
|
192
|
+
if (sceneAsset !== undefined && sceneAsset.kind !== 'scene') {
|
|
193
|
+
return err(
|
|
194
|
+
new AssetError({
|
|
195
|
+
code: 'asset-invalid-value',
|
|
196
|
+
expected: 'instantiate handle resolves to a SceneAsset',
|
|
197
|
+
hint: `resolved asset kind was ${sceneAsset.kind}`,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (sceneAsset !== undefined && sceneAsset.kind === 'scene') {
|
|
202
|
+
// feat-20260622 M3 / w8: find the scene's GUID key in the catalog
|
|
203
|
+
// so _resolveSceneGuids can reverse-decode from envelope.refs edges.
|
|
204
|
+
const sceneGuidKey = registry._guidForAsset(sceneAsset);
|
|
205
|
+
if (expectedPublication !== undefined) {
|
|
206
|
+
const entries = catalogEntriesForFence(registry);
|
|
207
|
+
if (sceneGuidKey === undefined) {
|
|
208
|
+
return err({
|
|
209
|
+
code: 'asset-generation-fence-mismatch',
|
|
210
|
+
phase: 'instantiate',
|
|
211
|
+
hint: 'generated Scene source has no Catalog identity for publication fence validation',
|
|
212
|
+
retryable: true,
|
|
213
|
+
recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
|
|
214
|
+
} as const);
|
|
215
|
+
}
|
|
216
|
+
const current = scenePublicationFenceFromCatalog(entries, sceneGuidKey);
|
|
217
|
+
if (!current.ok) return current;
|
|
218
|
+
const matches = compareScenePublicationFences(expectedPublication, current.value);
|
|
219
|
+
if (!matches.ok) return matches;
|
|
220
|
+
}
|
|
221
|
+
const guidToHandle = new Map<string, number>();
|
|
222
|
+
const resolvedSceneHandles = new Map<string, number>();
|
|
223
|
+
const sceneRes = registry._resolveSceneGuids(
|
|
224
|
+
sceneAsset,
|
|
225
|
+
world,
|
|
226
|
+
sceneGuidKey,
|
|
227
|
+
undefined,
|
|
228
|
+
guidToHandle,
|
|
229
|
+
resolvedSceneHandles,
|
|
230
|
+
);
|
|
231
|
+
if (!sceneRes.ok) {
|
|
232
|
+
rollbackSpawn(
|
|
233
|
+
world,
|
|
234
|
+
[],
|
|
235
|
+
[...resolvedSceneHandles.values()],
|
|
236
|
+
[...guidToHandle.values()],
|
|
237
|
+
sharedRefBaseline,
|
|
238
|
+
);
|
|
239
|
+
return sceneRes;
|
|
240
|
+
}
|
|
241
|
+
internedHandles.push(...guidToHandle.values());
|
|
242
|
+
allocHandles.push(...resolvedSceneHandles.values());
|
|
243
|
+
|
|
244
|
+
// feat-20260703 M1 (D-1): register the resolved copy -> original
|
|
245
|
+
// catalog GUID in the origin reverse-index so _guidForAsset can
|
|
246
|
+
// find it even after the local sceneGuidKey variable is discarded.
|
|
247
|
+
if (sceneGuidKey !== undefined) {
|
|
248
|
+
registry._originIndex.set(sceneRes.value, sceneGuidKey);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Register the GUID-resolved SceneAsset as a shared ref so
|
|
252
|
+
// Scene owner resolves it transparently. The shared
|
|
253
|
+
// ref alloc-grant rc=1 stays held by the alloc; the SceneInstance spawn
|
|
254
|
+
// retains to rc=2 and the despawn path releases back to rc=1.
|
|
255
|
+
const sharedHandle = world.allocSharedRef('SceneAsset', sceneRes.value);
|
|
256
|
+
allocHandles.push(unwrapHandle(sharedHandle));
|
|
257
|
+
|
|
258
|
+
// m3-i3: wire identity resolver so mount.source already resolved
|
|
259
|
+
// to a live handle number by _resolveMountsRec passes through.
|
|
260
|
+
// Scene mount resolution calls this resolver; when source is a number (live handle),
|
|
261
|
+
// return it as-is; when source is a string (unresolved GUID),
|
|
262
|
+
// fail (should not happen after resolution, but fail-safe).
|
|
263
|
+
worldSetSceneAssetResolver(world, (source, _parentHandle) => {
|
|
264
|
+
if (typeof source === 'number') {
|
|
265
|
+
return ok(source as unknown as Handle<'SceneAsset', 'shared'>);
|
|
266
|
+
}
|
|
267
|
+
return err({
|
|
268
|
+
code: 'asset-not-found' as const,
|
|
269
|
+
expected: `mount source GUID ${source} resolved before instantiate`,
|
|
270
|
+
hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// C-R2 (feat-20260622-s5 M6): instantiateScene now returns
|
|
275
|
+
// `{ root, diagnostics }` on success. This runtime API keeps its
|
|
276
|
+
// `Result<EntityHandle>` contract; unwrap to `root`. (Surfacing scene
|
|
277
|
+
// unknown-field diagnostics through `assets.instantiate` is M7 README
|
|
278
|
+
// scope — the Scene package owns this boundary.)
|
|
279
|
+
const sceneInst = worldInstantiateScene(world, sharedHandle, parent);
|
|
280
|
+
if (!sceneInst.ok) {
|
|
281
|
+
rollbackSpawn(world, [], allocHandles, internedHandles, sharedRefBaseline);
|
|
282
|
+
return sceneInst as unknown as Result<EntityHandle, AssetError | PackError | EcsError>;
|
|
283
|
+
}
|
|
284
|
+
instantiateResult = ok(sceneInst.value.root);
|
|
285
|
+
} else {
|
|
286
|
+
// Non-resolvable handle: original ecs direct path (backward compat).
|
|
287
|
+
const sceneInst = worldInstantiateScene(
|
|
288
|
+
world,
|
|
289
|
+
handle as Handle<'SceneAsset', 'shared'>,
|
|
290
|
+
parent,
|
|
291
|
+
);
|
|
292
|
+
if (!sceneInst.ok) {
|
|
293
|
+
return sceneInst as unknown as Result<EntityHandle, AssetError | PackError | EcsError>;
|
|
294
|
+
}
|
|
295
|
+
instantiateResult = ok(sceneInst.value.root);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Post-spawn hook: auto-wire Skin.joints from jointPaths. feat-20260614 M8
|
|
299
|
+
// (D-15): the Skin column holds a user-tier SkeletonAsset handle; resolve
|
|
300
|
+
// it to the payload via the two-tier `resolveAssetHandle`, then match the
|
|
301
|
+
// catalogued SkinAsset whose resolved skeleton payload is the same object
|
|
302
|
+
// (the registry holds no handle->guid index).
|
|
303
|
+
//
|
|
304
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w9 (D-1): the hook is
|
|
305
|
+
// injected via `registry.postSpawnHook` (the sole production assembly point
|
|
306
|
+
// createRenderer wires `postSpawnResolveJoints`). When no hook is present
|
|
307
|
+
// (standalone / test registries without joint-wiring needs), instantiate
|
|
308
|
+
// skips the post-spawn wiring silently -- the resolver closure below stays
|
|
309
|
+
// inline (it reads registry-internal state: assetCatalog / _guidForAsset).
|
|
310
|
+
const hook = registry.postSpawnHook;
|
|
311
|
+
if (hook !== undefined) {
|
|
312
|
+
const self = registry;
|
|
313
|
+
const resolver: SkinJointResolver = {
|
|
314
|
+
resolveSkinAsset(skeletonHandleRaw: number) {
|
|
315
|
+
const skelRes = resolveAssetHandle<SkeletonAsset>(
|
|
316
|
+
world,
|
|
317
|
+
skeletonHandleRaw as unknown as Handle<string, 'shared'>,
|
|
318
|
+
);
|
|
319
|
+
if (!skelRes.ok) return undefined;
|
|
320
|
+
const skeletonPayload = skelRes.value as Asset;
|
|
321
|
+
const skeletonGuid = self._guidForAsset(skeletonPayload);
|
|
322
|
+
if (skeletonGuid === undefined) return undefined;
|
|
323
|
+
for (const [, envelope] of self.assetCatalog) {
|
|
324
|
+
const asset = envelope.payload;
|
|
325
|
+
if (asset.kind !== 'skin') continue;
|
|
326
|
+
const skinSkeletonGuid = asset.skeletonGuid;
|
|
327
|
+
if (skinSkeletonGuid === undefined) continue;
|
|
328
|
+
if (skinSkeletonGuid.toLowerCase() === skeletonGuid) {
|
|
329
|
+
return asset;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
const jointResolveResult = hook(world, resolver, instantiateResult.value);
|
|
336
|
+
if (!jointResolveResult.ok) {
|
|
337
|
+
rollbackSpawn(
|
|
338
|
+
world,
|
|
339
|
+
[instantiateResult.value],
|
|
340
|
+
allocHandles,
|
|
341
|
+
internedHandles,
|
|
342
|
+
sharedRefBaseline,
|
|
343
|
+
);
|
|
344
|
+
return { ok: false, error: jointResolveResult.error } as unknown as Result<
|
|
345
|
+
EntityHandle,
|
|
346
|
+
AssetError | PackError | EcsError
|
|
347
|
+
>;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return instantiateResult;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Materialise a `SceneAsset` FLAT into an existing `World` — the "edit the
|
|
356
|
+
* scene itself" registry entry (#655). Shares the GUID-resolution + shared-ref +
|
|
357
|
+
* SceneAssetResolver prelude with {@link instantiate}, but calls
|
|
358
|
+
* `worldInstantiateSceneFlat` instead of `worldInstantiateScene`: NO synthetic
|
|
359
|
+
* SceneInstance root, NO forced `ChildOf` on top-level members. The scene's own
|
|
360
|
+
* entities become plain top-level world entities; nested prefabs (`mounts[]`)
|
|
361
|
+
* still become their own SceneInstance anchors. Returns the set of top-level
|
|
362
|
+
* entity handles.
|
|
363
|
+
*
|
|
364
|
+
* Use this to OPEN a scene for authoring; use {@link instantiate} (anchor) at
|
|
365
|
+
* runtime / Play and for nested prefabs.
|
|
366
|
+
*
|
|
367
|
+
* The post-spawn Skin.joints hook (when wired via `registry.postSpawnHook`)
|
|
368
|
+
* runs once per top-level root: each GLB root keeps its own `ChildOf` subtree,
|
|
369
|
+
* so joint resolution is scoped to each subtree exactly as the anchor path
|
|
370
|
+
* scopes it to the single synthetic root.
|
|
371
|
+
*/
|
|
372
|
+
export function instantiateFlat<T extends SceneAsset>(
|
|
373
|
+
registry: AssetRegistry,
|
|
374
|
+
handle: Handle<TagOf<T>, 'shared'>,
|
|
375
|
+
world: World,
|
|
376
|
+
expectedPublication?: ScenePublicationFence,
|
|
377
|
+
): Result<EntityHandle[], AssetError | PackError | EcsError | ScenePublicationFenceError> {
|
|
378
|
+
const sharedRefBaseline = world.sharedRefs._liveCount();
|
|
379
|
+
const allocHandles: number[] = [];
|
|
380
|
+
const internedHandles: number[] = [];
|
|
381
|
+
let roots: EntityHandle[];
|
|
382
|
+
let mountEntities: EntityHandle[];
|
|
383
|
+
const sceneRes0 = resolveAssetHandle<SceneAsset>(
|
|
384
|
+
world,
|
|
385
|
+
handle as unknown as Handle<string, 'shared'>,
|
|
386
|
+
);
|
|
387
|
+
const sceneAsset = sceneRes0.ok ? sceneRes0.value : undefined;
|
|
388
|
+
if (sceneAsset !== undefined && sceneAsset.kind === 'scene') {
|
|
389
|
+
const sceneGuidKey = registry._guidForAsset(sceneAsset);
|
|
390
|
+
if (expectedPublication !== undefined) {
|
|
391
|
+
const entries = catalogEntriesForFence(registry);
|
|
392
|
+
if (sceneGuidKey === undefined) {
|
|
393
|
+
return err({
|
|
394
|
+
code: 'asset-generation-fence-mismatch',
|
|
395
|
+
phase: 'instantiate',
|
|
396
|
+
hint: 'generated Scene source has no Catalog identity for publication fence validation',
|
|
397
|
+
retryable: true,
|
|
398
|
+
recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
|
|
399
|
+
} as const);
|
|
400
|
+
}
|
|
401
|
+
const current = scenePublicationFenceFromCatalog(entries, sceneGuidKey);
|
|
402
|
+
if (!current.ok) return current;
|
|
403
|
+
const matches = compareScenePublicationFences(expectedPublication, current.value);
|
|
404
|
+
if (!matches.ok) return matches;
|
|
405
|
+
}
|
|
406
|
+
const guidToHandle = new Map<string, number>();
|
|
407
|
+
const resolvedSceneHandles = new Map<string, number>();
|
|
408
|
+
const sceneRes = registry._resolveSceneGuids(
|
|
409
|
+
sceneAsset,
|
|
410
|
+
world,
|
|
411
|
+
sceneGuidKey,
|
|
412
|
+
undefined,
|
|
413
|
+
guidToHandle,
|
|
414
|
+
resolvedSceneHandles,
|
|
415
|
+
);
|
|
416
|
+
if (!sceneRes.ok) {
|
|
417
|
+
rollbackSpawn(
|
|
418
|
+
world,
|
|
419
|
+
[],
|
|
420
|
+
[...resolvedSceneHandles.values()],
|
|
421
|
+
[...guidToHandle.values()],
|
|
422
|
+
sharedRefBaseline,
|
|
423
|
+
);
|
|
424
|
+
return sceneRes;
|
|
425
|
+
}
|
|
426
|
+
internedHandles.push(...guidToHandle.values());
|
|
427
|
+
allocHandles.push(...resolvedSceneHandles.values());
|
|
428
|
+
if (sceneGuidKey !== undefined) {
|
|
429
|
+
registry._originIndex.set(sceneRes.value, sceneGuidKey);
|
|
430
|
+
}
|
|
431
|
+
const sharedHandle = world.allocSharedRef('SceneAsset', sceneRes.value);
|
|
432
|
+
allocHandles.push(unwrapHandle(sharedHandle));
|
|
433
|
+
worldSetSceneAssetResolver(world, (source, _parentHandle) => {
|
|
434
|
+
if (typeof source === 'number') {
|
|
435
|
+
return ok(source as unknown as Handle<'SceneAsset', 'shared'>);
|
|
436
|
+
}
|
|
437
|
+
return err({
|
|
438
|
+
code: 'asset-not-found' as const,
|
|
439
|
+
expected: `mount source GUID ${source} resolved before instantiate`,
|
|
440
|
+
hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
const sceneInst = worldInstantiateSceneFlat(world, sharedHandle);
|
|
444
|
+
if (!sceneInst.ok) {
|
|
445
|
+
rollbackSpawn(world, [], allocHandles, internedHandles, sharedRefBaseline);
|
|
446
|
+
return sceneInst as unknown as Result<EntityHandle[], AssetError | PackError | EcsError>;
|
|
447
|
+
}
|
|
448
|
+
roots = sceneInst.value.roots;
|
|
449
|
+
mountEntities = sceneInst.value.mountEntities;
|
|
450
|
+
} else {
|
|
451
|
+
const sceneInst = worldInstantiateSceneFlat(world, handle as Handle<'SceneAsset', 'shared'>);
|
|
452
|
+
if (!sceneInst.ok) {
|
|
453
|
+
return sceneInst as unknown as Result<EntityHandle[], AssetError | PackError | EcsError>;
|
|
454
|
+
}
|
|
455
|
+
roots = sceneInst.value.roots;
|
|
456
|
+
mountEntities = sceneInst.value.mountEntities;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Post-spawn Skin.joints wiring, per top-level root. Mirrors the anchor
|
|
460
|
+
// path's hook (D-1: injected via `registry.postSpawnHook`); when no hook is
|
|
461
|
+
// present the flat path skips joint wiring silently.
|
|
462
|
+
const hook = registry.postSpawnHook;
|
|
463
|
+
if (hook !== undefined) {
|
|
464
|
+
const self = registry;
|
|
465
|
+
const resolver: SkinJointResolver = {
|
|
466
|
+
resolveSkinAsset(skeletonHandleRaw: number) {
|
|
467
|
+
const skelRes = resolveAssetHandle<SkeletonAsset>(
|
|
468
|
+
world,
|
|
469
|
+
skeletonHandleRaw as unknown as Handle<string, 'shared'>,
|
|
470
|
+
);
|
|
471
|
+
if (!skelRes.ok) return undefined;
|
|
472
|
+
const skeletonPayload = skelRes.value as Asset;
|
|
473
|
+
const skeletonGuid = self._guidForAsset(skeletonPayload);
|
|
474
|
+
if (skeletonGuid === undefined) return undefined;
|
|
475
|
+
for (const [, envelope] of self.assetCatalog) {
|
|
476
|
+
const asset = envelope.payload;
|
|
477
|
+
if (asset.kind !== 'skin') continue;
|
|
478
|
+
const skinSkeletonGuid = asset.skeletonGuid;
|
|
479
|
+
if (skinSkeletonGuid === undefined) continue;
|
|
480
|
+
if (skinSkeletonGuid.toLowerCase() === skeletonGuid) {
|
|
481
|
+
return asset;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return undefined;
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
const hookRoots = new Set<EntityHandle>(roots);
|
|
488
|
+
for (const mountEntity of mountEntities) hookRoots.add(mountEntity);
|
|
489
|
+
for (const root of hookRoots) {
|
|
490
|
+
const jointResolveResult = hook(world, resolver, root);
|
|
491
|
+
if (!jointResolveResult.ok) {
|
|
492
|
+
rollbackSpawn(
|
|
493
|
+
world,
|
|
494
|
+
[...roots, ...mountEntities],
|
|
495
|
+
allocHandles,
|
|
496
|
+
internedHandles,
|
|
497
|
+
sharedRefBaseline,
|
|
498
|
+
);
|
|
499
|
+
return { ok: false, error: jointResolveResult.error } as unknown as Result<
|
|
500
|
+
EntityHandle[],
|
|
501
|
+
AssetError | PackError | EcsError
|
|
502
|
+
>;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return ok(roots);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* m3-i2: Recursively resolve mounts[].source GUID strings.
|
|
512
|
+
* Returns a PackError-shaped object on cycle (R-9) or AssetError
|
|
513
|
+
* on child resolution failure.
|
|
514
|
+
*
|
|
515
|
+
* feat-20260713 M3 / w13: also down-drills `mounts[].overrides[].value`,
|
|
516
|
+
* resolving any GUID string bound to a `shared<...>` / `array<shared<...>>`
|
|
517
|
+
* schema field to a live handle (D-2). `guidToHandle` is the caller's per-scene
|
|
518
|
+
* dedup map (shared with the entity-field resolution in `_resolveSceneGuids`) so
|
|
519
|
+
* the same catalogued GUID mints exactly one user-tier handle across entity
|
|
520
|
+
* fields and override values (D-15/D-17 dedup contract); it defaults to a fresh
|
|
521
|
+
* map so standalone callers keep working. Override resolution runs for every
|
|
522
|
+
* mount regardless of `source` branch — an unresolvable GUID fail-fasts here,
|
|
523
|
+
* before any spawn (P3: no half-initialized member).
|
|
524
|
+
*/
|
|
525
|
+
export function resolveMountsRec(
|
|
526
|
+
registry: AssetRegistry,
|
|
527
|
+
mounts: readonly SceneInstanceMount[],
|
|
528
|
+
world: World,
|
|
529
|
+
visited: Set<string>,
|
|
530
|
+
guidToHandle: Map<string, number> = new Map(),
|
|
531
|
+
resolvedSceneHandles: Map<string, number> = new Map(),
|
|
532
|
+
): Result<
|
|
533
|
+
SceneInstanceMount[],
|
|
534
|
+
| AssetError
|
|
535
|
+
| {
|
|
536
|
+
readonly code: 'pack-cyclic-reference';
|
|
537
|
+
readonly expected: string;
|
|
538
|
+
readonly hint: string;
|
|
539
|
+
readonly detail: {
|
|
540
|
+
readonly code: 'pack-cyclic-reference';
|
|
541
|
+
readonly kind: 'mount-asset';
|
|
542
|
+
readonly cycle: readonly string[];
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
| ScenePublicationFenceError
|
|
546
|
+
> {
|
|
547
|
+
const out: SceneInstanceMount[] = [];
|
|
548
|
+
for (const m of mounts) {
|
|
549
|
+
if (m.publicationFence !== undefined) {
|
|
550
|
+
const parsedFence = parseScenePublicationFence(m.publicationFence);
|
|
551
|
+
if (!parsedFence.ok) return parsedFence;
|
|
552
|
+
const handleSourceGuid =
|
|
553
|
+
typeof m.source === 'string'
|
|
554
|
+
? m.source
|
|
555
|
+
: (() => {
|
|
556
|
+
const resolved = resolveAssetHandle<SceneAsset>(world, m.source as never);
|
|
557
|
+
return resolved.ok && resolved.value.kind === 'scene'
|
|
558
|
+
? registry._guidForAsset(resolved.value)
|
|
559
|
+
: undefined;
|
|
560
|
+
})();
|
|
561
|
+
const sourceGuid =
|
|
562
|
+
handleSourceGuid ??
|
|
563
|
+
(typeof m.source === 'string'
|
|
564
|
+
? undefined
|
|
565
|
+
: catalogSourceGuidForFence(registry, parsedFence.value));
|
|
566
|
+
if (sourceGuid === undefined) {
|
|
567
|
+
return err({
|
|
568
|
+
code: 'asset-generation-fence-mismatch',
|
|
569
|
+
phase: 'instantiate',
|
|
570
|
+
hint: 'publication-fenced mount has no source GUID identity for validation',
|
|
571
|
+
expected: parsedFence.value,
|
|
572
|
+
retryable: true,
|
|
573
|
+
recoveryActions: ['continue-last-known-good', 'retry-rebuild', 'fresh-reopen'],
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const currentFence = scenePublicationFenceFromCatalog(
|
|
577
|
+
catalogEntriesForFence(registry),
|
|
578
|
+
sourceGuid,
|
|
579
|
+
);
|
|
580
|
+
if (!currentFence.ok) return currentFence;
|
|
581
|
+
const matches = compareScenePublicationFences(parsedFence.value, currentFence.value);
|
|
582
|
+
if (!matches.ok) return matches;
|
|
583
|
+
}
|
|
584
|
+
let componentPatch:
|
|
585
|
+
| { components: NonNullable<SceneInstanceMount['components']> }
|
|
586
|
+
| Record<string, never> = {};
|
|
587
|
+
if (m.components !== undefined) {
|
|
588
|
+
const resolvedComponents = resolveMountComponents(
|
|
589
|
+
registry,
|
|
590
|
+
world,
|
|
591
|
+
m.components,
|
|
592
|
+
guidToHandle,
|
|
593
|
+
);
|
|
594
|
+
if (!resolvedComponents.ok) return resolvedComponents;
|
|
595
|
+
componentPatch = {
|
|
596
|
+
components: resolvedComponents.value as NonNullable<SceneInstanceMount['components']>,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// Resolve override value GUIDs first — applies to every mount regardless of
|
|
600
|
+
// the source branch below (a number-source mount can still carry overrides).
|
|
601
|
+
let overridePatch: { overrides: readonly MountOverride[] } | Record<string, never> = {};
|
|
602
|
+
if (m.overrides !== undefined && m.overrides.length > 0) {
|
|
603
|
+
const ro = resolveMountOverrides(registry, world, m.overrides, guidToHandle);
|
|
604
|
+
if (!ro.ok) return ro;
|
|
605
|
+
overridePatch = { overrides: ro.value };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const src = m.source;
|
|
609
|
+
// m3-i3: if source is already a number (live handle from a prior
|
|
610
|
+
// resolution pass), pass through unchanged.
|
|
611
|
+
if (typeof src === 'number') {
|
|
612
|
+
out.push({ ...m, ...componentPatch, ...overridePatch });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// source is a GUID string — resolve it.
|
|
617
|
+
const guidKey = src.toLowerCase();
|
|
618
|
+
|
|
619
|
+
// Cycle detection.
|
|
620
|
+
if (visited.has(guidKey)) {
|
|
621
|
+
return err({
|
|
622
|
+
code: 'pack-cyclic-reference' as const,
|
|
623
|
+
expected: 'no circular mount.source GUID references',
|
|
624
|
+
hint: PACK_ERROR_HINTS['pack-cyclic-reference'],
|
|
625
|
+
detail: {
|
|
626
|
+
code: 'pack-cyclic-reference' as const,
|
|
627
|
+
kind: 'mount-asset' as const,
|
|
628
|
+
cycle: [...visited, guidKey],
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Look up child scene.
|
|
634
|
+
const childEnv = registry.assetCatalog.get(guidKey);
|
|
635
|
+
if (childEnv === undefined) {
|
|
636
|
+
// Not catalogued — pass through as-is (overrides still resolved above).
|
|
637
|
+
out.push({ ...m, ...componentPatch, ...overridePatch });
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const childPayload = childEnv.payload;
|
|
641
|
+
if (
|
|
642
|
+
typeof childPayload !== 'object' ||
|
|
643
|
+
childPayload === null ||
|
|
644
|
+
(childPayload as Asset).kind !== 'scene'
|
|
645
|
+
) {
|
|
646
|
+
out.push({ ...m, ...componentPatch, ...overridePatch });
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const cachedChildHandle = resolvedSceneHandles.get(guidKey);
|
|
651
|
+
if (cachedChildHandle !== undefined) {
|
|
652
|
+
out.push({
|
|
653
|
+
...m,
|
|
654
|
+
source: cachedChildHandle,
|
|
655
|
+
...componentPatch,
|
|
656
|
+
...overridePatch,
|
|
657
|
+
} as SceneInstanceMount);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Resolve mounts recursively.
|
|
662
|
+
const childVisited = new Set(visited);
|
|
663
|
+
// Don't add guidKey to visited here — _resolveSceneGuids will do it
|
|
664
|
+
// via its own _visitedMountGuids parameter.
|
|
665
|
+
const childRes = registry._resolveSceneGuids(
|
|
666
|
+
childPayload as SceneAsset,
|
|
667
|
+
world,
|
|
668
|
+
guidKey,
|
|
669
|
+
childVisited,
|
|
670
|
+
guidToHandle,
|
|
671
|
+
resolvedSceneHandles,
|
|
672
|
+
true,
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
if (!childRes.ok) {
|
|
676
|
+
// Propagate child resolution error.
|
|
677
|
+
return childRes as unknown as Result<SceneInstanceMount[], AssetError>;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Build resolved child with its own mounts.
|
|
681
|
+
const resolvedChild: SceneAsset = {
|
|
682
|
+
kind: 'scene',
|
|
683
|
+
entities: childRes.value.entities,
|
|
684
|
+
...(childRes.value.mounts !== undefined && childRes.value.mounts.length > 0
|
|
685
|
+
? { mounts: childRes.value.mounts }
|
|
686
|
+
: {}),
|
|
687
|
+
} as SceneAsset;
|
|
688
|
+
|
|
689
|
+
// allocSharedRef + register in originIndex (D-7).
|
|
690
|
+
const chRaw = unwrapHandle(world.allocSharedRef('SceneAsset', resolvedChild));
|
|
691
|
+
resolvedSceneHandles.set(guidKey, chRaw);
|
|
692
|
+
registry._originIndex.set(resolvedChild, guidKey);
|
|
693
|
+
|
|
694
|
+
// Replace source with live handle number (D-5: source is number|string).
|
|
695
|
+
out.push({
|
|
696
|
+
...m,
|
|
697
|
+
source: chRaw,
|
|
698
|
+
...componentPatch,
|
|
699
|
+
...overridePatch,
|
|
700
|
+
} as SceneInstanceMount);
|
|
701
|
+
}
|
|
702
|
+
return ok(out);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function resolveMountComponents(
|
|
706
|
+
registry: AssetRegistry,
|
|
707
|
+
world: World,
|
|
708
|
+
components: NonNullable<SceneInstanceMount['components']>,
|
|
709
|
+
guidToHandle: Map<string, number>,
|
|
710
|
+
): Result<Record<string, Record<string, unknown>>, AssetError> {
|
|
711
|
+
const source = components as Record<string, Record<string, unknown>>;
|
|
712
|
+
const output: Record<string, Record<string, unknown>> = {};
|
|
713
|
+
for (const [componentName, fields] of Object.entries(source))
|
|
714
|
+
output[componentName] = { ...fields };
|
|
715
|
+
const entries = extractSceneEntityHandleGuids(world.components.entries(), [
|
|
716
|
+
{ localId: 0, components: source },
|
|
717
|
+
]);
|
|
718
|
+
for (const entry of entries) {
|
|
719
|
+
const fieldPath =
|
|
720
|
+
`${entry.componentName}.${entry.fieldName}` +
|
|
721
|
+
(entry.arrayIndex === undefined ? '' : `[${entry.arrayIndex}]`);
|
|
722
|
+
const resolved = resolveHandleGuid(
|
|
723
|
+
registry,
|
|
724
|
+
world,
|
|
725
|
+
entry.guidString,
|
|
726
|
+
guidToHandle,
|
|
727
|
+
fieldPath,
|
|
728
|
+
'mount.components',
|
|
729
|
+
);
|
|
730
|
+
if (!resolved.ok) return resolved;
|
|
731
|
+
const fields = output[entry.componentName];
|
|
732
|
+
if (fields === undefined) continue;
|
|
733
|
+
if (entry.arrayIndex === undefined) {
|
|
734
|
+
fields[entry.fieldName] = resolved.value;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const current = fields[entry.fieldName];
|
|
738
|
+
if (!Array.isArray(current)) continue;
|
|
739
|
+
const next = [...current];
|
|
740
|
+
next[entry.arrayIndex] = resolved.value;
|
|
741
|
+
fields[entry.fieldName] = next;
|
|
742
|
+
}
|
|
743
|
+
return ok(output);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Resolve one GUID string to a live user-tier handle (feat-20260713 M3 / w13
|
|
748
|
+
* SSOT). Parses the GUID, uses `guidToHandle` as the per-resolution fast path,
|
|
749
|
+
* and on a miss looks the envelope up in the catalog + interns its shared ref
|
|
750
|
+
* by `(target, payload identity)` in the World. An unparseable / uncatalogued
|
|
751
|
+
* GUID returns `AssetError(code='asset-not-found')` with a breadcrumb hint
|
|
752
|
+
* (`fieldPath` + `location`) for AI-user debuggability (P3). Shared by the
|
|
753
|
+
* entity-field fallback in `_resolveSceneGuids` and the override-value down-drill
|
|
754
|
+
* in {@link resolveMountOverrides} so the parse/dedup/lookup/mint idiom has one
|
|
755
|
+
* home (architecture-principles §1 SSOT).
|
|
756
|
+
*/
|
|
757
|
+
export function resolveHandleGuid(
|
|
758
|
+
registry: AssetRegistry,
|
|
759
|
+
world: World,
|
|
760
|
+
guidString: string,
|
|
761
|
+
guidToHandle: Map<string, number>,
|
|
762
|
+
fieldPath: string,
|
|
763
|
+
location: string,
|
|
764
|
+
): Result<number, AssetError> {
|
|
765
|
+
const guidRes = AssetGuid.parse(guidString);
|
|
766
|
+
if (!guidRes.ok) {
|
|
767
|
+
return err(
|
|
768
|
+
new AssetError({
|
|
769
|
+
code: 'asset-not-found',
|
|
770
|
+
expected: `valid GUID string for field ${fieldPath}`,
|
|
771
|
+
hint: `GUID "${guidString}" could not be parsed; at ${location}, field=${fieldPath}`,
|
|
772
|
+
}),
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const guidKey = guidString.toLowerCase();
|
|
776
|
+
let slot = guidToHandle.get(guidKey);
|
|
777
|
+
if (slot === undefined) {
|
|
778
|
+
const envelope = registry.assetCatalog.get(guidKey);
|
|
779
|
+
if (envelope === undefined) {
|
|
780
|
+
return err(
|
|
781
|
+
new AssetError({
|
|
782
|
+
code: 'asset-not-found',
|
|
783
|
+
expected: `GUID ${guidString} catalogued in AssetRegistry`,
|
|
784
|
+
hint:
|
|
785
|
+
`GUID ${guidString} not catalogued; ` +
|
|
786
|
+
`call loadByGuid('${guidString}') before instantiate; ` +
|
|
787
|
+
`at ${location}, field=${fieldPath}`,
|
|
788
|
+
}),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
slot = unwrapHandle(world.internSharedRef(envelope.payload.kind, envelope.payload));
|
|
792
|
+
guidToHandle.set(guidKey, slot);
|
|
793
|
+
}
|
|
794
|
+
return ok(slot);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* feat-20260713 M3 / w13: resolve every GUID string inside a mount's
|
|
799
|
+
* `overrides[].value` to a live handle, returning a new overrides array whose
|
|
800
|
+
* shared fields hold numeric handles (D-2 — the ecs apply loop never sees a
|
|
801
|
+
* GUID). Identification is delegated to the shared w12 core
|
|
802
|
+
* ({@link extractMountOverrideHandleGuids}); resolution reuses the
|
|
803
|
+
* `envelope.payload → world.internSharedRef` path with the caller's
|
|
804
|
+
* `guidToHandle` fast path (D-15/D-17). Stop-on-first-error: an unparseable /
|
|
805
|
+
* uncatalogued GUID
|
|
806
|
+
* returns `AssetError(code='asset-not-found')` with a breadcrumb hint (P3),
|
|
807
|
+
* aborting before any spawn. Number elements pass through untouched (D-8).
|
|
808
|
+
*/
|
|
809
|
+
function resolveMountOverrides(
|
|
810
|
+
registry: AssetRegistry,
|
|
811
|
+
world: World,
|
|
812
|
+
overrides: readonly MountOverride[],
|
|
813
|
+
guidToHandle: Map<string, number>,
|
|
814
|
+
): Result<MountOverride[], AssetError> {
|
|
815
|
+
const entries = extractMountOverrideHandleGuids(world.components.entries(), overrides);
|
|
816
|
+
if (entries.length === 0) return ok([...overrides]);
|
|
817
|
+
|
|
818
|
+
// resolvedMap key: `${overrideIndex}|${fieldName}|${arrayIndex ?? ''}`.
|
|
819
|
+
const resolvedMap = new Map<string, number>();
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
const fieldPath =
|
|
822
|
+
`${entry.componentName}.${entry.fieldName}` +
|
|
823
|
+
(entry.arrayIndex !== undefined ? `[${entry.arrayIndex}]` : '');
|
|
824
|
+
const slot = resolveHandleGuid(
|
|
825
|
+
registry,
|
|
826
|
+
world,
|
|
827
|
+
entry.guidString,
|
|
828
|
+
guidToHandle,
|
|
829
|
+
fieldPath,
|
|
830
|
+
`mount override index=${entry.overrideIndex}`,
|
|
831
|
+
);
|
|
832
|
+
if (!slot.ok) return slot;
|
|
833
|
+
resolvedMap.set(
|
|
834
|
+
overrideFieldKey(entry.overrideIndex, entry.fieldName, entry.arrayIndex),
|
|
835
|
+
slot.value,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Reconstruct: substitute resolved handles back into each override's value.
|
|
840
|
+
const out: MountOverride[] = [];
|
|
841
|
+
for (let ovIdx = 0; ovIdx < overrides.length; ovIdx++) {
|
|
842
|
+
out.push(reconstructOverride(overrides[ovIdx] as MountOverride, ovIdx, resolvedMap));
|
|
843
|
+
}
|
|
844
|
+
return ok(out);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** resolvedMap key for a resolved override handle. */
|
|
848
|
+
function overrideFieldKey(overrideIndex: number, fieldName: string, arrayIndex?: number): string {
|
|
849
|
+
return `${overrideIndex}|${fieldName}|${arrayIndex ?? ''}`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Rebuild one override with its shared-field GUID strings replaced by resolved
|
|
854
|
+
* handles from `resolvedMap`. Preserves object identity when nothing resolved
|
|
855
|
+
* (no shared GUID in this override). Patch form substitutes the single `value`;
|
|
856
|
+
* add form substitutes each key of the value map.
|
|
857
|
+
*/
|
|
858
|
+
function reconstructOverride(
|
|
859
|
+
ov: MountOverride,
|
|
860
|
+
ovIdx: number,
|
|
861
|
+
resolvedMap: Map<string, number>,
|
|
862
|
+
): MountOverride {
|
|
863
|
+
if (ov.field !== undefined) {
|
|
864
|
+
const nv = reconstructFieldValue(ovIdx, ov.field, ov.value, resolvedMap);
|
|
865
|
+
return nv === ov.value ? ov : { ...ov, value: nv };
|
|
866
|
+
}
|
|
867
|
+
const value = ov.value;
|
|
868
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return ov;
|
|
869
|
+
const map = value as Record<string, unknown>;
|
|
870
|
+
let changed = false;
|
|
871
|
+
const next: Record<string, unknown> = {};
|
|
872
|
+
for (const key of Object.keys(map)) {
|
|
873
|
+
const nv = reconstructFieldValue(ovIdx, key, map[key], resolvedMap);
|
|
874
|
+
next[key] = nv;
|
|
875
|
+
if (nv !== map[key]) changed = true;
|
|
876
|
+
}
|
|
877
|
+
return changed ? { ...ov, value: next } : ov;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Resolve one field value: a scalar `shared<T>` string becomes its handle; an
|
|
882
|
+
* `array<shared<T>>` becomes an array with resolved handles substituted and
|
|
883
|
+
* already-numeric elements passed through (D-8). Non-shared / unresolved values
|
|
884
|
+
* pass through unchanged (identity preserved).
|
|
885
|
+
*/
|
|
886
|
+
function reconstructFieldValue(
|
|
887
|
+
ovIdx: number,
|
|
888
|
+
fieldName: string,
|
|
889
|
+
value: unknown,
|
|
890
|
+
resolvedMap: Map<string, number>,
|
|
891
|
+
): unknown {
|
|
892
|
+
const scalar = resolvedMap.get(overrideFieldKey(ovIdx, fieldName));
|
|
893
|
+
if (scalar !== undefined) return scalar;
|
|
894
|
+
if (Array.isArray(value)) {
|
|
895
|
+
const arr: unknown[] = [];
|
|
896
|
+
let anyResolved = false;
|
|
897
|
+
for (let i = 0; i < value.length; i++) {
|
|
898
|
+
const resolved = resolvedMap.get(overrideFieldKey(ovIdx, fieldName, i));
|
|
899
|
+
if (resolved !== undefined) {
|
|
900
|
+
arr.push(resolved);
|
|
901
|
+
anyResolved = true;
|
|
902
|
+
} else {
|
|
903
|
+
arr.push(value[i]);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return anyResolved ? arr : value;
|
|
907
|
+
}
|
|
908
|
+
return value;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* tweak-20260609 M1 helper: build the per-sub-ref parent context for a
|
|
913
|
+
* SceneAsset child. feat-20260622 M3 / w9: re-sourced to lookup in the
|
|
914
|
+
* scene envelope's ``refs[]`` edges instead of walking entity components
|
|
915
|
+
* via extractSceneEntityHandleGuids (D-7). When the scene envelope is not
|
|
916
|
+
* found in the catalog, falls back to the entity-walk path (backward compat
|
|
917
|
+
* for call sites that lack a catalogued envelope).
|
|
918
|
+
*
|
|
919
|
+
* Texture edges (sourceField=undefined) produce ``componentField:
|
|
920
|
+
* undefined`` — the breadcrumb will show GUID+kind only, no per-entity
|
|
921
|
+
* detail (D-2: texture has no per-entity origin).
|
|
922
|
+
*/
|
|
923
|
+
export function buildSceneChildContext(
|
|
924
|
+
registry: AssetRegistry,
|
|
925
|
+
scene: Asset & { kind: 'scene' },
|
|
926
|
+
subGuidKey: string,
|
|
927
|
+
sceneGuidKey?: string,
|
|
928
|
+
):
|
|
929
|
+
| {
|
|
930
|
+
sceneEntityId?: number;
|
|
931
|
+
componentField?: string;
|
|
932
|
+
sourceField?: {
|
|
933
|
+
componentName?: string;
|
|
934
|
+
fieldName: string;
|
|
935
|
+
arrayIndex?: number;
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
| undefined {
|
|
939
|
+
// feat-20260622 M3 / w9: direct lookup in envelope.refs edges.
|
|
940
|
+
// feat-20260622 review r1: address the recursing scene's OWN envelope by
|
|
941
|
+
// its guidKey, not the first scene in the catalog -- under a multi-scene
|
|
942
|
+
// glTF catalog the first-scene scan attributes the breadcrumb to the wrong
|
|
943
|
+
// scene. Fall back to the first-scene scan only when no guidKey is given
|
|
944
|
+
// (legacy call sites lacking a catalogued envelope).
|
|
945
|
+
let sceneEnvelope: AssetEnvelope | undefined;
|
|
946
|
+
if (sceneGuidKey !== undefined) {
|
|
947
|
+
const env = registry.assetCatalog.get(sceneGuidKey);
|
|
948
|
+
if (env?.kind === 'scene') sceneEnvelope = env;
|
|
949
|
+
}
|
|
950
|
+
if (sceneEnvelope === undefined) {
|
|
951
|
+
for (const [, env] of registry.assetCatalog) {
|
|
952
|
+
if (env.kind === 'scene' && env.refs !== undefined && env.refs.length > 0) {
|
|
953
|
+
sceneEnvelope = env;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
let edgeResult:
|
|
959
|
+
| {
|
|
960
|
+
sceneEntityId?: number;
|
|
961
|
+
componentField?: string;
|
|
962
|
+
}
|
|
963
|
+
| undefined;
|
|
964
|
+
if (sceneEnvelope?.refs !== undefined) {
|
|
965
|
+
for (const ref of sceneEnvelope.refs) {
|
|
966
|
+
if (ref.guid.toLowerCase() === subGuidKey) {
|
|
967
|
+
const { sceneEntityId, sourceField } = ref;
|
|
968
|
+
const result: {
|
|
969
|
+
sceneEntityId?: number;
|
|
970
|
+
componentField?: string;
|
|
971
|
+
sourceField?: {
|
|
972
|
+
componentName?: string;
|
|
973
|
+
fieldName: string;
|
|
974
|
+
arrayIndex?: number;
|
|
975
|
+
};
|
|
976
|
+
} = {};
|
|
977
|
+
if (sceneEntityId !== undefined) {
|
|
978
|
+
result.sceneEntityId = sceneEntityId;
|
|
979
|
+
}
|
|
980
|
+
if (sourceField?.componentName !== undefined && sourceField?.fieldName !== undefined) {
|
|
981
|
+
result.componentField =
|
|
982
|
+
`${sourceField.componentName}.${sourceField.fieldName}` +
|
|
983
|
+
(sourceField.arrayIndex !== undefined ? `[${sourceField.arrayIndex}]` : '');
|
|
984
|
+
}
|
|
985
|
+
if (sourceField !== undefined) {
|
|
986
|
+
result.sourceField = sourceField;
|
|
987
|
+
}
|
|
988
|
+
// A rich edge (dev register path) carries full detail — return now.
|
|
989
|
+
if (result.sceneEntityId !== undefined || result.componentField !== undefined) {
|
|
990
|
+
return result;
|
|
991
|
+
}
|
|
992
|
+
// feat-20260622 M4 / w14: a GUID-only edge (prod path: on-disk refs[]
|
|
993
|
+
// strip sourceField / sceneEntityId at the serialization boundary, w7
|
|
994
|
+
// D-10) carries no per-entity detail. Keep this empty-but-defined
|
|
995
|
+
// result as the fallback, then try the entity walk below to recover
|
|
996
|
+
// the entity localId + component.field path (D-7 / B-8). The walk
|
|
997
|
+
// recovers handle-field edges (mesh / material); a texture edge (D-2:
|
|
998
|
+
// no per-entity origin) is not found by the walk, so the empty
|
|
999
|
+
// edgeResult is returned (w10 texture-edge contract preserved).
|
|
1000
|
+
edgeResult = result;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Backward compat: fall back to entity walk when the envelope edge carries
|
|
1006
|
+
// no per-entity detail (prod path: GUID-only refs[]) or no envelope is
|
|
1007
|
+
// available (e.g. direct catalog() registration with scene payload, no refs).
|
|
1008
|
+
const entries = extractSceneEntityHandleGuids(
|
|
1009
|
+
registry.componentCatalog,
|
|
1010
|
+
scene.entities as unknown as ReadonlyArray<{
|
|
1011
|
+
readonly localId: number;
|
|
1012
|
+
readonly components: Record<string, Record<string, unknown>>;
|
|
1013
|
+
}>,
|
|
1014
|
+
);
|
|
1015
|
+
for (const entry of entries) {
|
|
1016
|
+
if (entry.guidString.toLowerCase() === subGuidKey) {
|
|
1017
|
+
return {
|
|
1018
|
+
sceneEntityId: entry.entityLocalId,
|
|
1019
|
+
componentField: `${entry.componentName}.${entry.fieldName}${entry.arrayIndex !== undefined ? `[${entry.arrayIndex}]` : ''}`,
|
|
1020
|
+
// feat-20260622 verify r1: also surface the recovered provenance in
|
|
1021
|
+
// structured parts so the failure `.detail` can expose them for AI
|
|
1022
|
+
// property access (charter P3), not only the concatenated hint string.
|
|
1023
|
+
sourceField: {
|
|
1024
|
+
componentName: entry.componentName,
|
|
1025
|
+
fieldName: entry.fieldName,
|
|
1026
|
+
...(entry.arrayIndex !== undefined ? { arrayIndex: entry.arrayIndex } : {}),
|
|
1027
|
+
},
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return edgeResult;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* tweak-20260609 M1 helper: build the error-hint breadcrumb string
|
|
1036
|
+
* containing the parent asset's GUID + kind, enriched with the
|
|
1037
|
+
* caller-provided `parentContext` (entity localId + component.field).
|
|
1038
|
+
*
|
|
1039
|
+
* Per D-7 / B-8: the breadcrumb appears before the sub-asset's own hint,
|
|
1040
|
+
* separated by " / ".
|
|
1041
|
+
*/
|
|
1042
|
+
export function buildBreadcrumbHint(
|
|
1043
|
+
parentGuidKey: string,
|
|
1044
|
+
parentKind: string,
|
|
1045
|
+
subGuidKey: string,
|
|
1046
|
+
parentContext?: {
|
|
1047
|
+
sceneEntityId?: number;
|
|
1048
|
+
componentField?: string;
|
|
1049
|
+
},
|
|
1050
|
+
): string {
|
|
1051
|
+
let breadcrumb = `sub-asset ${subGuidKey} referenced by ${parentKind} ${parentGuidKey}`;
|
|
1052
|
+
if (parentContext?.sceneEntityId !== undefined && parentContext?.componentField !== undefined) {
|
|
1053
|
+
breadcrumb += ` (entity ${parentContext.sceneEntityId}, field ${parentContext.componentField})`;
|
|
1054
|
+
}
|
|
1055
|
+
return breadcrumb;
|
|
1056
|
+
}
|