@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,1657 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime -- load-by-guid + pack-fetch collaboration module
|
|
2
|
+
// (feat-20260705-runtime-tier2-decomposition M1 / w7, D-4). Free functions
|
|
3
|
+
// taking the AssetRegistry instance as first param; logic byte-preserved from the
|
|
4
|
+
// class body (this. -> registry.). This is the largest method cluster (loadByGuid
|
|
5
|
+
// + the DDC / pack-index / pack-file fetch + parse pipeline).
|
|
6
|
+
|
|
7
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
8
|
+
import { err, ok, type Result, type RhiError } from '@forgeax/engine-rhi';
|
|
9
|
+
import { isEngineMaterial } from '@forgeax/engine-shader';
|
|
10
|
+
import {
|
|
11
|
+
type ArtifactDescriptor,
|
|
12
|
+
ASSET_ERROR_HINTS,
|
|
13
|
+
type Asset,
|
|
14
|
+
type AssetCompression,
|
|
15
|
+
AssetError,
|
|
16
|
+
type AssetErrorCode,
|
|
17
|
+
type AssetErrorDetail,
|
|
18
|
+
type AssetRef,
|
|
19
|
+
type CatalogSubject,
|
|
20
|
+
type ImageError,
|
|
21
|
+
type ImageMetadata,
|
|
22
|
+
type LoadContext,
|
|
23
|
+
type LoaderAsyncResult,
|
|
24
|
+
type MaterialAsset,
|
|
25
|
+
type MeshAsset,
|
|
26
|
+
type ParseErrorDetail,
|
|
27
|
+
} from '@forgeax/engine-types';
|
|
28
|
+
import type { AssetRegistry, ParsedPackFile } from '../asset-registry';
|
|
29
|
+
import {
|
|
30
|
+
createMaterialLoader,
|
|
31
|
+
type MaterialLoadError,
|
|
32
|
+
type MaterialLoadRequest,
|
|
33
|
+
type MaterialPublication,
|
|
34
|
+
type MaterialReady,
|
|
35
|
+
} from '../material/loader';
|
|
36
|
+
import { readArtifact } from './artifact-io';
|
|
37
|
+
import { fetchPackIndex, resolveCatalogAssetUrl } from './catalog';
|
|
38
|
+
import { buildBreadcrumbHint, buildSceneChildContext } from './instantiate';
|
|
39
|
+
import { resolveRuntimeProjection } from './runtime-projection';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load an asset and all its transitively referenced sub-assets by GUID;
|
|
43
|
+
* returns `ok(handle)` only when the asset and every sub-asset are in the
|
|
44
|
+
* registry.
|
|
45
|
+
*
|
|
46
|
+
* **Post-condition:** `ok(payload)` is returned ONLY when the asset AND every
|
|
47
|
+
* transitively referenced sub-asset (per the asset envelope's `refs[]`) are
|
|
48
|
+
* present in this registry. The implementation walks `envelope.refs` and
|
|
49
|
+
* recursively calls `loadByGuid` on each ref before cataloguing the top-level
|
|
50
|
+
* asset. The resolved value is the PAYLOAD `T`
|
|
51
|
+
* (D-17), never a handle -- mint a column handle with
|
|
52
|
+
* `world.allocSharedRef('Kind', payload)` when one is needed (e.g. before
|
|
53
|
+
* `instantiate`).
|
|
54
|
+
*
|
|
55
|
+
* Two paths:
|
|
56
|
+
* - **Dev / fallback** (no `configurePackIndex` call): synchronous catalogue
|
|
57
|
+
* lookup wrapped in `Promise.resolve`. Returns `Err(asset-not-found)` if not
|
|
58
|
+
* catalogued.
|
|
59
|
+
* - **Prod** (after `configurePackIndex(url)`): fetches `pack-index.json`
|
|
60
|
+
* on the first call (cached as a `Map<guid, {packageUrl, kind}>`), then
|
|
61
|
+
* fetches the individual resource URL and parses the asset payload, then
|
|
62
|
+
* catalogues it (GUID -> payload) and returns the payload.
|
|
63
|
+
*
|
|
64
|
+
* Error union: `AssetError | PackError | ImageError | RhiError` (closed -- no
|
|
65
|
+
* new codes were introduced by the recursive walk; every code is pre-existing).
|
|
66
|
+
*
|
|
67
|
+
* An in-flight `Map` (D-5) deduplicates concurrent calls for the same GUID and
|
|
68
|
+
* prevents stack overflow on cycles (A->B->A).
|
|
69
|
+
*
|
|
70
|
+
* **Breaking-change classification:** this is a semantic strengthening, not a
|
|
71
|
+
* shape change. Sub-assets catalogued by a prior `catalog(guid, payload)` /
|
|
72
|
+
* `loadByGuid` call are protected by the catalogue fast-path: the recursive
|
|
73
|
+
* walk hits cache on every node and incurs zero additional fetch.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const res = await engine.assets.loadByGuid<SceneAsset>(sceneGuid);
|
|
78
|
+
* if (!res.ok) {
|
|
79
|
+
* switch (res.error.code) {
|
|
80
|
+
* case 'asset-not-found':
|
|
81
|
+
* // top GUID or any sub-asset GUID is missing from the catalog
|
|
82
|
+
* break;
|
|
83
|
+
* case 'asset-fetch-failed':
|
|
84
|
+
* // network / CORS
|
|
85
|
+
* break;
|
|
86
|
+
* case 'asset-parse-failed':
|
|
87
|
+
* // payload malformed
|
|
88
|
+
* break;
|
|
89
|
+
* // ... AssetErrorCode | PackErrorCode | ImageErrorCode | RhiErrorCode exhaustive
|
|
90
|
+
* }
|
|
91
|
+
* return;
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export async function loadByGuid<T = Asset>(
|
|
96
|
+
registry: AssetRegistry,
|
|
97
|
+
guid: AssetGuid,
|
|
98
|
+
parentContext?: {
|
|
99
|
+
sceneEntityId?: number;
|
|
100
|
+
componentField?: string;
|
|
101
|
+
},
|
|
102
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
103
|
+
return loadByGuidInternal(registry, guid, parentContext, new Set());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cookedRecordFromPayload(payload: Record<string, unknown>): unknown {
|
|
107
|
+
if (payload.schemaVersion === 'material-cook/2') return payload;
|
|
108
|
+
if (payload.cooked !== null && typeof payload.cooked === 'object') return payload.cooked;
|
|
109
|
+
if (payload.record !== null && typeof payload.record === 'object') return payload.record;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function materialArtifactKey(
|
|
114
|
+
record: Record<string, unknown> | undefined,
|
|
115
|
+
descriptors: Readonly<Record<string, ArtifactDescriptor>>,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
const artifact = record?.artifact;
|
|
118
|
+
const path =
|
|
119
|
+
artifact !== null && typeof artifact === 'object' && 'path' in artifact
|
|
120
|
+
? (artifact as { readonly path?: unknown }).path
|
|
121
|
+
: undefined;
|
|
122
|
+
if (typeof path === 'string') {
|
|
123
|
+
const match = Object.entries(descriptors).find(([, descriptor]) => descriptor.path === path);
|
|
124
|
+
if (match !== undefined) return match[0];
|
|
125
|
+
}
|
|
126
|
+
const shaderArtifacts = Object.entries(descriptors).filter(
|
|
127
|
+
([, descriptor]) => descriptor.mediaType === 'text/wgsl',
|
|
128
|
+
);
|
|
129
|
+
return shaderArtifacts.length === 1 ? shaderArtifacts[0]?.[0] : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function materialRecordPath(descriptor: ArtifactDescriptor): string {
|
|
133
|
+
return `${descriptor.path}.record.json`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function inlineMaterialArtifact(
|
|
137
|
+
record: Record<string, unknown> | undefined,
|
|
138
|
+
): MaterialPublication['artifact'] | undefined {
|
|
139
|
+
const artifact = record?.artifact;
|
|
140
|
+
if (artifact === null || typeof artifact !== 'object' || Array.isArray(artifact)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const bytes = (artifact as { readonly bytes?: unknown }).bytes;
|
|
144
|
+
const normalizedBytes =
|
|
145
|
+
bytes instanceof Uint8Array
|
|
146
|
+
? new Uint8Array(bytes)
|
|
147
|
+
: Array.isArray(bytes) &&
|
|
148
|
+
bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)
|
|
149
|
+
? Uint8Array.from(bytes)
|
|
150
|
+
: undefined;
|
|
151
|
+
if (normalizedBytes === undefined) return undefined;
|
|
152
|
+
const digest = (artifact as { readonly digest?: unknown }).digest;
|
|
153
|
+
return {
|
|
154
|
+
bytes: normalizedBytes,
|
|
155
|
+
...(typeof digest === 'string' ? { digest } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function loadMaterialPublicationByGuid(
|
|
160
|
+
registry: AssetRegistry,
|
|
161
|
+
request: MaterialLoadRequest,
|
|
162
|
+
): Promise<MaterialPublication | undefined> {
|
|
163
|
+
const parsedGuid = AssetGuid.parse(request.guid);
|
|
164
|
+
if (!parsedGuid.ok) return undefined;
|
|
165
|
+
const entry = await resolveCatalogEntry(registry, request.guid.toLowerCase());
|
|
166
|
+
if (entry === undefined) return undefined;
|
|
167
|
+
let pack = registry.packFileCache.get(entry.packageUrl);
|
|
168
|
+
if (pack === undefined) {
|
|
169
|
+
const fetched = await fetchAndCachePackFile(
|
|
170
|
+
registry,
|
|
171
|
+
entry.packageUrl,
|
|
172
|
+
request.guid.toLowerCase(),
|
|
173
|
+
);
|
|
174
|
+
if (!fetched.ok) return undefined;
|
|
175
|
+
pack = registry.packFileCache.get(entry.packageUrl);
|
|
176
|
+
}
|
|
177
|
+
const asset = pack?.assets.find(
|
|
178
|
+
(candidate) => candidate.guid.toLowerCase() === request.guid.toLowerCase(),
|
|
179
|
+
);
|
|
180
|
+
if (asset === undefined || asset.kind !== 'material') return undefined;
|
|
181
|
+
const record = cookedRecordFromPayload(asset.payload);
|
|
182
|
+
const recordObject =
|
|
183
|
+
record !== undefined && typeof record === 'object' && record !== null
|
|
184
|
+
? (record as Record<string, unknown>)
|
|
185
|
+
: undefined;
|
|
186
|
+
const artifactKey = materialArtifactKey(recordObject, asset.artifacts ?? {});
|
|
187
|
+
if (artifactKey === undefined) {
|
|
188
|
+
const inlineArtifact = inlineMaterialArtifact(recordObject);
|
|
189
|
+
if (inlineArtifact !== undefined) {
|
|
190
|
+
return {
|
|
191
|
+
guid: request.guid,
|
|
192
|
+
record,
|
|
193
|
+
artifact: inlineArtifact,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
guid: request.guid,
|
|
198
|
+
record,
|
|
199
|
+
artifactError: {
|
|
200
|
+
code: 'asset-artifact-missing',
|
|
201
|
+
expected: 'a published material artifact descriptor',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const descriptor = asset.artifacts?.[artifactKey];
|
|
206
|
+
if (descriptor === undefined) {
|
|
207
|
+
return {
|
|
208
|
+
guid: request.guid,
|
|
209
|
+
record,
|
|
210
|
+
artifactError: {
|
|
211
|
+
code: 'asset-artifact-missing',
|
|
212
|
+
expected: `artifact descriptor '${artifactKey}'`,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
let publicationRecord = record;
|
|
217
|
+
if (publicationRecord === undefined) {
|
|
218
|
+
const recordArtifact = await readArtifact({
|
|
219
|
+
packageUrl: entry.packageUrl,
|
|
220
|
+
guid: request.guid,
|
|
221
|
+
artifactKey: `${artifactKey}.record`,
|
|
222
|
+
descriptor: {
|
|
223
|
+
path: materialRecordPath(descriptor),
|
|
224
|
+
mediaType: 'application/json',
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
if (!recordArtifact.ok) return undefined;
|
|
228
|
+
try {
|
|
229
|
+
publicationRecord = JSON.parse(new TextDecoder().decode(recordArtifact.value));
|
|
230
|
+
} catch {
|
|
231
|
+
publicationRecord = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const artifactCacheKey = `${entry.packageUrl}\0${request.guid.toLowerCase()}\0${artifactKey}`;
|
|
235
|
+
const artifact = await registry.artifactCache.read(artifactCacheKey, () =>
|
|
236
|
+
readArtifact({
|
|
237
|
+
packageUrl: entry.packageUrl,
|
|
238
|
+
guid: request.guid,
|
|
239
|
+
artifactKey,
|
|
240
|
+
descriptor,
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
if (!artifact.ok) {
|
|
244
|
+
if (artifact.error.code === 'asset-artifact-missing') {
|
|
245
|
+
return {
|
|
246
|
+
guid: request.guid,
|
|
247
|
+
record: publicationRecord,
|
|
248
|
+
artifactError: {
|
|
249
|
+
code: artifact.error.code,
|
|
250
|
+
expected: artifact.error.expected,
|
|
251
|
+
actual: artifact.error.detail.observed,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (artifact.error.code === 'asset-artifact-integrity-mismatch') {
|
|
256
|
+
return {
|
|
257
|
+
guid: request.guid,
|
|
258
|
+
record: publicationRecord,
|
|
259
|
+
artifactError: {
|
|
260
|
+
code: artifact.error.code,
|
|
261
|
+
expected: artifact.error.expected,
|
|
262
|
+
actual: artifact.error.detail.observed,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
guid: request.guid,
|
|
268
|
+
record: publicationRecord,
|
|
269
|
+
artifactError: {
|
|
270
|
+
code: 'asset-artifact-missing',
|
|
271
|
+
expected: artifact.error.expected,
|
|
272
|
+
actual: artifact.error.detail.observed,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
guid: request.guid,
|
|
278
|
+
record: publicationRecord,
|
|
279
|
+
artifact: {
|
|
280
|
+
bytes: artifact.value,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function loadMaterialReadyByGuid(
|
|
286
|
+
registry: AssetRegistry,
|
|
287
|
+
request: MaterialLoadRequest,
|
|
288
|
+
): Promise<MaterialReady | MaterialLoadError> {
|
|
289
|
+
const publication = await loadMaterialPublicationByGuid(registry, request);
|
|
290
|
+
return loadMaterialReadyPublication(registry, request, publication);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function publicationSpecializationKey(publication: MaterialPublication | undefined): string {
|
|
294
|
+
const record = publication?.record;
|
|
295
|
+
if (record === null || typeof record !== 'object') return '';
|
|
296
|
+
const specializationKey = (record as { readonly specializationKey?: unknown }).specializationKey;
|
|
297
|
+
return typeof specializationKey === 'string' ? specializationKey : '';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function loadMaterialReadyPublication(
|
|
301
|
+
registry: AssetRegistry,
|
|
302
|
+
request: MaterialLoadRequest,
|
|
303
|
+
publication: MaterialPublication | undefined,
|
|
304
|
+
): Promise<MaterialReady | MaterialLoadError> {
|
|
305
|
+
const loader = createMaterialLoader({
|
|
306
|
+
loadPublication: async () => publication,
|
|
307
|
+
loadReference: async (guid) => {
|
|
308
|
+
const parsed = AssetGuid.parse(guid);
|
|
309
|
+
if (!parsed.ok) return true;
|
|
310
|
+
const result = await loadByGuid(registry, parsed.value);
|
|
311
|
+
return result.ok;
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const readiness = await loader.load(request);
|
|
315
|
+
registry.recordMaterialReadiness(request.guid, readiness);
|
|
316
|
+
return readiness;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function loadByGuidInternal<T = Asset>(
|
|
320
|
+
registry: AssetRegistry,
|
|
321
|
+
guid: AssetGuid,
|
|
322
|
+
parentContext:
|
|
323
|
+
| {
|
|
324
|
+
sceneEntityId?: number;
|
|
325
|
+
componentField?: string;
|
|
326
|
+
}
|
|
327
|
+
| undefined,
|
|
328
|
+
ancestry: ReadonlySet<string>,
|
|
329
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
330
|
+
const guidKey = AssetGuid.format(guid).toLowerCase();
|
|
331
|
+
|
|
332
|
+
// feat-20260614 M8 (D-17): the registry catalogues GUID -> payload and
|
|
333
|
+
// returns the PAYLOAD (never a handle). Fast path: already catalogued
|
|
334
|
+
// (covers dev catalog() + prod cached repeat calls).
|
|
335
|
+
const existing = registry.assetCatalog.get(guidKey);
|
|
336
|
+
if (existing !== undefined) {
|
|
337
|
+
const state = registry.loadState.get(guidKey);
|
|
338
|
+
if (state?.status === 'provisional') {
|
|
339
|
+
// A provisional value is only a legal bridge for a true SCC back-edge.
|
|
340
|
+
// A sibling or unrelated concurrent load must await its owning promise;
|
|
341
|
+
// otherwise the parent reaches promoteReady while the dependency is
|
|
342
|
+
// still provisional and reports a spurious public-readiness failure.
|
|
343
|
+
if (ancestry.has(guidKey)) {
|
|
344
|
+
const provisional = registry.loadState.getProvisional<T>(guidKey);
|
|
345
|
+
if (provisional !== undefined) return ok(provisional);
|
|
346
|
+
} else {
|
|
347
|
+
const inFlight = registry.inFlight.get(guidKey);
|
|
348
|
+
if (inFlight !== undefined) {
|
|
349
|
+
return inFlight as Promise<Result<T, AssetError | ImageError | RhiError>>;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return err(
|
|
353
|
+
new AssetError({
|
|
354
|
+
code: 'asset-parse-failed',
|
|
355
|
+
expected: `GUID ${guidKey} to be promoted to Ready before public load`,
|
|
356
|
+
hint: 'wait for the active load to finish or retry after the referenced assets are ready',
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (state?.status === 'ready' || (state === undefined && registry.packIndexUrl === undefined)) {
|
|
361
|
+
const ready = registry.loadState.getReady<T>(guidKey);
|
|
362
|
+
if (ready !== undefined) return ok(ready);
|
|
363
|
+
if (state === undefined) return ok(existing.payload as T);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// In-flight dedup (D-5 / B-10): if another call is already loading this
|
|
368
|
+
// GUID, return that same Promise — covers (a) concurrent same-GUID calls
|
|
369
|
+
// and (b) cycle A→B→A termination (B reaches A's in-flight entry).
|
|
370
|
+
const inFlightPromise = registry.inFlight.get(guidKey);
|
|
371
|
+
if (inFlightPromise !== undefined) {
|
|
372
|
+
return inFlightPromise as Promise<Result<T, AssetError | ImageError | RhiError>>;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Prod fetch path: only enabled when packIndexUrl is configured.
|
|
376
|
+
if (registry.packIndexUrl !== undefined && typeof globalThis.fetch === 'function') {
|
|
377
|
+
// F22: capture generation snapshot at Promise creation time so the
|
|
378
|
+
// resolve path can detect whether invalidate/invalidateAll was called
|
|
379
|
+
// while the fetch was in flight.
|
|
380
|
+
const genAtStart = registry.generations.get(guidKey) ?? 0;
|
|
381
|
+
const globalGenAtStart = registry.globalGeneration;
|
|
382
|
+
|
|
383
|
+
const promise = (async () => {
|
|
384
|
+
const result = await loadByGuidProd<T>(registry, guid, guidKey, parentContext, ancestry);
|
|
385
|
+
// F22: if the generation counters changed since the Promise was
|
|
386
|
+
// created, discard the result -- the asset was invalidated.
|
|
387
|
+
if (
|
|
388
|
+
genAtStart !== (registry.generations.get(guidKey) ?? 0) ||
|
|
389
|
+
globalGenAtStart !== registry.globalGeneration
|
|
390
|
+
) {
|
|
391
|
+
registry.assetCatalog.delete(guidKey);
|
|
392
|
+
registry.loadState.remove(guidKey);
|
|
393
|
+
return err(
|
|
394
|
+
new AssetError({
|
|
395
|
+
code: 'asset-invalidated',
|
|
396
|
+
expected: `GUID ${guidKey} was invalidated during load`,
|
|
397
|
+
hint: ASSET_ERROR_HINTS['asset-invalidated'],
|
|
398
|
+
}),
|
|
399
|
+
) as Result<T, AssetError | ImageError | RhiError>;
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
})();
|
|
403
|
+
registry.inFlight.set(guidKey, promise);
|
|
404
|
+
try {
|
|
405
|
+
return await promise;
|
|
406
|
+
} finally {
|
|
407
|
+
if (registry.inFlight.get(guidKey) === promise) registry.inFlight.delete(guidKey);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Dev / fallback: synchronous catalogue miss (no network).
|
|
412
|
+
return Promise.resolve(
|
|
413
|
+
err(
|
|
414
|
+
new AssetError({
|
|
415
|
+
code: 'asset-not-found',
|
|
416
|
+
expected: `GUID ${guidKey} catalogued in AssetRegistry`,
|
|
417
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
418
|
+
}),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* feat-20260603-asset-import-loader-injection M1 / w6: load an
|
|
425
|
+
* upstream-branch kind (texture / font) straight from its catalog entry
|
|
426
|
+
* through the injected async loader, then register the produced POD. Replaces
|
|
427
|
+
* the bespoke `loadTextureFromEntry` / `loadFontFromEntry` methods; the decode
|
|
428
|
+
* / glyph-parse logic moved verbatim into the loader bodies (D-2 — loader is
|
|
429
|
+
* pure of `registerWithGuid`, which stays here).
|
|
430
|
+
*/
|
|
431
|
+
export async function loadFromUpstreamEntry<T = Asset>(
|
|
432
|
+
registry: AssetRegistry,
|
|
433
|
+
guidKey: string,
|
|
434
|
+
entry: {
|
|
435
|
+
packageUrl: string;
|
|
436
|
+
kind: string;
|
|
437
|
+
name?: string;
|
|
438
|
+
metadata?: ImageMetadata | undefined;
|
|
439
|
+
},
|
|
440
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
441
|
+
const loader = registry.loaders.get(entry.kind);
|
|
442
|
+
if (loader === undefined) {
|
|
443
|
+
return err(
|
|
444
|
+
new AssetError({
|
|
445
|
+
code: 'loader-not-registered',
|
|
446
|
+
expected: `a loader registered for kind '${entry.kind}'`,
|
|
447
|
+
hint: ASSET_ERROR_HINTS['loader-not-registered'],
|
|
448
|
+
detail: { kind: entry.kind, registeredKinds: registry.loaders.registeredKinds() },
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
const out = loader.load({ ...entry, guidKey }, undefined, makeLoadContext(registry));
|
|
453
|
+
// Upstream-branch loaders are async (Promise<LoaderAsyncResult>).
|
|
454
|
+
const result = (await out) as LoaderAsyncResult;
|
|
455
|
+
if (!result.ok) {
|
|
456
|
+
return err(result.error as AssetError | ImageError | RhiError);
|
|
457
|
+
}
|
|
458
|
+
const guid = AssetGuid.parse(guidKey);
|
|
459
|
+
if (!guid.ok) {
|
|
460
|
+
return err(
|
|
461
|
+
new AssetError({
|
|
462
|
+
code: 'asset-parse-failed',
|
|
463
|
+
expected: `valid GUID ${guidKey}`,
|
|
464
|
+
hint: ASSET_ERROR_HINTS['asset-parse-failed'],
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return registry.catalog(guid.value, result.value) as Result<
|
|
469
|
+
T,
|
|
470
|
+
AssetError | ImageError | RhiError
|
|
471
|
+
>;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Internal: prod fetch path for `loadByGuid`.
|
|
476
|
+
* Fetches pack-index.json (cached), then fetches the pack file, parses the
|
|
477
|
+
* asset payload, and registers it.
|
|
478
|
+
*/
|
|
479
|
+
export async function loadByGuidProd<T = Asset>(
|
|
480
|
+
registry: AssetRegistry,
|
|
481
|
+
guid: AssetGuid,
|
|
482
|
+
guidKey: string,
|
|
483
|
+
parentContext?: {
|
|
484
|
+
sceneEntityId?: number;
|
|
485
|
+
componentField?: string;
|
|
486
|
+
},
|
|
487
|
+
ancestry: ReadonlySet<string> = new Set(),
|
|
488
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
489
|
+
// feat-20260603-asset-import-loader-injection M4 / w31 (AC-19 lazy iron law):
|
|
490
|
+
// wrap the DDC fetch + load path so a DDC miss can be routed through the
|
|
491
|
+
// injected ImportTransport (studio form) or fail-fast with
|
|
492
|
+
// `asset-not-imported` (shipped form, AC-22). The load path after a
|
|
493
|
+
// successful DDC resolve is identical in both forms -- zero branches on
|
|
494
|
+
// `registry.importTransport` (AC-23 key invariant).
|
|
495
|
+
//
|
|
496
|
+
// A DDC miss is: (a) the GUID is absent from the catalog, OR (b) the
|
|
497
|
+
// `.pack.json` fetch returns `asset-not-found` / `asset-fetch-failed`.
|
|
498
|
+
// In case (a) the transport is probed first (the pack-index may have been
|
|
499
|
+
// built before the asset was imported); in case (b) the transport is the
|
|
500
|
+
// only fallback (the pack file is genuinely missing).
|
|
501
|
+
|
|
502
|
+
const entry = await resolveCatalogEntry(registry, guidKey);
|
|
503
|
+
if (entry !== undefined) {
|
|
504
|
+
const projection = resolveRuntimeProjection(guidKey, entry);
|
|
505
|
+
if (!projection.ok) return projection;
|
|
506
|
+
if (projection.value?.lifecycle !== undefined && projection.value.lifecycle !== 'current') {
|
|
507
|
+
return transportOrFail<T>(registry, guid, guidKey, 'asset-not-imported');
|
|
508
|
+
}
|
|
509
|
+
// Catalog hit: try the DDC load path.
|
|
510
|
+
const result = await ddcLoad<T>(registry, guid, guidKey, entry, parentContext, ancestry);
|
|
511
|
+
if (result.ok) return result;
|
|
512
|
+
// DDC miss: only route through transport when the error indicates a
|
|
513
|
+
// missing pack file (not a parse / validation failure inside the pack) or
|
|
514
|
+
// an unimported texture source (feat-20260604 M2 / D-1: import-on-demand).
|
|
515
|
+
// `texture-source-not-imported` is an AssetError, so it passes the
|
|
516
|
+
// `instanceof AssetError` guard naturally. `image-decode-failed` is an
|
|
517
|
+
// ImageError (a genuinely corrupt imported .bin) -- it fails the guard and
|
|
518
|
+
// is therefore never transport-eligible (Risk-1), so a real decode
|
|
519
|
+
// failure is never silently lazy-imported.
|
|
520
|
+
const ddcError = result.error;
|
|
521
|
+
const transportEligible =
|
|
522
|
+
ddcError instanceof AssetError &&
|
|
523
|
+
(ddcError.code === 'asset-not-found' ||
|
|
524
|
+
ddcError.code === 'asset-fetch-failed' ||
|
|
525
|
+
ddcError.code === 'texture-source-not-imported' ||
|
|
526
|
+
// perf-20260706: the raw-container fail-fast (mesh/material/scene whose
|
|
527
|
+
// packageUrl is still a .glb/.gltf/.fbx) surfaces source-not-imported;
|
|
528
|
+
// it is transport-eligible so the import runs once and rewrites the row
|
|
529
|
+
// to .bin/.pack.json (the shipped form, with no transport, fails fast).
|
|
530
|
+
// Distinct from the generic asset-not-imported, which must stay
|
|
531
|
+
// NON-eligible so the parent-missing breadcrumb is never masked.
|
|
532
|
+
ddcError.code === 'source-not-imported');
|
|
533
|
+
if (transportEligible) {
|
|
534
|
+
return transportOrFail<T>(registry, guid, guidKey, ddcError.code);
|
|
535
|
+
}
|
|
536
|
+
return result;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Catalog miss: the GUID is not in the pack-index. In the studio form the
|
|
540
|
+
// import transport can lazily create the missing DDC.
|
|
541
|
+
return transportOrFail<T>(registry, guid, guidKey, 'asset-not-found');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Resolve the catalog entry for a GUID, lazily fetching the pack-index on
|
|
546
|
+
* first call. Returns `undefined` when the GUID is absent from the catalog.
|
|
547
|
+
*/
|
|
548
|
+
export async function resolveCatalogEntry(
|
|
549
|
+
registry: AssetRegistry,
|
|
550
|
+
guidKey: string,
|
|
551
|
+
): Promise<
|
|
552
|
+
| {
|
|
553
|
+
packageUrl: string;
|
|
554
|
+
kind: string;
|
|
555
|
+
name?: string;
|
|
556
|
+
metadata?: ImageMetadata | undefined;
|
|
557
|
+
compression?: AssetCompression;
|
|
558
|
+
}
|
|
559
|
+
| undefined
|
|
560
|
+
> {
|
|
561
|
+
const key = guidKey.toLowerCase();
|
|
562
|
+
// Re-fetch the pack-index when it has never been fetched (=== undefined) OR
|
|
563
|
+
// when the cached Map lacks this GUID. The miss case covers invalidate(guid)
|
|
564
|
+
// round-2 M-A, which deletes the per-GUID index entry (targeted, bystanders
|
|
565
|
+
// survive) without nuking the whole Map to undefined: the next loadByGuid
|
|
566
|
+
// must re-consult the source so the GUID re-resolves and its freshly-cleared
|
|
567
|
+
// body cache re-fetches. A genuinely absent GUID re-fetches once then still
|
|
568
|
+
// misses, falling through to the transport / asset-not-found path as before.
|
|
569
|
+
if (registry.packIndexCache === undefined || !registry.packIndexCache.has(key)) {
|
|
570
|
+
const catalogResult = await fetchPackIndex(registry);
|
|
571
|
+
if (!catalogResult.ok) {
|
|
572
|
+
// Keep packIndexCache === undefined so next resolveCatalogEntry re-enters
|
|
573
|
+
// the fetch path instead of short-circuiting on an empty (polluted) cache.
|
|
574
|
+
if (registry.packIndexCache === undefined) return undefined;
|
|
575
|
+
} else {
|
|
576
|
+
registry.packIndexCache = catalogResult.value;
|
|
577
|
+
registerPackagesFromIndex(registry, registry.packIndexCache);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return registry.packIndexCache?.get(key);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* feat-20260618 M3 (D-2): once the pack-index is parsed, group every row by
|
|
585
|
+
* its `packageUrl` and register each package fully -- all of its GUIDs and
|
|
586
|
+
* their entry display names in one `registerPackage` call. Registering the
|
|
587
|
+
* whole package at once (rather than one GUID per load) means the package
|
|
588
|
+
* cardinality is known up front, so `resolveName` returns the basename for a
|
|
589
|
+
* genuinely single-asset package and the entry name for a multi-asset one,
|
|
590
|
+
* with no incremental 1->N promotion needed on the prod path. The name travels
|
|
591
|
+
* entry -> Package, never through the payload (Risk-3 JSON-roundtrip safety),
|
|
592
|
+
* and covers both the sync (parseAssetPayload) and async (texture/font) loads.
|
|
593
|
+
*/
|
|
594
|
+
export function registerPackagesFromIndex(
|
|
595
|
+
registry: AssetRegistry,
|
|
596
|
+
catalog: Map<
|
|
597
|
+
string,
|
|
598
|
+
{
|
|
599
|
+
packageUrl: string;
|
|
600
|
+
name?: string;
|
|
601
|
+
sourcePath?: string;
|
|
602
|
+
subject?: CatalogSubject;
|
|
603
|
+
}
|
|
604
|
+
>,
|
|
605
|
+
): void {
|
|
606
|
+
const byPath = new Map<string, { guids: string[]; names: Map<string, string> }>();
|
|
607
|
+
for (const [guidKey, entry] of catalog) {
|
|
608
|
+
// Imported outputs are published under generated DDC/package URLs, but
|
|
609
|
+
// their stable package identity is the authored source. Keeping that
|
|
610
|
+
// source path here preserves `sky.hdr` (and equivalent source names) for
|
|
611
|
+
// single-asset lazy imports; authored internal packages retain their
|
|
612
|
+
// package URL semantics.
|
|
613
|
+
const packagePath =
|
|
614
|
+
entry.subject === 'imported-output' && entry.sourcePath !== undefined
|
|
615
|
+
? entry.sourcePath
|
|
616
|
+
: entry.packageUrl;
|
|
617
|
+
let group = byPath.get(packagePath);
|
|
618
|
+
if (group === undefined) {
|
|
619
|
+
group = { guids: [], names: new Map() };
|
|
620
|
+
byPath.set(packagePath, group);
|
|
621
|
+
}
|
|
622
|
+
group.guids.push(guidKey);
|
|
623
|
+
if (entry.name !== undefined) group.names.set(guidKey, entry.name);
|
|
624
|
+
}
|
|
625
|
+
for (const [path, group] of byPath) {
|
|
626
|
+
registry._registerPackage(path, group.guids, group.names);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Load an asset through the DDC (catalog entry -> fetch pack -> loader.load
|
|
632
|
+
* -> register). Returns `Err(asset-not-found)` or `Err(asset-fetch-failed)`
|
|
633
|
+
* on DDC miss (the caller then decides whether to route through the
|
|
634
|
+
* import transport).
|
|
635
|
+
*
|
|
636
|
+
* This path is IDENTICAL in studio and shipped forms -- the only difference
|
|
637
|
+
* between the two is whether `registry.importTransport` exists when the caller
|
|
638
|
+
* falls back to `transportOrFail` (AC-23 key invariant).
|
|
639
|
+
*/
|
|
640
|
+
export async function ddcLoad<T = Asset>(
|
|
641
|
+
registry: AssetRegistry,
|
|
642
|
+
guid: AssetGuid,
|
|
643
|
+
guidKey: string,
|
|
644
|
+
entry: {
|
|
645
|
+
packageUrl: string;
|
|
646
|
+
kind: string;
|
|
647
|
+
name?: string;
|
|
648
|
+
metadata?: ImageMetadata | undefined;
|
|
649
|
+
compression?: AssetCompression;
|
|
650
|
+
},
|
|
651
|
+
parentContext?: {
|
|
652
|
+
sceneEntityId?: number;
|
|
653
|
+
componentField?: string;
|
|
654
|
+
},
|
|
655
|
+
ancestry: ReadonlySet<string> = new Set(),
|
|
656
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
657
|
+
if (typeof entry.packageUrl !== 'string' || entry.packageUrl.length === 0) {
|
|
658
|
+
return err(
|
|
659
|
+
new AssetError({
|
|
660
|
+
code: 'asset-not-imported',
|
|
661
|
+
expected: `catalog entry for GUID ${guidKey} to contain a packageUrl locator`,
|
|
662
|
+
hint: ASSET_ERROR_HINTS['asset-not-imported'],
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const packResult = await loadPackV2Asset(registry, guidKey, entry.packageUrl);
|
|
667
|
+
if (packResult === undefined) {
|
|
668
|
+
return err(
|
|
669
|
+
new AssetError({
|
|
670
|
+
code: 'asset-parse-failed',
|
|
671
|
+
expected: `Pack v2 package for GUID ${guidKey}`,
|
|
672
|
+
hint: 'legacy top-level asset payloads are not accepted',
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (!packResult.ok) {
|
|
677
|
+
return packResult as Result<T, AssetError>;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const asset = packResult.value.asset;
|
|
681
|
+
// feat-20260622 M4 / w12: project the pack-entry refs[] (GUID strings) into
|
|
682
|
+
// AssetRef[] for the envelope. The on-disk pack.json refs[] carries only
|
|
683
|
+
// GUID strings (sourceField / sceneEntityId are stripped at the
|
|
684
|
+
// serialization boundary, w7 D-10), so prod-loaded edges have no per-entity
|
|
685
|
+
// metadata — the scene breadcrumb fallback (buildSceneChildContext) still
|
|
686
|
+
// walks the payload for entity/field detail. Dev-server register paths that
|
|
687
|
+
// carry rich AssetRef[] keep their edge metadata end-to-end.
|
|
688
|
+
const packRefs: readonly AssetRef[] = packResult.value.refs.map((g) => ({ guid: g }));
|
|
689
|
+
|
|
690
|
+
// feat-20260622 M5 / w17 (D-8, R5): the former material parent preload
|
|
691
|
+
// "Path B" (an independent early-return that loaded the parent BEFORE the
|
|
692
|
+
// unified for-loop and carried the precise breadcrumb hint `loading parent
|
|
693
|
+
// material X for child Y`) is folded into the unified envelope.refs
|
|
694
|
+
// for-loop. The parent GUID already rides on the material envelope.refs
|
|
695
|
+
// (gltf-importer w5 writes it; the on-disk pack refs[] carries it as a
|
|
696
|
+
// GUID string -> packRefs above projects it), so the unified for-loop
|
|
697
|
+
// recurses on it like any other edge. Here we only resolve the parent
|
|
698
|
+
// GUID -> AssetGuid and stamp `parent` onto the asset payload (the
|
|
699
|
+
// renderer-facing field read by walkMaterialPassesOverSharedRefs); the
|
|
700
|
+
// parent EDGE load + the `loading parent material X for child Y` breadcrumb
|
|
701
|
+
// + the not-a-material guard all move into the for-loop's
|
|
702
|
+
// sourceField.fieldName==='parent' / parent-edge branch below. No early
|
|
703
|
+
// return: the material registers (register-before-recurse) and its parent
|
|
704
|
+
// edge loads through the same unified path as texture / scene edges.
|
|
705
|
+
let assetToRegister: Asset = asset;
|
|
706
|
+
let parentGuidKey: string | undefined;
|
|
707
|
+
if (
|
|
708
|
+
asset.kind === 'material' &&
|
|
709
|
+
'parentGuid' in (asset as unknown as Record<string, unknown>) &&
|
|
710
|
+
typeof (asset as unknown as Record<string, unknown>).parentGuid === 'string'
|
|
711
|
+
) {
|
|
712
|
+
const parentGuidStr = (asset as unknown as MaterialAsset & { parentGuid: string }).parentGuid;
|
|
713
|
+
const parentGuid = AssetGuid.parse(parentGuidStr);
|
|
714
|
+
if (!parentGuid.ok) {
|
|
715
|
+
return err(
|
|
716
|
+
new AssetError({
|
|
717
|
+
code: 'asset-parse-failed',
|
|
718
|
+
expected: `valid parent GUID for child ${guidKey}`,
|
|
719
|
+
hint: `parent GUID '${parentGuidStr}' is not a valid UUID format`,
|
|
720
|
+
}),
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
parentGuidKey = parentGuidStr.toLowerCase();
|
|
724
|
+
const matAsset = asset as unknown as MaterialAsset & { parentGuid?: string };
|
|
725
|
+
const passes = matAsset.passes;
|
|
726
|
+
const values = matAsset.values;
|
|
727
|
+
assetToRegister = {
|
|
728
|
+
kind: 'material',
|
|
729
|
+
...(passes !== undefined ? { passes } : {}),
|
|
730
|
+
...(values !== undefined ? { values } : {}),
|
|
731
|
+
parent: parentGuid.value,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
registry.loadState.begin(
|
|
736
|
+
guidKey,
|
|
737
|
+
packRefs.map((ref) => ref.guid),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
// The provisional record lets cycle back-edges terminate internally while
|
|
741
|
+
// lookup()/inspect() remain limited to the ready domain.
|
|
742
|
+
// tweak-20260609 M1: catalogue the asset BEFORE recursing into its
|
|
743
|
+
// sub-assets. This way, when a cycle (A→B→A) reaches back to A during
|
|
744
|
+
// B's recursion, A is already catalogued (fast-path hit) and the inFlight
|
|
745
|
+
// Promise for A can be fulfilled. The inFlight entry in `loadByGuid` is
|
|
746
|
+
// the second line of defense — it catches concurrent same-GUID calls
|
|
747
|
+
// before the asset is catalogued.
|
|
748
|
+
const registerResult = registerParsedAsset<T>(registry, guid, assetToRegister, guidKey, packRefs);
|
|
749
|
+
if (!registerResult.ok) {
|
|
750
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, registerResult.error);
|
|
751
|
+
return registerResult;
|
|
752
|
+
}
|
|
753
|
+
const registeredPayload = registerResult.value;
|
|
754
|
+
registry.loadState.resolveAsset(guidKey, registeredPayload);
|
|
755
|
+
|
|
756
|
+
// feat-20260622 M4 / w12 (D-5): the recursion source is the just-catalogued
|
|
757
|
+
// envelope's refs[]. The for-loop is kind-agnostic
|
|
758
|
+
// — every AssetRef carries the GUID to recurse on; scene/material/skin all
|
|
759
|
+
// flow through this one loop. Each edge optionally carries sourceField /
|
|
760
|
+
// sceneEntityId; when present the childContext is built straight from the
|
|
761
|
+
// edge, otherwise the scene branch falls back to walking the payload
|
|
762
|
+
// (buildSceneChildContext) so the prod-path breadcrumb keeps its entity /
|
|
763
|
+
// field detail (on-disk refs[] are GUID-string-only, w7 D-10).
|
|
764
|
+
const envelope = registry.assetCatalog.get(guidKey);
|
|
765
|
+
const refs: readonly AssetRef[] = envelope?.refs ?? [];
|
|
766
|
+
if (refs.length > 0) {
|
|
767
|
+
const subResults = await Promise.all(
|
|
768
|
+
refs.map((ref) => {
|
|
769
|
+
const refGuidKey = ref.guid.toLowerCase();
|
|
770
|
+
const parsedRef = AssetGuid.parse(ref.guid);
|
|
771
|
+
if (!parsedRef.ok) {
|
|
772
|
+
return Promise.resolve({
|
|
773
|
+
guidKey: refGuidKey,
|
|
774
|
+
result: err(
|
|
775
|
+
new AssetError({
|
|
776
|
+
code: 'asset-parse-failed',
|
|
777
|
+
expected: `valid sub-asset GUID referenced by ${asset.kind} ${guidKey}`,
|
|
778
|
+
hint: `refs[] entry '${ref.guid}' is not a valid UUID format`,
|
|
779
|
+
}),
|
|
780
|
+
) as Result<Asset, AssetError | ImageError | RhiError>,
|
|
781
|
+
childContext: undefined as
|
|
782
|
+
| {
|
|
783
|
+
sceneEntityId?: number;
|
|
784
|
+
componentField?: string;
|
|
785
|
+
sourceField?: {
|
|
786
|
+
componentName?: string;
|
|
787
|
+
fieldName: string;
|
|
788
|
+
arrayIndex?: number;
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
| undefined,
|
|
792
|
+
isParentEdge: false,
|
|
793
|
+
edge: ref,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
let childContext:
|
|
797
|
+
| {
|
|
798
|
+
sceneEntityId?: number;
|
|
799
|
+
componentField?: string;
|
|
800
|
+
sourceField?: {
|
|
801
|
+
componentName?: string;
|
|
802
|
+
fieldName: string;
|
|
803
|
+
arrayIndex?: number;
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
| undefined;
|
|
807
|
+
if (ref.sceneEntityId !== undefined || ref.sourceField !== undefined) {
|
|
808
|
+
childContext = {};
|
|
809
|
+
if (ref.sceneEntityId !== undefined) childContext.sceneEntityId = ref.sceneEntityId;
|
|
810
|
+
if (ref.sourceField?.fieldName !== undefined) {
|
|
811
|
+
childContext.componentField =
|
|
812
|
+
(ref.sourceField.componentName !== undefined
|
|
813
|
+
? `${ref.sourceField.componentName}.`
|
|
814
|
+
: '') +
|
|
815
|
+
ref.sourceField.fieldName +
|
|
816
|
+
(ref.sourceField.arrayIndex !== undefined ? `[${ref.sourceField.arrayIndex}]` : '');
|
|
817
|
+
childContext.sourceField = ref.sourceField;
|
|
818
|
+
}
|
|
819
|
+
} else if (asset.kind === 'scene') {
|
|
820
|
+
childContext = buildSceneChildContext(registry, asset, refGuidKey, guidKey);
|
|
821
|
+
}
|
|
822
|
+
// feat-20260622 M5 / w17 (D-8): the material parent edge. Identify it
|
|
823
|
+
// by either the rich dev-path marker (sourceField.fieldName==='parent')
|
|
824
|
+
// or the prod-path GUID match against the resolved parent GUID
|
|
825
|
+
// (on-disk refs[] strip sourceField, w7 D-10, so the GUID is the only
|
|
826
|
+
// signal). The parent edge carries the distinct `loading parent
|
|
827
|
+
// material X for child Y` breadcrumb (AC-10) instead of the generic
|
|
828
|
+
// buildBreadcrumbHint form, and is guarded to be a material.
|
|
829
|
+
const isParentEdge =
|
|
830
|
+
asset.kind === 'material' &&
|
|
831
|
+
(ref.sourceField?.fieldName === 'parent' ||
|
|
832
|
+
(parentGuidKey !== undefined && refGuidKey === parentGuidKey));
|
|
833
|
+
const childAncestry = new Set(ancestry);
|
|
834
|
+
childAncestry.add(guidKey);
|
|
835
|
+
return loadByGuidInternal(
|
|
836
|
+
registry,
|
|
837
|
+
parsedRef.value,
|
|
838
|
+
childContext ?? parentContext,
|
|
839
|
+
childAncestry,
|
|
840
|
+
).then((r) => ({
|
|
841
|
+
guidKey: refGuidKey,
|
|
842
|
+
result: r,
|
|
843
|
+
childContext,
|
|
844
|
+
isParentEdge,
|
|
845
|
+
edge: ref,
|
|
846
|
+
}));
|
|
847
|
+
}),
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// If any sub-asset load failed, propagate the first error enriched with
|
|
851
|
+
// parent breadcrumb.
|
|
852
|
+
for (const {
|
|
853
|
+
guidKey: subGuidKey,
|
|
854
|
+
result: subResult,
|
|
855
|
+
childContext: subChildContext,
|
|
856
|
+
isParentEdge,
|
|
857
|
+
edge: subEdge,
|
|
858
|
+
} of subResults) {
|
|
859
|
+
// feat-20260622 M5 / w17: parent-edge breadcrumb migration (former Path
|
|
860
|
+
// B). On load failure, carry the distinct `loading parent material X for
|
|
861
|
+
// child Y: <subErr.hint>` form (AC-10 downstream literal assertion) and
|
|
862
|
+
// propagate the parent's own error code verbatim.
|
|
863
|
+
if (isParentEdge && !subResult.ok) {
|
|
864
|
+
const subErr = subResult.error;
|
|
865
|
+
const code: AssetErrorCode =
|
|
866
|
+
subErr instanceof AssetError ? subErr.code : 'asset-parse-failed';
|
|
867
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, subErr);
|
|
868
|
+
return err(
|
|
869
|
+
new AssetError({
|
|
870
|
+
code,
|
|
871
|
+
expected: subErr.expected,
|
|
872
|
+
hint: `loading parent material ${subGuidKey} for child ${guidKey}: ${
|
|
873
|
+
subErr.hint ?? ''
|
|
874
|
+
}`,
|
|
875
|
+
...(subErr instanceof AssetError && subErr.detail !== undefined
|
|
876
|
+
? { detail: subErr.detail as Readonly<AssetErrorDetail> }
|
|
877
|
+
: {}),
|
|
878
|
+
}),
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
// feat-20260622 M5 / w17: parent edge loaded but is not a material —
|
|
882
|
+
// same guard the former Path B carried, with the matching breadcrumb.
|
|
883
|
+
if (isParentEdge && subResult.ok && subResult.value?.kind !== 'material') {
|
|
884
|
+
const error = new AssetError({
|
|
885
|
+
code: 'asset-parse-failed',
|
|
886
|
+
expected: `parent GUID ${subGuidKey} to reference a MaterialAsset`,
|
|
887
|
+
hint: `loading parent material ${subGuidKey} for child ${guidKey}: referenced asset is ${subResult.value?.kind ?? 'unknown'}, not 'material'`,
|
|
888
|
+
});
|
|
889
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, error);
|
|
890
|
+
return err(error);
|
|
891
|
+
}
|
|
892
|
+
if (!subResult.ok) {
|
|
893
|
+
const subErr = subResult.error;
|
|
894
|
+
const breadcrumb = buildBreadcrumbHint(
|
|
895
|
+
guidKey,
|
|
896
|
+
asset.kind,
|
|
897
|
+
subGuidKey,
|
|
898
|
+
subChildContext ?? parentContext,
|
|
899
|
+
);
|
|
900
|
+
const code: AssetErrorCode =
|
|
901
|
+
subErr instanceof AssetError ? subErr.code : 'asset-fetch-failed';
|
|
902
|
+
// feat-20260622 verify r1: deliver the breadcrumb provenance in
|
|
903
|
+
// structured form so AI users locate the broken edge by property
|
|
904
|
+
// access (charter P3), not by parsing the hint. Preserve the sub
|
|
905
|
+
// error's own detail when it carries one (more specific); otherwise
|
|
906
|
+
// expose the edge provenance (entity / source field).
|
|
907
|
+
// Prefer the rich dev-path edge provenance; on the prod path the
|
|
908
|
+
// on-disk edge is GUID-only (sourceField stripped, w7 D-10), so fall
|
|
909
|
+
// back to the entity-walk-recovered provenance carried on the
|
|
910
|
+
// childContext (verify r1).
|
|
911
|
+
const provEntityId = subEdge?.sceneEntityId ?? subChildContext?.sceneEntityId;
|
|
912
|
+
const provSourceField = subEdge?.sourceField ?? subChildContext?.sourceField;
|
|
913
|
+
const breadcrumbDetail: Readonly<AssetErrorDetail> = {
|
|
914
|
+
referencedByGuid: guidKey,
|
|
915
|
+
referencedByKind: asset.kind,
|
|
916
|
+
subAssetGuid: subGuidKey,
|
|
917
|
+
...(provEntityId !== undefined ? { sceneEntityId: provEntityId } : {}),
|
|
918
|
+
...(provSourceField !== undefined ? { sourceField: provSourceField } : {}),
|
|
919
|
+
};
|
|
920
|
+
const detail: Readonly<AssetErrorDetail> =
|
|
921
|
+
subErr instanceof AssetError && subErr.detail !== undefined
|
|
922
|
+
? subErr.detail
|
|
923
|
+
: breadcrumbDetail;
|
|
924
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, subErr);
|
|
925
|
+
return err(
|
|
926
|
+
new AssetError({
|
|
927
|
+
code,
|
|
928
|
+
expected: subErr.expected,
|
|
929
|
+
hint: `${breadcrumb} / ${subErr.hint ?? ''}`,
|
|
930
|
+
detail,
|
|
931
|
+
}),
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// A Mesh default is a typed dependency, not merely a loadable GUID. This
|
|
938
|
+
// check deliberately lives outside the `refs.length > 0` branch: a producer
|
|
939
|
+
// that puts a default GUID in the payload but omits the matching refs[] edge
|
|
940
|
+
// must fail closed instead of publishing a Mesh whose dependency closure is
|
|
941
|
+
// incomplete.
|
|
942
|
+
if (asset.kind === 'mesh') {
|
|
943
|
+
const directRefGuids = new Set(refs.map((ref) => ref.guid.toLowerCase()));
|
|
944
|
+
const slots = (asset as MeshAsset).materialSlots;
|
|
945
|
+
for (let slotIndex = 0; slotIndex < slots.length; slotIndex++) {
|
|
946
|
+
const slot = slots[slotIndex];
|
|
947
|
+
if (slot?.defaultMaterial === undefined) continue;
|
|
948
|
+
const defaultMaterialGuid = AssetGuid.format(slot.defaultMaterial).toLowerCase();
|
|
949
|
+
const loaded = directRefGuids.has(defaultMaterialGuid)
|
|
950
|
+
? registry.assetCatalog.get(defaultMaterialGuid)?.payload
|
|
951
|
+
: undefined;
|
|
952
|
+
if (loaded?.kind === 'material') continue;
|
|
953
|
+
const actualKind = directRefGuids.has(defaultMaterialGuid)
|
|
954
|
+
? (loaded?.kind ?? 'missing')
|
|
955
|
+
: 'missing-ref-edge';
|
|
956
|
+
const error = new AssetError({
|
|
957
|
+
code: 'asset-parse-failed',
|
|
958
|
+
expected: `mesh ${guidKey} materialSlots[${slotIndex}] (${slot.slotName}) default ${defaultMaterialGuid} to reference a MaterialAsset`,
|
|
959
|
+
hint: `recook mesh ${guidKey}; slot ${slotIndex} '${slot.slotName}' resolves to ${actualKind}, not 'material'`,
|
|
960
|
+
detail: {
|
|
961
|
+
meshAssetGuid: guidKey,
|
|
962
|
+
slotIndex,
|
|
963
|
+
slotName: slot.slotName,
|
|
964
|
+
defaultMaterialGuid,
|
|
965
|
+
actualKind,
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, error);
|
|
969
|
+
return err(error);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
registry.loadState.promoteReady(guidKey);
|
|
974
|
+
if (registry.loadState.getReady(guidKey) === undefined) {
|
|
975
|
+
const error = new AssetError({
|
|
976
|
+
code: 'asset-parse-failed',
|
|
977
|
+
expected: `GUID ${guidKey} and all referenced assets to be public-ready`,
|
|
978
|
+
hint: 'retry after every referenced GUID has loaded successfully',
|
|
979
|
+
});
|
|
980
|
+
purgeFailedLoad(registry, guidKey, entry.packageUrl, error);
|
|
981
|
+
return err(error);
|
|
982
|
+
}
|
|
983
|
+
const registeredAsset = registeredPayload as Asset;
|
|
984
|
+
if (registeredAsset.kind === 'material' && !isEngineMaterial(registeredAsset)) {
|
|
985
|
+
const publication = await loadMaterialPublicationByGuid(registry, {
|
|
986
|
+
guid: guidKey,
|
|
987
|
+
specializationKey: '',
|
|
988
|
+
});
|
|
989
|
+
await loadMaterialReadyPublication(
|
|
990
|
+
registry,
|
|
991
|
+
{
|
|
992
|
+
guid: guidKey,
|
|
993
|
+
specializationKey: publicationSpecializationKey(publication),
|
|
994
|
+
},
|
|
995
|
+
publication,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
return ok(registeredPayload as T);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async function loadPackV2Asset(
|
|
1002
|
+
registry: AssetRegistry,
|
|
1003
|
+
guidKey: string,
|
|
1004
|
+
packageUrl: string,
|
|
1005
|
+
): Promise<
|
|
1006
|
+
| Result<
|
|
1007
|
+
{
|
|
1008
|
+
asset: Asset;
|
|
1009
|
+
refs: readonly string[];
|
|
1010
|
+
},
|
|
1011
|
+
AssetError
|
|
1012
|
+
>
|
|
1013
|
+
| undefined
|
|
1014
|
+
> {
|
|
1015
|
+
const cached = registry.packFileCache.get(packageUrl);
|
|
1016
|
+
if (cached === undefined) {
|
|
1017
|
+
const inFlight = registry.packFileInFlight.get(packageUrl);
|
|
1018
|
+
if (inFlight !== undefined) await inFlight.catch(() => undefined);
|
|
1019
|
+
const fetched =
|
|
1020
|
+
registry.packFileCache.get(packageUrl) === undefined
|
|
1021
|
+
? await fetchAndCachePackFile(registry, packageUrl, guidKey)
|
|
1022
|
+
: undefined;
|
|
1023
|
+
if (registry.packFileCache.get(packageUrl) === undefined) {
|
|
1024
|
+
if (fetched === undefined) {
|
|
1025
|
+
return err(
|
|
1026
|
+
new AssetError({
|
|
1027
|
+
code: 'asset-fetch-failed',
|
|
1028
|
+
expected: `pack file ${packageUrl} to be cached after its shared fetch`,
|
|
1029
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1030
|
+
}),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
return fetched as unknown as Result<{ asset: Asset; refs: readonly string[] }, AssetError>;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const pack = registry.packFileCache.get(packageUrl);
|
|
1037
|
+
if (pack?.schemaVersion !== '2.0.0') return undefined;
|
|
1038
|
+
const asset = pack.assets.find((candidate) => candidate.guid.toLowerCase() === guidKey);
|
|
1039
|
+
if (asset === undefined) {
|
|
1040
|
+
return err(
|
|
1041
|
+
new AssetError({
|
|
1042
|
+
code: 'asset-not-found',
|
|
1043
|
+
expected: `GUID ${guidKey} present in Pack v2 package ${packageUrl}`,
|
|
1044
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1045
|
+
}),
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const artifacts: Record<string, { descriptor: ArtifactDescriptor; bytes: Uint8Array }> = {};
|
|
1049
|
+
for (const [artifactKey, descriptor] of Object.entries(asset.artifacts ?? {})) {
|
|
1050
|
+
const cacheKey = `${packageUrl}\0${guidKey}\0${artifactKey}`;
|
|
1051
|
+
const artifact = await registry.artifactCache.read(cacheKey, () =>
|
|
1052
|
+
readArtifact({ packageUrl, guid: guidKey, artifactKey, descriptor }),
|
|
1053
|
+
);
|
|
1054
|
+
if (!artifact.ok)
|
|
1055
|
+
return artifact as unknown as Result<{ asset: Asset; refs: readonly string[] }, AssetError>;
|
|
1056
|
+
artifacts[artifactKey] = { descriptor, bytes: artifact.value };
|
|
1057
|
+
}
|
|
1058
|
+
const loaded = await registry.loaders.loadPack(
|
|
1059
|
+
{
|
|
1060
|
+
guid: guidKey,
|
|
1061
|
+
kind: asset.kind,
|
|
1062
|
+
payload: asset.payload,
|
|
1063
|
+
refs: asset.refs ?? [],
|
|
1064
|
+
artifacts,
|
|
1065
|
+
},
|
|
1066
|
+
makeLoadContext(registry),
|
|
1067
|
+
);
|
|
1068
|
+
if (!loaded.ok) return err(loaded.error as AssetError);
|
|
1069
|
+
if (loaded.value === undefined || typeof loaded.value !== 'object') {
|
|
1070
|
+
return err(
|
|
1071
|
+
new AssetError({
|
|
1072
|
+
code: 'asset-parse-failed',
|
|
1073
|
+
expected: `loader '${asset.kind}' to return an asset payload`,
|
|
1074
|
+
hint: 'register a loader that accepts the Pack v2 asset-local input',
|
|
1075
|
+
}),
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
return ok({ asset: loaded.value as Asset, refs: asset.refs ?? [] });
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function purgeFailedLoad(
|
|
1082
|
+
registry: AssetRegistry,
|
|
1083
|
+
guidKey: string,
|
|
1084
|
+
packageUrl: string,
|
|
1085
|
+
error: unknown,
|
|
1086
|
+
): void {
|
|
1087
|
+
const doomed = registry.loadState.fail(guidKey, error);
|
|
1088
|
+
for (const key of doomed) registry.assetCatalog.delete(key);
|
|
1089
|
+
registry.packFileCache.delete(packageUrl);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* M4 transport fallback: try the injected {@link ImportTransport} to lazily
|
|
1094
|
+
* import a missing DDC, then re-enter the DDC load path. When no transport
|
|
1095
|
+
* is wired (shipped form), fail fast with `asset-not-imported` (AC-22).
|
|
1096
|
+
*/
|
|
1097
|
+
export async function transportOrFail<T = Asset>(
|
|
1098
|
+
registry: AssetRegistry,
|
|
1099
|
+
guid: AssetGuid,
|
|
1100
|
+
guidKey: string,
|
|
1101
|
+
_missReason: AssetErrorCode,
|
|
1102
|
+
): Promise<Result<T, AssetError | ImageError | RhiError>> {
|
|
1103
|
+
if (registry.importTransport === undefined) {
|
|
1104
|
+
// shipped form: no transport wired -> fail fast, never degrade to
|
|
1105
|
+
// runtime import (AC-22, charter P3 explicit failure).
|
|
1106
|
+
return err(
|
|
1107
|
+
new AssetError({
|
|
1108
|
+
code: 'asset-not-imported',
|
|
1109
|
+
expected: `GUID ${guidKey} to have been pre-imported at build time or to have an ImportTransport wired`,
|
|
1110
|
+
hint: ASSET_ERROR_HINTS['asset-not-imported'],
|
|
1111
|
+
}),
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// studio form: request the transport to import this GUID on-the-fly.
|
|
1116
|
+
// After a successful transport call the DDC is available; re-enter the
|
|
1117
|
+
// catalog + DDC load path (the transport writes the DDC but does NOT
|
|
1118
|
+
// register the asset — that's the Loader's job).
|
|
1119
|
+
const transportResult = await registry.importTransport.fetchPack(
|
|
1120
|
+
guidKey,
|
|
1121
|
+
registry.runtimeBinding,
|
|
1122
|
+
);
|
|
1123
|
+
if (!transportResult.ok) {
|
|
1124
|
+
return err(
|
|
1125
|
+
new AssetError({
|
|
1126
|
+
code: 'asset-not-imported',
|
|
1127
|
+
expected: `import transport to fetch pack for GUID ${guidKey}`,
|
|
1128
|
+
hint: ASSET_ERROR_HINTS['asset-not-imported'],
|
|
1129
|
+
}),
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Patch ONLY the freshly imported rows into the catalog cache (per-asset
|
|
1134
|
+
// incremental, the four-verb redesign 2026-06-06) instead of nuking the
|
|
1135
|
+
// cache and re-fetching the whole pack-index. The transport returns the one
|
|
1136
|
+
// imported entry (+ sub-asset siblings); each becomes / overwrites a cache
|
|
1137
|
+
// row. This keeps 122 concurrent texture imports O(N) instead of O(N^2)
|
|
1138
|
+
// whole-catalog re-fetches and never resets a sibling's imported row.
|
|
1139
|
+
const importedEntries = 'entries' in transportResult ? transportResult.entries : undefined;
|
|
1140
|
+
if (importedEntries !== undefined && importedEntries.length > 0) {
|
|
1141
|
+
// F20: serialise packIndexCache writes through a per-cache Promise queue.
|
|
1142
|
+
// The "check -> new Map -> set" block is not atomic across concurrent
|
|
1143
|
+
// transportOrFail calls; chaining through the queue ensures each patch
|
|
1144
|
+
// completes before the next starts, preventing new-Map overwrite races.
|
|
1145
|
+
registry.packIndexCachePatchQueue = registry.packIndexCachePatchQueue.then(() => {
|
|
1146
|
+
if (registry.packIndexCache === undefined) registry.packIndexCache = new Map();
|
|
1147
|
+
for (const e of importedEntries) {
|
|
1148
|
+
if (typeof e.packageUrl !== 'string' || e.packageUrl.length === 0) continue;
|
|
1149
|
+
registry.packIndexCache.set(e.guid.toLowerCase(), {
|
|
1150
|
+
packageUrl: resolveCatalogAssetUrl(registry, e.packageUrl),
|
|
1151
|
+
kind: e.kind,
|
|
1152
|
+
// Carry the transport's derived display name into the cache row.
|
|
1153
|
+
// buildCatalog already resolves it (deriveAssetName: basename of the
|
|
1154
|
+
// source for single-/no-storedName sub-assets), so a freshly imported
|
|
1155
|
+
// GLB's 1000+ sub-assets show as "<file>.glb" in the Content Browser
|
|
1156
|
+
// instead of blank. Dropping it here made listCatalog fall back to
|
|
1157
|
+
// `entry.name ?? ''` — the whole-index re-read path (else branch) kept
|
|
1158
|
+
// names, so only the incremental patch path was blank.
|
|
1159
|
+
...(e.name !== undefined ? { name: e.name } : {}),
|
|
1160
|
+
// Carry refs on the incremental patch path too, else an asset
|
|
1161
|
+
// imported via POST /__import shows missing dependency edges until
|
|
1162
|
+
// the next full pack-index refresh (feat: listCatalog refs).
|
|
1163
|
+
...(e.refs !== undefined ? { refs: e.refs } : {}),
|
|
1164
|
+
...(e.packageId !== undefined ? { packageId: e.packageId } : {}),
|
|
1165
|
+
...(e.provenance !== undefined ? { provenance: e.provenance } : {}),
|
|
1166
|
+
...(e.revision !== undefined ? { revision: e.revision } : {}),
|
|
1167
|
+
...(e.authoring !== undefined ? { authoring: e.authoring } : {}),
|
|
1168
|
+
...(e.sourceKey !== undefined ? { sourceKey: e.sourceKey } : {}),
|
|
1169
|
+
...(e.sourceIndex !== undefined ? { sourceIndex: e.sourceIndex } : {}),
|
|
1170
|
+
...(e.relations !== undefined ? { relations: e.relations } : {}),
|
|
1171
|
+
...(e.diagnostics !== undefined ? { diagnostics: e.diagnostics } : {}),
|
|
1172
|
+
// Carry sourcePath on the incremental patch path too (same red-line
|
|
1173
|
+
// as refs above): an asset imported via POST /__import would
|
|
1174
|
+
// otherwise expose no source-file path in listCatalog until the next
|
|
1175
|
+
// full pack-index refresh, breaking editor CRUD sidecar lookup for
|
|
1176
|
+
// freshly imported assets. `sourcePath` is a required PackIndexEntry
|
|
1177
|
+
// field, so it is always present on the transport row.
|
|
1178
|
+
...(e.sourcePath !== undefined ? { sourcePath: e.sourcePath } : {}),
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
await registry.packIndexCachePatchQueue;
|
|
1183
|
+
} else {
|
|
1184
|
+
// No inline rows -- fall back to a full pack-index re-read so the freshly
|
|
1185
|
+
// imported DDC entry is visible (legacy / non-row-returning transports).
|
|
1186
|
+
registry.packIndexCache = undefined;
|
|
1187
|
+
}
|
|
1188
|
+
const entry = await resolveCatalogEntry(registry, guidKey);
|
|
1189
|
+
if (entry === undefined) {
|
|
1190
|
+
return err(
|
|
1191
|
+
new AssetError({
|
|
1192
|
+
code: 'asset-not-imported',
|
|
1193
|
+
expected: `import transport to produce a catalog entry for GUID ${guidKey}`,
|
|
1194
|
+
hint: ASSET_ERROR_HINTS['asset-not-imported'],
|
|
1195
|
+
}),
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Re-enter the DDC load path (identical to the catalog-hit path).
|
|
1200
|
+
return ddcLoad<T>(registry, guid, guidKey, entry);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Register a parsed asset POD (the synchronous tail of the DDC load path:
|
|
1205
|
+
* `registerWithGuid`). Material parent preload is handled asynchronously
|
|
1206
|
+
* inside `ddcLoad` before calling this method; the registered asset is
|
|
1207
|
+
* always fully resolved by the time it reaches here.
|
|
1208
|
+
*
|
|
1209
|
+
* Extracted from the old `loadByGuidProd` body so `ddcLoad` and
|
|
1210
|
+
* `transportOrFail` share an identical load path (AC-23 key invariant).
|
|
1211
|
+
*/
|
|
1212
|
+
export function registerParsedAsset<T = Asset>(
|
|
1213
|
+
registry: AssetRegistry,
|
|
1214
|
+
guid: AssetGuid,
|
|
1215
|
+
asset: Asset,
|
|
1216
|
+
_guidKey: string,
|
|
1217
|
+
refs?: readonly AssetRef[],
|
|
1218
|
+
): Result<T, AssetError | ImageError | RhiError> {
|
|
1219
|
+
// feat-20260614 M8 (D-17): catalogue the parsed payload under its GUID and
|
|
1220
|
+
// return the PAYLOAD. `catalog` validates mesh stride + material passes and
|
|
1221
|
+
// returns Result.err on failure (no throw), so the loadByGuid surface stays
|
|
1222
|
+
// a consistent Result (charter P4 consistent abstraction).
|
|
1223
|
+
//
|
|
1224
|
+
// feat-20260622 M4 / w12 (D-9): the pack-entry refs[] ride onto the
|
|
1225
|
+
// catalogued envelope here so the recursive core can read envelope.refs as
|
|
1226
|
+
// its single recursion source.
|
|
1227
|
+
return registry.catalog<T>(guid, asset as T, refs) as Result<
|
|
1228
|
+
T,
|
|
1229
|
+
AssetError | ImageError | RhiError
|
|
1230
|
+
>;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Fetch a .pack.json file, find the asset entry matching guidKey, and
|
|
1235
|
+
* reconstruct the Asset from its payload.
|
|
1236
|
+
*/
|
|
1237
|
+
/**
|
|
1238
|
+
* bug-20260610: fetch one pack file and return the raw asset entry without
|
|
1239
|
+
* parsing. Used by `loadByGuidProd` for material kinds so the caller can
|
|
1240
|
+
* recursively preload `refs[]` (texture sub-assets) BEFORE the synchronous
|
|
1241
|
+
* materialLoader runs and rewrites values handle fields to their refs[]
|
|
1242
|
+
* GUID strings (feat-20260614 M8 / D-19: GUID verbatim, no handle minting).
|
|
1243
|
+
*/
|
|
1244
|
+
export async function fetchPackEntry(
|
|
1245
|
+
_registry: AssetRegistry,
|
|
1246
|
+
packageUrl: string,
|
|
1247
|
+
guidKey: string,
|
|
1248
|
+
): Promise<
|
|
1249
|
+
Result<{ kind: string; payload: Record<string, unknown>; refs?: string[] }, AssetError>
|
|
1250
|
+
> {
|
|
1251
|
+
let raw: unknown;
|
|
1252
|
+
try {
|
|
1253
|
+
const res = await globalThis.fetch(packageUrl);
|
|
1254
|
+
if (!res.ok) {
|
|
1255
|
+
return err(
|
|
1256
|
+
new AssetError({
|
|
1257
|
+
code: 'asset-fetch-failed',
|
|
1258
|
+
expected: `fetch(${packageUrl}) to return ok`,
|
|
1259
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1260
|
+
}),
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
raw = (await res.json()) as unknown;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return err(
|
|
1266
|
+
new AssetError({
|
|
1267
|
+
code: 'asset-fetch-failed',
|
|
1268
|
+
expected: `fetch(${packageUrl}) to succeed`,
|
|
1269
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1270
|
+
}),
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const packFile = raw as {
|
|
1274
|
+
assets?: Array<{
|
|
1275
|
+
guid: string;
|
|
1276
|
+
kind: string;
|
|
1277
|
+
payload: Record<string, unknown>;
|
|
1278
|
+
refs?: string[];
|
|
1279
|
+
}>;
|
|
1280
|
+
};
|
|
1281
|
+
const assetEntry = (packFile.assets ?? []).find(
|
|
1282
|
+
(a) => a.guid.toLowerCase() === guidKey.toLowerCase(),
|
|
1283
|
+
);
|
|
1284
|
+
if (assetEntry === undefined) {
|
|
1285
|
+
return err(
|
|
1286
|
+
new AssetError({
|
|
1287
|
+
code: 'asset-not-found',
|
|
1288
|
+
expected: `GUID ${guidKey} present in pack file ${packageUrl}`,
|
|
1289
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1290
|
+
}),
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
return ok({
|
|
1294
|
+
kind: assetEntry.kind,
|
|
1295
|
+
payload: assetEntry.payload,
|
|
1296
|
+
...(assetEntry.refs !== undefined ? { refs: assetEntry.refs } : {}),
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Fetch one pack file, locate the requested asset entry, and either parse it
|
|
1302
|
+
* inline or expose the entry to the caller (for kinds that need to preload
|
|
1303
|
+
* `refs[]` BEFORE running the loader — currently 'material', whose
|
|
1304
|
+
* values handle fields are rewritten to their refs[] GUID strings
|
|
1305
|
+
* (feat-20260614 M8 / D-19: GUID verbatim, no handle minting at load time)).
|
|
1306
|
+
*
|
|
1307
|
+
* bug-20260610 Fix B (M3 / D-4): the fetch+parse result is cached per
|
|
1308
|
+
* `packageUrl` in `packFileCache`; concurrent calls for the same URL share
|
|
1309
|
+
* a single in-flight promise via `packFileInFlight`. Only the raw parsed
|
|
1310
|
+
* body is cached — `parseAssetPayload` still runs per-call (CON-2).
|
|
1311
|
+
*/
|
|
1312
|
+
export async function fetchPackFile(
|
|
1313
|
+
registry: AssetRegistry,
|
|
1314
|
+
packageUrl: string,
|
|
1315
|
+
guidKey: string,
|
|
1316
|
+
_kind: string,
|
|
1317
|
+
): Promise<Result<{ asset: Asset; refs: readonly string[] }, AssetError>> {
|
|
1318
|
+
// ── cache hit ───────────────────────────────────────────────────────
|
|
1319
|
+
const cached = registry.packFileCache.get(packageUrl);
|
|
1320
|
+
if (cached !== undefined) {
|
|
1321
|
+
const assetEntry = cached.assets.find((a) => a.guid.toLowerCase() === guidKey.toLowerCase());
|
|
1322
|
+
if (assetEntry === undefined) {
|
|
1323
|
+
return err(
|
|
1324
|
+
new AssetError({
|
|
1325
|
+
code: 'asset-not-found',
|
|
1326
|
+
expected: `GUID ${guidKey} present in pack file ${packageUrl}`,
|
|
1327
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1328
|
+
}),
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return parseAndReturnAsset(registry, assetEntry);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// ── in-flight dedup ─────────────────────────────────────────────────
|
|
1335
|
+
const inFlight = registry.packFileInFlight.get(packageUrl);
|
|
1336
|
+
if (inFlight !== undefined) {
|
|
1337
|
+
try {
|
|
1338
|
+
const packFile = await inFlight;
|
|
1339
|
+
const assetEntry = packFile.assets.find(
|
|
1340
|
+
(a) => a.guid.toLowerCase() === guidKey.toLowerCase(),
|
|
1341
|
+
);
|
|
1342
|
+
if (assetEntry === undefined) {
|
|
1343
|
+
return err(
|
|
1344
|
+
new AssetError({
|
|
1345
|
+
code: 'asset-not-found',
|
|
1346
|
+
expected: `GUID ${guidKey} present in pack file ${packageUrl}`,
|
|
1347
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1348
|
+
}),
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
return parseAndReturnAsset(registry, assetEntry);
|
|
1352
|
+
} catch {
|
|
1353
|
+
// In-flight promise rejected (network failure) — fall through to
|
|
1354
|
+
// re-fetch. The in-flight entry was already cleaned by the
|
|
1355
|
+
// catch block in the original miss path.
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ── miss: fetch + parse + cache ─────────────────────────────────────
|
|
1360
|
+
return fetchAndCachePackFile(registry, packageUrl, guidKey);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Parse the asset payload from a pack-file entry and return the result.
|
|
1365
|
+
* Extracted so cache-hit and in-flight-dedup paths share the same
|
|
1366
|
+
* parseAssetPayload + error-wrapping logic.
|
|
1367
|
+
*/
|
|
1368
|
+
export function parseAndReturnAsset(
|
|
1369
|
+
registry: AssetRegistry,
|
|
1370
|
+
assetEntry: {
|
|
1371
|
+
kind: string;
|
|
1372
|
+
payload: Record<string, unknown>;
|
|
1373
|
+
refs?: string[];
|
|
1374
|
+
},
|
|
1375
|
+
): Result<{ asset: Asset; refs: readonly string[] }, AssetError> {
|
|
1376
|
+
const parsed = parseAssetPayload(registry, assetEntry.kind, assetEntry.payload, assetEntry.refs);
|
|
1377
|
+
// F21: the scene loader returns its structured ParseErrorDetail inline via
|
|
1378
|
+
// the LoaderOutput `{ ok: false, error }` arm, surfaced here through
|
|
1379
|
+
// parseAssetPayload's return value -- no shared instance slot.
|
|
1380
|
+
if (parsed !== undefined && typeof parsed === 'object' && 'ok' in parsed) {
|
|
1381
|
+
const e = (parsed as { readonly ok: false; readonly error: ParseErrorDetail }).error;
|
|
1382
|
+
return err(
|
|
1383
|
+
new AssetError({
|
|
1384
|
+
code: 'asset-parse-failed',
|
|
1385
|
+
expected: `refs index ${e.index} within [0, ${e.refsLength})`,
|
|
1386
|
+
detail: {
|
|
1387
|
+
localId: e.localId,
|
|
1388
|
+
component: e.component,
|
|
1389
|
+
field: e.field,
|
|
1390
|
+
index: e.index,
|
|
1391
|
+
refsLength: e.refsLength,
|
|
1392
|
+
},
|
|
1393
|
+
hint:
|
|
1394
|
+
`at node localId=${e.localId}, component=${e.component}, ` +
|
|
1395
|
+
`field=${e.field}: index ${e.index} is out of bounds ` +
|
|
1396
|
+
`(refs has ${e.refsLength} entries)`,
|
|
1397
|
+
}),
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
if (parsed === undefined) {
|
|
1401
|
+
return err(
|
|
1402
|
+
new AssetError({
|
|
1403
|
+
code: 'asset-parse-failed',
|
|
1404
|
+
expected: `parseable asset payload for kind ${assetEntry.kind}`,
|
|
1405
|
+
hint: ASSET_ERROR_HINTS['asset-parse-failed'],
|
|
1406
|
+
}),
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
// feat-20260622 M4 / w12: surface the pack-entry refs[] (GUID-string
|
|
1410
|
+
// projection) alongside the parsed payload so ddcLoad can store them on
|
|
1411
|
+
// the catalogued envelope. The recursive core then reads envelope.refs
|
|
1412
|
+
// as the single recursion source (D-5), never re-deriving them from
|
|
1413
|
+
// the payload.
|
|
1414
|
+
return ok({ asset: parsed as Asset, refs: assetEntry.refs ?? [] });
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Fetch a pack file from the network, parse the JSON body, store the
|
|
1419
|
+
* result in the cache, and return the requested asset entry.
|
|
1420
|
+
*
|
|
1421
|
+
* Registers the in-flight promise in `packFileInFlight` so concurrent
|
|
1422
|
+
* callers share a single fetch. On success the body moves to
|
|
1423
|
+
* `packFileCache`; on failure the in-flight entry is removed so
|
|
1424
|
+
* subsequent retries re-fetch (D-7).
|
|
1425
|
+
*/
|
|
1426
|
+
export async function fetchAndCachePackFile(
|
|
1427
|
+
registry: AssetRegistry,
|
|
1428
|
+
packageUrl: string,
|
|
1429
|
+
guidKey: string,
|
|
1430
|
+
): Promise<Result<{ asset: Asset; refs: readonly string[] }, AssetError>> {
|
|
1431
|
+
const fetchPromise = (async (): Promise<ParsedPackFile> => {
|
|
1432
|
+
let raw: unknown;
|
|
1433
|
+
try {
|
|
1434
|
+
const res = await globalThis.fetch(packageUrl);
|
|
1435
|
+
if (!res.ok) {
|
|
1436
|
+
throw new AssetError({
|
|
1437
|
+
code: 'asset-fetch-failed',
|
|
1438
|
+
expected: `fetch(${packageUrl}) to return ok`,
|
|
1439
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
raw = (await res.json()) as unknown;
|
|
1443
|
+
} catch (e) {
|
|
1444
|
+
if (e instanceof AssetError) throw e;
|
|
1445
|
+
throw new AssetError({
|
|
1446
|
+
code: 'asset-fetch-failed',
|
|
1447
|
+
expected: `fetch(${packageUrl}) to succeed`,
|
|
1448
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
// Shape guard: the dev-server / preview / 404 fallback can return
|
|
1452
|
+
// index.html or an unrelated JSON body that satisfies res.ok but lacks
|
|
1453
|
+
// the ParsedPackFile contract. Without this guard the downstream
|
|
1454
|
+
// `packFile.assets.find` raises TypeError outside any AssetError
|
|
1455
|
+
// branch, escapes as a process-level Unhandled Rejection, and drives
|
|
1456
|
+
// vitest browser-project exit=1 even when every onerror-gate test
|
|
1457
|
+
// assertion passes (feat-20260611 step-implement F-4).
|
|
1458
|
+
if (
|
|
1459
|
+
raw === null ||
|
|
1460
|
+
typeof raw !== 'object' ||
|
|
1461
|
+
!Array.isArray((raw as { assets?: unknown }).assets)
|
|
1462
|
+
) {
|
|
1463
|
+
throw new AssetError({
|
|
1464
|
+
code: 'asset-fetch-failed',
|
|
1465
|
+
expected: `pack-file body at ${packageUrl} to be { assets: [...] }`,
|
|
1466
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return raw as ParsedPackFile;
|
|
1470
|
+
})();
|
|
1471
|
+
|
|
1472
|
+
registry.packFileInFlight.set(packageUrl, fetchPromise);
|
|
1473
|
+
|
|
1474
|
+
try {
|
|
1475
|
+
const packFile = await fetchPromise;
|
|
1476
|
+
registry.packFileCache.set(packageUrl, packFile);
|
|
1477
|
+
registry.packFileInFlight.delete(packageUrl);
|
|
1478
|
+
|
|
1479
|
+
const assetEntry = packFile.assets.find((a) => a.guid.toLowerCase() === guidKey.toLowerCase());
|
|
1480
|
+
if (assetEntry === undefined) {
|
|
1481
|
+
return err(
|
|
1482
|
+
new AssetError({
|
|
1483
|
+
code: 'asset-not-found',
|
|
1484
|
+
expected: `GUID ${guidKey} present in pack file ${packageUrl}`,
|
|
1485
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
1486
|
+
}),
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
return parseAndReturnAsset(registry, assetEntry);
|
|
1490
|
+
} catch (e) {
|
|
1491
|
+
registry.packFileInFlight.delete(packageUrl);
|
|
1492
|
+
if (e instanceof AssetError) {
|
|
1493
|
+
return err(e);
|
|
1494
|
+
}
|
|
1495
|
+
throw e;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Reconstruct a typed `Asset` from a raw payload object.
|
|
1501
|
+
*
|
|
1502
|
+
* @param kind The asset kind discriminant (matches the pack entry or
|
|
1503
|
+
* dev-register dispatch).
|
|
1504
|
+
* @param payload The serialised asset payload (keys mirror the asset
|
|
1505
|
+
* interface field names).
|
|
1506
|
+
* @param refs Pack-file refs array for Handle fields — when a field
|
|
1507
|
+
* value is `number` it resolves to `refs[N]` (glTF-style index).
|
|
1508
|
+
* Optional to preserve compatibility with callers outside the pack
|
|
1509
|
+
* ingestion path (e.g., direct `registerWithGuid`).
|
|
1510
|
+
*/
|
|
1511
|
+
export function parseAssetPayload(
|
|
1512
|
+
registry: AssetRegistry,
|
|
1513
|
+
kind: string,
|
|
1514
|
+
payload: Record<string, unknown>,
|
|
1515
|
+
refs?: string[],
|
|
1516
|
+
):
|
|
1517
|
+
| Asset
|
|
1518
|
+
| Record<string, unknown>
|
|
1519
|
+
| undefined
|
|
1520
|
+
| { readonly ok: false; readonly error: ParseErrorDetail } {
|
|
1521
|
+
// feat-20260603-asset-import-loader-injection M1 / w4: dispatch on
|
|
1522
|
+
// `kind` through the injected LoaderRegistry instead of a hardcoded
|
|
1523
|
+
// `if (kind === ...)` chain (D-1 / AC-01). The seven inline pack-payload
|
|
1524
|
+
// loaders parse synchronously; texture / font live on the upstream
|
|
1525
|
+
// loadByGuidProd branch (w6) and are never reached here.
|
|
1526
|
+
// feat-20260623 M2 / w5: unknown kinds pass through the raw payload so
|
|
1527
|
+
// host-registered loaders can parse their own kind. The engine does not
|
|
1528
|
+
// parse payloads it cannot match; parse responsibility is explicit on the
|
|
1529
|
+
// missing loader (charter P3).
|
|
1530
|
+
const loader = registry.loaders.get(kind);
|
|
1531
|
+
if (loader === undefined) return { ...payload, kind };
|
|
1532
|
+
const out = loader.load(payload, refs, makeLoadContext(registry));
|
|
1533
|
+
// The inline pack-payload loaders are synchronous (`Asset | undefined`);
|
|
1534
|
+
// the async texture / font loaders are dispatched from loadByGuidProd, not
|
|
1535
|
+
// here. A Promise here would mean a misregistered loader -> treat as a
|
|
1536
|
+
// parse miss rather than leaking a thenable into the sync return.
|
|
1537
|
+
if (out !== undefined && typeof (out as { then?: unknown }).then === 'function') {
|
|
1538
|
+
return undefined;
|
|
1539
|
+
}
|
|
1540
|
+
// F21: the scene loader returns { ok: false, error: ParseErrorDetail } for
|
|
1541
|
+
// structured parse errors. Pass the error arm straight through the return
|
|
1542
|
+
// value so the caller constructs a precise AssetError -- no instance slot.
|
|
1543
|
+
if (out !== undefined && out !== null && typeof out === 'object' && 'ok' in out) {
|
|
1544
|
+
return out as { readonly ok: false; readonly error: ParseErrorDetail };
|
|
1545
|
+
}
|
|
1546
|
+
return out as Asset | undefined;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Build the {@link LoadContext} passed to a loader's `load`.
|
|
1551
|
+
* `fetchBinary` / `resolveRef` / `device` are wired for the async texture /
|
|
1552
|
+
* font loaders (w6).
|
|
1553
|
+
*/
|
|
1554
|
+
export function makeLoadContext(registry: AssetRegistry): LoadContext {
|
|
1555
|
+
return {
|
|
1556
|
+
/**
|
|
1557
|
+
* feat-20260706 M3 / w19: fetchBinary signature extended per D-2.
|
|
1558
|
+
* `opts?.compression` triggers the single decompression gate (AC-02).
|
|
1559
|
+
* 'zstd' → lazy-init codec decompressZstd · 'none' / undefined → pass-through.
|
|
1560
|
+
* On decompression failure, the codec error is nested in asset-fetch-failed
|
|
1561
|
+
* detail (D-8: runtime error union NOT extended).
|
|
1562
|
+
*/
|
|
1563
|
+
fetchBinary: async (url: string, opts?: { readonly compression?: AssetCompression }) => {
|
|
1564
|
+
try {
|
|
1565
|
+
const res = await globalThis.fetch(url);
|
|
1566
|
+
if (!res.ok) {
|
|
1567
|
+
return {
|
|
1568
|
+
ok: false as const,
|
|
1569
|
+
error: new AssetError({
|
|
1570
|
+
code: 'asset-fetch-failed',
|
|
1571
|
+
expected: `fetch(${url}) to return ok`,
|
|
1572
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1573
|
+
}),
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
const buf = await res.arrayBuffer();
|
|
1577
|
+
let bytes: Uint8Array = new Uint8Array(buf);
|
|
1578
|
+
|
|
1579
|
+
// --- Decompression gate (AC-02: single gate inside fetchBinary) ---
|
|
1580
|
+
if (opts?.compression === 'zstd') {
|
|
1581
|
+
const { decompressZstd } = await import('@forgeax/engine-codec');
|
|
1582
|
+
const decRes = await decompressZstd(bytes);
|
|
1583
|
+
if (!decRes.ok) {
|
|
1584
|
+
return {
|
|
1585
|
+
ok: false as const,
|
|
1586
|
+
error: new AssetError({
|
|
1587
|
+
code: 'asset-parse-failed',
|
|
1588
|
+
expected: `zstd decompression for ${url}`,
|
|
1589
|
+
hint: `[${decRes.error.code}] ${decRes.error.hint}`,
|
|
1590
|
+
detail: { sourcePath: url },
|
|
1591
|
+
}),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
bytes = new Uint8Array(
|
|
1595
|
+
decRes.value.buffer,
|
|
1596
|
+
decRes.value.byteOffset,
|
|
1597
|
+
decRes.value.byteLength,
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
// compression === 'none' / undefined → E1 pass-through
|
|
1601
|
+
|
|
1602
|
+
return { ok: true as const, value: bytes };
|
|
1603
|
+
} catch {
|
|
1604
|
+
return {
|
|
1605
|
+
ok: false as const,
|
|
1606
|
+
error: new AssetError({
|
|
1607
|
+
code: 'asset-fetch-failed',
|
|
1608
|
+
expected: `fetch(${url}) to succeed`,
|
|
1609
|
+
hint: ASSET_ERROR_HINTS['asset-fetch-failed'],
|
|
1610
|
+
}),
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
resolveRef: async (guid: string) => {
|
|
1615
|
+
const parsed = AssetGuid.parse(guid);
|
|
1616
|
+
if (!parsed.ok) {
|
|
1617
|
+
return { ok: false as const, error: parsed.error };
|
|
1618
|
+
}
|
|
1619
|
+
const r = await loadByGuid(registry, parsed.value);
|
|
1620
|
+
if (!r.ok) return { ok: false as const, error: r.error };
|
|
1621
|
+
// feat-20260614 M8 (D-19): resolveRef ensures the sub-asset is
|
|
1622
|
+
// catalogued (recursive load). The numeric value is vestigial -- the
|
|
1623
|
+
// registry mints no handles; callers store the GUID, not this number.
|
|
1624
|
+
return { ok: true as const, value: 0 };
|
|
1625
|
+
},
|
|
1626
|
+
// feat-20260613-material-paramschema-driven-binding M4 / w22 (D-5 graceful):
|
|
1627
|
+
// expose the registered shader's derive(paramSchema).textureFieldNames to
|
|
1628
|
+
// the materialLoader so it can decide which values fields carry
|
|
1629
|
+
// refs[] indices without a hardcoded texture-field allowlist Set
|
|
1630
|
+
// (AC-03). Returns `undefined` when the shader is not registered (cross-
|
|
1631
|
+
// worktree shader-late-register, plan R-4) — the loader then falls back
|
|
1632
|
+
// to a graceful "try every int paramValue" walk.
|
|
1633
|
+
getMaterialShaderTextureFieldNames: (shaderId: string) => {
|
|
1634
|
+
const lookup = registry.shaderRegistry.findMaterialArtifact(shaderId);
|
|
1635
|
+
if (!lookup.ok) return undefined;
|
|
1636
|
+
return lookup.value.paramSchemaProjection.derivedInterface.textureFieldNames;
|
|
1637
|
+
},
|
|
1638
|
+
transcodeCaps: registry.transcodeCaps,
|
|
1639
|
+
device: undefined,
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Return a runtime snapshot of every catalogued asset. Each entry exposes
|
|
1645
|
+
* `{ guid, kind, name }` where `kind` is the asset discriminant string
|
|
1646
|
+
* from `payload.kind`. feat-20260614 M8 (D-15): the registry holds no
|
|
1647
|
+
* handles -- entries are keyed by GUID (the catalogue key).
|
|
1648
|
+
*
|
|
1649
|
+
* AI-user narrowing flow (AC-11 + plan-strategy §7.4):
|
|
1650
|
+
* ```ts
|
|
1651
|
+
* for (const e of registry.inspect().assets) {
|
|
1652
|
+
* if (e.kind === 'texture') {
|
|
1653
|
+
* // re-query via registry.lookup(e.guid) to get the typed Asset value.
|
|
1654
|
+
* }
|
|
1655
|
+
* }
|
|
1656
|
+
* ```
|
|
1657
|
+
*/
|