@forgeax/engine-assets-runtime 0.1.4 → 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 +1 -1
- package/dist/internal.d.ts.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 +1 -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,386 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime -- material validation collaboration module (feat-20260705-runtime-tier2-decomposition M1 / w5, D-4). Free functions taking the AssetRegistry instance as first param; extracted from the class body, logic byte-preserved.
|
|
2
|
+
|
|
3
|
+
import type { CookedMaterialRecord } from '@forgeax/engine-pack';
|
|
4
|
+
import {
|
|
5
|
+
AssetError,
|
|
6
|
+
createMaterialError,
|
|
7
|
+
type MaterialAsset,
|
|
8
|
+
type MaterialError,
|
|
9
|
+
type MaterialParameter,
|
|
10
|
+
} from '@forgeax/engine-types';
|
|
11
|
+
import type { AssetRegistry } from '../asset-registry';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate a MaterialAsset's passes[] against the ShaderRegistry's
|
|
15
|
+
* paramSchema (union semantics: all declared params across all passes
|
|
16
|
+
* must be satisfiable from values).
|
|
17
|
+
*
|
|
18
|
+
* - Empty / undefined passes[] → error
|
|
19
|
+
* - Each pass's shader must exist in ShaderRegistry
|
|
20
|
+
* - Union of all pass paramSchemas: params without `default` must
|
|
21
|
+
* appear in values with matching type
|
|
22
|
+
* - Extra keys in values are silently ignored (D-5)
|
|
23
|
+
*
|
|
24
|
+
* @returns AssetError on failure, null on success
|
|
25
|
+
*/
|
|
26
|
+
export function validateMaterialPasses(
|
|
27
|
+
registry: AssetRegistry,
|
|
28
|
+
asset: MaterialAsset,
|
|
29
|
+
): AssetError | null {
|
|
30
|
+
const passes = asset.passes;
|
|
31
|
+
// undefined passes is valid (material inherits from parent at resolve time);
|
|
32
|
+
// only explicit empty passes[] is an error.
|
|
33
|
+
if (passes === undefined || passes.length === 0) {
|
|
34
|
+
if (passes !== undefined && passes.length === 0) {
|
|
35
|
+
return new AssetError({
|
|
36
|
+
code: 'asset-invalid-value',
|
|
37
|
+
expected: 'MaterialAsset with at least one pass',
|
|
38
|
+
hint: 'add at least one pass descriptor to passes[] before register',
|
|
39
|
+
detail: { passCount: 0 },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// passes undefined: skip validation (inherits from parent later)
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const allSchemas: MaterialParameter[] = [...(asset.parameters ?? [])];
|
|
47
|
+
for (let passIndex = 0; passIndex < passes.length; passIndex++) {
|
|
48
|
+
const pass = passes[passIndex];
|
|
49
|
+
if (pass === undefined) continue;
|
|
50
|
+
if (pass.program.module.length === 0) {
|
|
51
|
+
return new AssetError({
|
|
52
|
+
code: 'asset-invalid-value',
|
|
53
|
+
expected: 'pass.program.module to be a non-empty module identifier',
|
|
54
|
+
hint: `pass[${passIndex}] has an empty program.module`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Deduplicate by name (first occurrence wins)
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
const unionSchema: MaterialParameter[] = [];
|
|
62
|
+
for (const entry of allSchemas) {
|
|
63
|
+
if (!seen.has(entry.name)) {
|
|
64
|
+
seen.add(entry.name);
|
|
65
|
+
unionSchema.push(entry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const values: Record<string, unknown> = (asset.values as Record<string, unknown>) ?? {};
|
|
70
|
+
|
|
71
|
+
// feat-20260613-material-paramschema-driven-binding M3 / w16 (D-2):
|
|
72
|
+
// derive(schema) is the SSOT for which schema fields are textures vs
|
|
73
|
+
// samplers vs numeric. The register-time three-layer validation (extra-
|
|
74
|
+
// key / type-mismatch / missing-required) categorizes fields via
|
|
75
|
+
// derive output instead of a hardcoded literal type list. Texture
|
|
76
|
+
// and sampler fields are always optional at register time (the
|
|
77
|
+
// resource handles may not be available yet — D-5 graceful path),
|
|
78
|
+
// so derive-derived membership decides the skip set without keeping
|
|
79
|
+
// a parallel literal table.
|
|
80
|
+
const missingParams: string[] = [];
|
|
81
|
+
for (const entry of unionSchema) {
|
|
82
|
+
const value = values[entry.name];
|
|
83
|
+
if (value === undefined) {
|
|
84
|
+
if (entry.default !== undefined || entry.optional || entry.type === 'texture') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
missingParams.push(entry.name);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (value === null && entry.optional) continue;
|
|
91
|
+
// Type-check supplied values
|
|
92
|
+
const typeOk = validateParamType(registry, entry.name, entry.type, value);
|
|
93
|
+
if (!typeOk) {
|
|
94
|
+
return new AssetError({
|
|
95
|
+
code: 'asset-invalid-value',
|
|
96
|
+
expected: `values.${entry.name} to be of type ${entry.type}`,
|
|
97
|
+
hint: `values['${entry.name}'] has type ${typeof value} but paramSchema declares ${entry.type}`,
|
|
98
|
+
detail: { paramName: entry.name, expectedType: entry.type, got: typeof value },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (missingParams.length > 0) {
|
|
104
|
+
return new AssetError({
|
|
105
|
+
code: 'asset-invalid-value',
|
|
106
|
+
expected: `values to contain keys: ${missingParams.join(', ')}`,
|
|
107
|
+
hint: `missing required params: ${missingParams.join(', ')}`,
|
|
108
|
+
detail: { missingParams },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface MaterialEffectiveRootIdentity {
|
|
116
|
+
readonly guid: string;
|
|
117
|
+
readonly layoutIdentity: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function validateMaterialCookIdentity(
|
|
121
|
+
record: Pick<CookedMaterialRecord, 'guid' | 'receipt'>,
|
|
122
|
+
root: MaterialEffectiveRootIdentity,
|
|
123
|
+
): MaterialError | null {
|
|
124
|
+
const actual = record.receipt.layoutIdentity;
|
|
125
|
+
if (
|
|
126
|
+
actual !== root.layoutIdentity ||
|
|
127
|
+
(record.receipt as { readonly derivedInterface?: { readonly layoutIdentity?: string } })
|
|
128
|
+
.derivedInterface?.layoutIdentity !== root.layoutIdentity
|
|
129
|
+
) {
|
|
130
|
+
return createMaterialError('material-derived-interface-mismatch', {
|
|
131
|
+
code: 'material-derived-interface-mismatch',
|
|
132
|
+
stage: 'extract',
|
|
133
|
+
material: record.guid,
|
|
134
|
+
layoutIdentity: root.layoutIdentity,
|
|
135
|
+
expectedIdentity: root.layoutIdentity,
|
|
136
|
+
actualIdentity: actual,
|
|
137
|
+
action: 'recook',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sprite 9-slice values fail-fast validation
|
|
145
|
+
* (feat-20260527-sprite-nineslice M2 / w8, plan-strategy §D-1 + AC-08).
|
|
146
|
+
*
|
|
147
|
+
* Fires when:
|
|
148
|
+
* - asset.kind === 'material'
|
|
149
|
+
* - first pass shader === 'forgeax::sprite'
|
|
150
|
+
* - values.slices is present
|
|
151
|
+
*
|
|
152
|
+
* Six fail-fast branches (1:1 with w4 test):
|
|
153
|
+
* (1) any component is negative
|
|
154
|
+
* (2) slices.x + slices.z >= region.zw[0] (X-axis overlap)
|
|
155
|
+
* (3) slices.y + slices.w >= region.zw[1] (Y-axis overlap)
|
|
156
|
+
* (4) any component is NaN
|
|
157
|
+
* (5) any component is Infinity
|
|
158
|
+
* (6) length !== 4
|
|
159
|
+
*
|
|
160
|
+
* Reuses the existing 'asset-invalid-value' member of the closed
|
|
161
|
+
* `AssetErrorCode` 13-member union (no new code added per
|
|
162
|
+
* AGENTS.md §Error model). The `.expected` literal mirrors the AI-User
|
|
163
|
+
* Charter §3 string; `.hint` inlines the offending sum + the relevant
|
|
164
|
+
* `region.zw` numeral so AI users can copy-paste the prompt straight
|
|
165
|
+
* back into the IDE for self-recovery (plan-strategy §R-4).
|
|
166
|
+
*
|
|
167
|
+
* @returns AssetError on failure, null on success.
|
|
168
|
+
*/
|
|
169
|
+
export function validateSpriteSlices(
|
|
170
|
+
_registry: AssetRegistry,
|
|
171
|
+
asset: MaterialAsset,
|
|
172
|
+
): AssetError | null {
|
|
173
|
+
const passes = asset.passes;
|
|
174
|
+
if (passes === undefined || passes.length === 0) return null;
|
|
175
|
+
const firstPass = passes[0];
|
|
176
|
+
if (firstPass === undefined || firstPass.program.module !== 'forgeax_material::sprite')
|
|
177
|
+
return null;
|
|
178
|
+
const pv = (asset.values ?? {}) as Record<string, unknown>;
|
|
179
|
+
const slicesRaw = pv.slices;
|
|
180
|
+
// Field absent — caller relies on paramSchema default [0, 0, 0, 0]; nothing to check.
|
|
181
|
+
if (slicesRaw === undefined) return null;
|
|
182
|
+
const expected =
|
|
183
|
+
'values.slices: [number, number, number, number] with 0 ≤ left + right < region.zw[0] and 0 ≤ top + bottom < region.zw[1]';
|
|
184
|
+
if (!Array.isArray(slicesRaw)) {
|
|
185
|
+
return new AssetError({
|
|
186
|
+
code: 'asset-invalid-value',
|
|
187
|
+
expected,
|
|
188
|
+
hint: `values.slices is not an array (got ${typeof slicesRaw}); must be a 4-tuple [left, top, right, bottom]`,
|
|
189
|
+
detail: { paramName: 'slices', got: typeof slicesRaw },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// (6) length check
|
|
193
|
+
if (slicesRaw.length !== 4) {
|
|
194
|
+
return new AssetError({
|
|
195
|
+
code: 'asset-invalid-value',
|
|
196
|
+
expected,
|
|
197
|
+
hint: `values.slices length is ${slicesRaw.length}; must be 4 ([left, top, right, bottom])`,
|
|
198
|
+
detail: { paramName: 'slices', got: slicesRaw.length },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const slices = slicesRaw as readonly unknown[];
|
|
202
|
+
// Type check each component first.
|
|
203
|
+
for (let i = 0; i < 4; i++) {
|
|
204
|
+
if (typeof slices[i] !== 'number') {
|
|
205
|
+
return new AssetError({
|
|
206
|
+
code: 'asset-invalid-value',
|
|
207
|
+
expected,
|
|
208
|
+
hint: `values.slices[${i}] is not a number (got ${typeof slices[i]})`,
|
|
209
|
+
detail: { paramName: 'slices', got: typeof slices[i] },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const left = slices[0] as number;
|
|
214
|
+
const top = slices[1] as number;
|
|
215
|
+
const right = slices[2] as number;
|
|
216
|
+
const bottom = slices[3] as number;
|
|
217
|
+
// (4) NaN
|
|
218
|
+
for (let i = 0; i < 4; i++) {
|
|
219
|
+
if (Number.isNaN(slices[i] as number)) {
|
|
220
|
+
return new AssetError({
|
|
221
|
+
code: 'asset-invalid-value',
|
|
222
|
+
expected,
|
|
223
|
+
hint: `values.slices[${i}] is NaN; all four components must be finite non-negative numbers`,
|
|
224
|
+
detail: { paramName: 'slices', got: 'NaN' },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// (5) Infinity
|
|
229
|
+
for (let i = 0; i < 4; i++) {
|
|
230
|
+
if (!Number.isFinite(slices[i] as number)) {
|
|
231
|
+
return new AssetError({
|
|
232
|
+
code: 'asset-invalid-value',
|
|
233
|
+
expected,
|
|
234
|
+
hint: `values.slices[${i}] is Infinity; all four components must be finite non-negative numbers`,
|
|
235
|
+
detail: { paramName: 'slices', got: 'Infinity' },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// (1) negative — D-3 sentinel uses negative .w for tile mode but only
|
|
240
|
+
// after extract; at register-time the user-supplied tuple must be all
|
|
241
|
+
// non-negative (the engine encodes the sign downstream).
|
|
242
|
+
for (let i = 0; i < 4; i++) {
|
|
243
|
+
if ((slices[i] as number) < 0) {
|
|
244
|
+
return new AssetError({
|
|
245
|
+
code: 'asset-invalid-value',
|
|
246
|
+
expected,
|
|
247
|
+
hint: `values.slices[${i}] = ${slices[i]}; all four components must be non-negative`,
|
|
248
|
+
detail: { paramName: 'slices', got: slices[i] as number },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// (2)/(3) overlap with region. region default is [0, 0, 1, 1];
|
|
253
|
+
// user override comes via values.region (vec4).
|
|
254
|
+
const regionRaw = pv.region;
|
|
255
|
+
let regionZ = 1;
|
|
256
|
+
let regionW = 1;
|
|
257
|
+
if (Array.isArray(regionRaw) && regionRaw.length >= 4) {
|
|
258
|
+
const rz = regionRaw[2];
|
|
259
|
+
const rw = regionRaw[3];
|
|
260
|
+
if (typeof rz === 'number') regionZ = rz;
|
|
261
|
+
if (typeof rw === 'number') regionW = rw;
|
|
262
|
+
}
|
|
263
|
+
const sumX = left + right;
|
|
264
|
+
if (sumX >= regionZ) {
|
|
265
|
+
return new AssetError({
|
|
266
|
+
code: 'asset-invalid-value',
|
|
267
|
+
expected,
|
|
268
|
+
hint: `received slices=[${left}, ${top}, ${right}, ${bottom}]; left + right = ${sumX} ≥ ${regionZ} (region.z)`,
|
|
269
|
+
detail: { paramName: 'slices', got: sumX },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const sumY = top + bottom;
|
|
273
|
+
if (sumY >= regionW) {
|
|
274
|
+
return new AssetError({
|
|
275
|
+
code: 'asset-invalid-value',
|
|
276
|
+
expected,
|
|
277
|
+
hint: `received slices=[${left}, ${top}, ${right}, ${bottom}]; top + bottom = ${sumY} ≥ ${regionW} (region.w)`,
|
|
278
|
+
detail: { paramName: 'slices', got: sumY },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function validateParamType(
|
|
285
|
+
_registry: AssetRegistry,
|
|
286
|
+
_name: string,
|
|
287
|
+
type: string,
|
|
288
|
+
value: unknown,
|
|
289
|
+
): boolean {
|
|
290
|
+
switch (type) {
|
|
291
|
+
case 'f32':
|
|
292
|
+
case 'i32':
|
|
293
|
+
case 'u32':
|
|
294
|
+
return typeof value === 'number';
|
|
295
|
+
case 'vec2':
|
|
296
|
+
return Array.isArray(value) && value.length >= 2 && value.every((v) => typeof v === 'number');
|
|
297
|
+
case 'vec3':
|
|
298
|
+
return Array.isArray(value) && value.length >= 3 && value.every((v) => typeof v === 'number');
|
|
299
|
+
case 'vec4':
|
|
300
|
+
return Array.isArray(value) && value.length >= 4 && value.every((v) => typeof v === 'number');
|
|
301
|
+
case 'color':
|
|
302
|
+
return (
|
|
303
|
+
Array.isArray(value) &&
|
|
304
|
+
(value.length === 3 || value.length === 4) &&
|
|
305
|
+
value.every((v) => typeof v === 'number')
|
|
306
|
+
);
|
|
307
|
+
case 'texture':
|
|
308
|
+
// A cooked pack may keep an asset GUID as the compact texture
|
|
309
|
+
// shorthand. The render material resolver expands that reference at
|
|
310
|
+
// the resource boundary; rejecting it here makes authored packs fail
|
|
311
|
+
// before the same value can reach the resolver.
|
|
312
|
+
return (
|
|
313
|
+
typeof value === 'string' ||
|
|
314
|
+
(typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
315
|
+
);
|
|
316
|
+
case 'bool':
|
|
317
|
+
return typeof value === 'boolean';
|
|
318
|
+
default:
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* feat-20260527-sprite-nineslice M4 / w18 (D-9): register-time soft-warn
|
|
325
|
+
* for sliceMode=1 (tile) bound to a sampler whose addressMode is not
|
|
326
|
+
* 'repeat'. Bumps `nineslice.tile-needs-repeat-sampler` once per offending
|
|
327
|
+
* catalogue call. Never throws -- the counter is the sole AI-user-facing
|
|
328
|
+
* signal (charter P3 machine-readable; AC-08 closed, never extends
|
|
329
|
+
* AssetErrorCode).
|
|
330
|
+
*
|
|
331
|
+
* feat-20260614 M8 (D-19): `values.sampler` is now an embedded GUID
|
|
332
|
+
* string (dash-form), resolved against the catalogue rather than a handle.
|
|
333
|
+
*/
|
|
334
|
+
export function detectTileNeedsRepeatSampler(registry: AssetRegistry, asset: MaterialAsset): void {
|
|
335
|
+
if (registry.metrics === null) return;
|
|
336
|
+
const passes = asset.passes;
|
|
337
|
+
if (passes === undefined || passes.length === 0) return;
|
|
338
|
+
const firstPass = passes[0];
|
|
339
|
+
if (
|
|
340
|
+
firstPass === undefined ||
|
|
341
|
+
(firstPass.program.module !== 'forgeax_material::sprite' &&
|
|
342
|
+
firstPass.program.module !== 'forgeax::sprite')
|
|
343
|
+
)
|
|
344
|
+
return;
|
|
345
|
+
const pv = (asset.values ?? {}) as Record<string, unknown>;
|
|
346
|
+
const slicesAndMode = pv.slicesAndMode;
|
|
347
|
+
const encodedTile =
|
|
348
|
+
Array.isArray(slicesAndMode) && typeof slicesAndMode[3] === 'number'
|
|
349
|
+
? slicesAndMode[3] < 0
|
|
350
|
+
: false;
|
|
351
|
+
const legacyTile = typeof pv.sliceMode === 'number' && pv.sliceMode === 1;
|
|
352
|
+
if (!encodedTile && !legacyTile) return;
|
|
353
|
+
const samplerGuid = typeof pv.sampler === 'string' ? pv.sampler : undefined;
|
|
354
|
+
if (samplerGuid === undefined) return;
|
|
355
|
+
const samplerEnvelope = registry.assetCatalog.get(samplerGuid.toLowerCase());
|
|
356
|
+
if (samplerEnvelope === undefined || samplerEnvelope.kind !== 'sampler') return;
|
|
357
|
+
const samplerAsset = samplerEnvelope.payload;
|
|
358
|
+
if (samplerAsset.kind !== 'sampler') return;
|
|
359
|
+
const u = samplerAsset.addressModeU;
|
|
360
|
+
const v = samplerAsset.addressModeV;
|
|
361
|
+
if (u !== 'repeat' || v !== 'repeat') {
|
|
362
|
+
registry.metrics.increment('nineslice.tile-needs-repeat-sampler');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* feat-20260613-material-paramschema-driven-binding M4 / w23 (D-5 graceful):
|
|
368
|
+
* Return the texture-field name set for the given material-shader id,
|
|
369
|
+
* derived from the registered shader's paramSchema via `derive(paramSchema)
|
|
370
|
+
* .textureFieldNames`. Returns `undefined` when the shader is not yet
|
|
371
|
+
* registered (cross-worktree shader-late-register, plan R-4).
|
|
372
|
+
*
|
|
373
|
+
* Used by `extractFrame` to know which values fields the shader
|
|
374
|
+
* declares as texture handles; the extract layer validates handle-vs-
|
|
375
|
+
* scalar typing and drops misclassified slots so the record stage's
|
|
376
|
+
* MISSING_TEXTURE_HANDLE fallback can take over (white default texture)
|
|
377
|
+
* rather than letting a stray handle reach `device.createBindGroup`.
|
|
378
|
+
*/
|
|
379
|
+
export function materialShaderTextureFieldNames(
|
|
380
|
+
registry: AssetRegistry,
|
|
381
|
+
shaderId: string,
|
|
382
|
+
): ReadonlySet<string> | undefined {
|
|
383
|
+
const lookup = registry.shaderRegistry.findMaterialArtifact(shaderId);
|
|
384
|
+
if (!lookup.ok) return undefined;
|
|
385
|
+
return lookup.value.paramSchemaProjection.derivedInterface.textureFieldNames;
|
|
386
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// M8 w59 (round 5 D-18 atomic): two-tier resolution + material-walk over
|
|
2
|
+
// world.sharedRefs.
|
|
3
|
+
//
|
|
4
|
+
// D-15 two-tier asset resolution: slots in [1, BUILTIN_BASE) are builtins
|
|
5
|
+
// (process-static, resolve through BuiltinAssetRegistry.resolve, never
|
|
6
|
+
// reference-counted); slots >= BUILTIN_BASE are user-tier (resolve through
|
|
7
|
+
// world.sharedRefs.resolve). Resolution is entirely ECS/render-side -- it
|
|
8
|
+
// never goes through AssetRegistry (human-inputs implement-architecture-
|
|
9
|
+
// correction 2026-06-16T12:35:07Z: AssetRegistry has NO handle concept).
|
|
10
|
+
//
|
|
11
|
+
// AI-user surface: `resolveAssetHandle(world, handle)` is the single entry
|
|
12
|
+
// point for handle-to-payload resolution; `walkMaterialPassesOverSharedRefs`
|
|
13
|
+
// is the material parent-chain walk used by the render extract stage. Charter
|
|
14
|
+
// F1 single-entry indexability -- one import, one call, no dual-path leak.
|
|
15
|
+
|
|
16
|
+
import type { SharedRefStaleError, UniqueRefStaleError, World } from '@forgeax/engine-ecs';
|
|
17
|
+
import type { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
18
|
+
import { err, ok, type Result } from '@forgeax/engine-rhi';
|
|
19
|
+
import type {
|
|
20
|
+
Asset,
|
|
21
|
+
AssetError as AssetErrorType,
|
|
22
|
+
Handle,
|
|
23
|
+
MaterialAsset,
|
|
24
|
+
MaterialError,
|
|
25
|
+
MaterialPass,
|
|
26
|
+
} from '@forgeax/engine-types';
|
|
27
|
+
import {
|
|
28
|
+
ASSET_ERROR_HINTS,
|
|
29
|
+
AssetError,
|
|
30
|
+
BUILTIN_BASE,
|
|
31
|
+
handleSlot,
|
|
32
|
+
materialGuidText,
|
|
33
|
+
resolveMaterialAsset,
|
|
34
|
+
} from '@forgeax/engine-types';
|
|
35
|
+
import { BuiltinAssetRegistry } from './builtin-asset-registry';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a handle to its asset payload with two-tier slot-range dispatch.
|
|
39
|
+
*
|
|
40
|
+
* slot < BUILTIN_BASE -> BuiltinAssetRegistry.resolve(handle) (process-static)
|
|
41
|
+
* slot >= BUILTIN_BASE -> world.sharedRefs.resolve(handle) (user-tier RC)
|
|
42
|
+
*
|
|
43
|
+
* Callers must specify the expected asset type explicitly:
|
|
44
|
+
* `resolveAssetHandle<MeshAsset>(world, handle)`. The type parameter is an
|
|
45
|
+
* assertion -- the runtime dispatch returns the correct payload based on slot
|
|
46
|
+
* range; the type parameter mirrors the caller's expectation.
|
|
47
|
+
*
|
|
48
|
+
* @param world - The World owning the per-World SharedRefStore (user-tier).
|
|
49
|
+
* @param handle - The branded handle to resolve.
|
|
50
|
+
* @returns `Result.ok(payload)` on hit, `Result.err(AssetError)` on miss.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveAssetHandle<T extends Asset>(
|
|
53
|
+
world: World,
|
|
54
|
+
handle: Handle<string, 'shared'>,
|
|
55
|
+
): Result<T, AssetErrorType | SharedRefStaleError | UniqueRefStaleError> {
|
|
56
|
+
const slot = handleSlot(handle);
|
|
57
|
+
if (slot < BUILTIN_BASE) {
|
|
58
|
+
const builtin = BuiltinAssetRegistry.resolve(handle);
|
|
59
|
+
if (builtin !== null) return ok(builtin as unknown as T);
|
|
60
|
+
return err(
|
|
61
|
+
new AssetError({
|
|
62
|
+
code: 'asset-not-found',
|
|
63
|
+
expected: `builtin slot ${slot} present in BuiltinAssetRegistry`,
|
|
64
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const res = world.sharedRefs.resolve<string, T>(handle);
|
|
69
|
+
if (res.ok) return ok(res.value);
|
|
70
|
+
// Forward the closed error code transparently. Checking the discriminant,
|
|
71
|
+
// rather than constructor identity, preserves the structured stale detail
|
|
72
|
+
// when a browser bundles the bridge and its World through different module
|
|
73
|
+
// instances.
|
|
74
|
+
switch (res.error.code) {
|
|
75
|
+
case 'shared-ref-stale':
|
|
76
|
+
return err(res.error);
|
|
77
|
+
case 'shared-ref-released':
|
|
78
|
+
case 'builtin-slot-not-owned':
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
return err(
|
|
82
|
+
new AssetError({
|
|
83
|
+
code: 'asset-not-found',
|
|
84
|
+
expected: `user-tier slot ${slot} present in world.sharedRefs`,
|
|
85
|
+
hint: ASSET_ERROR_HINTS['asset-not-found'],
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walk a MaterialAsset parent chain to produce the inherited passes +
|
|
92
|
+
* shallow-merged values (W-1..W-7 semantics). feat-20260614 M8 (D-19):
|
|
93
|
+
* the starting material is resolved from a column `handle` via
|
|
94
|
+
* {@link resolveAssetHandle} (builtin / user-tier world.sharedRefs); each
|
|
95
|
+
* ancestor `material.parent` is an embedded GUID (AssetGuid) resolved through
|
|
96
|
+
* the AssetRegistry catalogue ({@link AssetRegistry.lookup}). The registry
|
|
97
|
+
* holds no handles -- it is the GUID->payload SSOT for parent resolution.
|
|
98
|
+
*
|
|
99
|
+
* Zero-cache (AC-04): every call builds the same table-shaped input and delegates
|
|
100
|
+
* inheritance semantics to the MaterialAsset resolver in `@forgeax/engine-types`.
|
|
101
|
+
* Compiler and runtime therefore share root-contract ownership and child-value
|
|
102
|
+
* override semantics instead of maintaining two subtly different walks.
|
|
103
|
+
*/
|
|
104
|
+
export function walkMaterialPassesOverSharedRefs(
|
|
105
|
+
world: World,
|
|
106
|
+
handle: Handle<'MaterialAsset', 'shared'>,
|
|
107
|
+
registry: { lookup(guid: AssetGuid | string): Asset | undefined },
|
|
108
|
+
): Result<
|
|
109
|
+
{
|
|
110
|
+
passes: MaterialPass[];
|
|
111
|
+
parameters: MaterialAsset['parameters'];
|
|
112
|
+
values: NonNullable<MaterialAsset['values']>;
|
|
113
|
+
colorSpace: MaterialAsset['colorSpace'];
|
|
114
|
+
},
|
|
115
|
+
AssetErrorType | MaterialError | SharedRefStaleError | UniqueRefStaleError
|
|
116
|
+
> {
|
|
117
|
+
// Resolve the root material payload from its column handle.
|
|
118
|
+
const rootRes = resolveAssetHandle<MaterialAsset>(
|
|
119
|
+
world,
|
|
120
|
+
handle as unknown as Handle<string, 'shared'>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const rootLabel = `handle-${handleSlot(handle)}`;
|
|
124
|
+
if (!rootRes.ok) return err(rootRes.error);
|
|
125
|
+
|
|
126
|
+
const table: Record<string, MaterialAsset> = {};
|
|
127
|
+
const visited = new Set<string>();
|
|
128
|
+
const collect = (label: string, material: MaterialAsset): void => {
|
|
129
|
+
if (visited.has(label)) return;
|
|
130
|
+
visited.add(label);
|
|
131
|
+
table[label] = material;
|
|
132
|
+
if (material.parent === undefined) return;
|
|
133
|
+
const parentId = materialGuidText(material.parent);
|
|
134
|
+
const parent = registry.lookup(material.parent);
|
|
135
|
+
if (parent?.kind === 'material') collect(parentId, parent);
|
|
136
|
+
};
|
|
137
|
+
collect(rootLabel, rootRes.value);
|
|
138
|
+
|
|
139
|
+
const resolved = resolveMaterialAsset(rootLabel, table);
|
|
140
|
+
if (!resolved.ok) return err(resolved.error);
|
|
141
|
+
return ok({
|
|
142
|
+
passes: [...(resolved.value.asset.passes ?? [])],
|
|
143
|
+
parameters: resolved.value.asset.parameters ?? [],
|
|
144
|
+
values: { ...(resolved.value.asset.values ?? {}) },
|
|
145
|
+
colorSpace: resolved.value.asset.colorSpace,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { Asset, Handle, Result, TextureAsset } from '@forgeax/engine-types';
|
|
3
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
4
|
+
|
|
5
|
+
export type TilesetRuntimeErrorCode =
|
|
6
|
+
| 'tileset-atlas-not-found'
|
|
7
|
+
| 'tileset-atlas-stale'
|
|
8
|
+
| 'tileset-atlas-kind-mismatch';
|
|
9
|
+
|
|
10
|
+
export interface TilesetRuntimeError {
|
|
11
|
+
readonly code: TilesetRuntimeErrorCode;
|
|
12
|
+
readonly expected: string;
|
|
13
|
+
readonly hint: string;
|
|
14
|
+
readonly detail: {
|
|
15
|
+
readonly guid: string;
|
|
16
|
+
readonly actualKind?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ResolvedTilesetRuntime {
|
|
21
|
+
readonly guid: string;
|
|
22
|
+
readonly payload: TextureAsset;
|
|
23
|
+
readonly handle: Handle<'TextureAsset', 'shared'>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type TilesetAtlasLookup = Asset | { readonly code: 'stale' } | undefined;
|
|
27
|
+
|
|
28
|
+
/** Project one durable atlas GUID into the current World at the tilemap owner. */
|
|
29
|
+
export function resolveTilesetRuntime(
|
|
30
|
+
world: World,
|
|
31
|
+
guid: string,
|
|
32
|
+
lookup: (guid: string) => TilesetAtlasLookup,
|
|
33
|
+
): Result<ResolvedTilesetRuntime, TilesetRuntimeError> {
|
|
34
|
+
if (guid.length === 0) {
|
|
35
|
+
return err({
|
|
36
|
+
code: 'tileset-atlas-not-found',
|
|
37
|
+
expected: 'a non-empty durable atlas GUID',
|
|
38
|
+
hint: 'load the atlas payload before extracting the tilemap',
|
|
39
|
+
detail: { guid },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const payload = lookup(guid);
|
|
43
|
+
if (payload === undefined) {
|
|
44
|
+
return err({
|
|
45
|
+
code: 'tileset-atlas-not-found',
|
|
46
|
+
expected: `a loaded texture payload for atlas GUID ${guid}`,
|
|
47
|
+
hint: 'load or retain the atlas before extracting the tilemap',
|
|
48
|
+
detail: { guid },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if ('code' in payload) {
|
|
52
|
+
return err({
|
|
53
|
+
code: 'tileset-atlas-stale',
|
|
54
|
+
expected: `a current texture payload for atlas GUID ${guid}`,
|
|
55
|
+
hint: 'rebuild the stale atlas projection before extracting the tilemap',
|
|
56
|
+
detail: { guid },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (payload.kind !== 'texture') {
|
|
60
|
+
return err({
|
|
61
|
+
code: 'tileset-atlas-kind-mismatch',
|
|
62
|
+
expected: 'a texture atlas payload',
|
|
63
|
+
hint: `replace atlas GUID ${guid} with a texture asset`,
|
|
64
|
+
detail: { guid, actualKind: payload.kind },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return ok({
|
|
68
|
+
guid,
|
|
69
|
+
payload,
|
|
70
|
+
handle: world.internSharedRef('TextureAsset', payload),
|
|
71
|
+
});
|
|
72
|
+
}
|