@forgeax/engine-assets-runtime 0.1.4 → 0.1.7
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 +453 -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 +554 -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 +143 -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 +7233 -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 +508 -0
- package/src/__tests__/material-stale-generation.unit.test.ts +147 -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 +27 -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 +2045 -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 +186 -0
- package/src/image-error.ts +45 -0
- package/src/index.ts +183 -16
- 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 +385 -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,573 @@
|
|
|
1
|
+
// @forgeax/engine-assets-runtime -- register-time payload validation
|
|
2
|
+
// (feat-20260705-runtime-tier2-decomposition M1 / w4, D-4 F1 straight-cut).
|
|
3
|
+
// Pure move from asset-registry.ts; zero identifier changes.
|
|
4
|
+
|
|
5
|
+
import { PROCEDURAL_FLOATS_PER_VERTEX } from '@forgeax/engine-geometry';
|
|
6
|
+
import type {
|
|
7
|
+
Asset,
|
|
8
|
+
AssetErrorDetail,
|
|
9
|
+
TilesetAsset,
|
|
10
|
+
MeshAsset as TypesMeshAsset,
|
|
11
|
+
} from '@forgeax/engine-types';
|
|
12
|
+
import { ASSET_ERROR_HINTS, AssetError, countExtraUvSets } from '@forgeax/engine-types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register-stage fail-fast for `kind: 'mesh'` payloads whose vertices buffer
|
|
16
|
+
* is not the canonical 12-floats-per-vertex layout.
|
|
17
|
+
*
|
|
18
|
+
* Validation spec (plan-strategy D-3):
|
|
19
|
+
* (a) asset.kind !== 'mesh' -> return null immediately
|
|
20
|
+
* (b) vertices.length === 0 && indices.length === 0 -> return null (empty mesh legal)
|
|
21
|
+
* (c) vertices.length % PROCEDURAL_FLOATS_PER_VERTEX !== 0 -> `AssetError` with
|
|
22
|
+
* code='mesh-vertex-stride-mismatch', detail = { vertexCount: 0,
|
|
23
|
+
* floatsPerVertex: vertices.length / PROCEDURAL_FLOATS_PER_VERTEX } (non-integer)
|
|
24
|
+
* (d) otherwise compute vertexCount = vertices.length / PROCEDURAL_FLOATS_PER_VERTEX;
|
|
25
|
+
* scan indices for maxIndex;
|
|
26
|
+
* if maxIndex + 1 !== vertexCount -> same AssetError shape with
|
|
27
|
+
* detail = { vertexCount: maxIndex + 1, floatsPerVertex: vertices.length / (maxIndex + 1) }
|
|
28
|
+
*
|
|
29
|
+
* Isomorphic with `validateMaterialPayload` — both are private module-level helpers,
|
|
30
|
+
* both return `AssetError | null`, and both are called from `register()` at entry.
|
|
31
|
+
*
|
|
32
|
+
* Anchors: charter P3 (structured failure); plan-strategy D-2 (gate at register stage);
|
|
33
|
+
* plan-strategy D-3 (three-branch validation: empty / non-divisible-12 / maxIndex mismatch);
|
|
34
|
+
* plan-strategy D-5 (physical location co-located with validateMaterialPayload).
|
|
35
|
+
*/
|
|
36
|
+
export function validateMeshPayload(asset: Asset): AssetError | null {
|
|
37
|
+
if (asset.kind !== 'mesh') return null;
|
|
38
|
+
|
|
39
|
+
// feat-20260604-mesh-topology-debug-draw M5 / w13: semantic topology gate
|
|
40
|
+
// (plan-strategy D-A2).
|
|
41
|
+
//
|
|
42
|
+
// feat-20260608 M2 / w9: topology is now per-submesh (MeshAsset.submeshes[]).
|
|
43
|
+
// All topology + submesh-empty + index-OOB validation runs here.
|
|
44
|
+
const submeshes = (asset as TypesMeshAsset).submeshes;
|
|
45
|
+
if (submeshes.length === 0) {
|
|
46
|
+
// TypesMeshAsset POD does not carry an inline GUID; the registry assigns
|
|
47
|
+
// one on register, but at validation time we only have the payload. Emit
|
|
48
|
+
// a stable sentinel so the closed AssetErrorDetail union still narrows.
|
|
49
|
+
const guid = '<no-guid>';
|
|
50
|
+
return new AssetError({
|
|
51
|
+
code: 'mesh-asset-submeshes-empty',
|
|
52
|
+
expected: 'submeshes array has at least one Submesh entry',
|
|
53
|
+
hint: ASSET_ERROR_HINTS['mesh-asset-submeshes-empty'],
|
|
54
|
+
detail: { meshAssetGuid: guid },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const materialSlots = (asset as TypesMeshAsset).materialSlots;
|
|
59
|
+
if (!Array.isArray(materialSlots) || materialSlots.length === 0) {
|
|
60
|
+
return new AssetError({
|
|
61
|
+
code: 'mesh-asset-material-slot-index-out-of-range',
|
|
62
|
+
expected: 'a non-empty materialSlots table for a mesh with submeshes',
|
|
63
|
+
hint: ASSET_ERROR_HINTS['mesh-asset-material-slot-index-out-of-range'],
|
|
64
|
+
detail: {
|
|
65
|
+
meshAssetGuid: '<no-guid>',
|
|
66
|
+
submeshIndex: 0,
|
|
67
|
+
materialSlot: -1,
|
|
68
|
+
materialSlotCount: 0,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const slotNames = new Set<string>();
|
|
73
|
+
const sourceKeys = new Set<string>();
|
|
74
|
+
for (let slotIndex = 0; slotIndex < materialSlots.length; slotIndex++) {
|
|
75
|
+
const slot = materialSlots[slotIndex];
|
|
76
|
+
const slotName = slot?.slotName.trim() ?? '';
|
|
77
|
+
const sourceKey = slot?.sourceKey?.trim();
|
|
78
|
+
if (slotName.length === 0 || slotNames.has(slotName)) {
|
|
79
|
+
return new AssetError({
|
|
80
|
+
code: 'asset-invalid-value',
|
|
81
|
+
expected: `materialSlots[${slotIndex}].slotName is non-empty and unique`,
|
|
82
|
+
hint: 'give every Mesh material slot a stable unique slotName',
|
|
83
|
+
detail: {
|
|
84
|
+
field: `materialSlots[${slotIndex}].slotName`,
|
|
85
|
+
value: slotName,
|
|
86
|
+
reason: 'empty-or-duplicate',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
slotNames.add(slotName);
|
|
91
|
+
if (sourceKey !== undefined && sourceKey.length > 0) {
|
|
92
|
+
if (sourceKeys.has(sourceKey)) {
|
|
93
|
+
return new AssetError({
|
|
94
|
+
code: 'asset-invalid-value',
|
|
95
|
+
expected: `materialSlots[${slotIndex}].sourceKey is unique when present`,
|
|
96
|
+
hint: 'derive a stable unique source identity for every imported Mesh material slot',
|
|
97
|
+
detail: {
|
|
98
|
+
field: `materialSlots[${slotIndex}].sourceKey`,
|
|
99
|
+
value: sourceKey,
|
|
100
|
+
reason: 'duplicate',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
sourceKeys.add(sourceKey);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasIndices = (asset.indices?.length ?? 0) > 0;
|
|
109
|
+
const indexBufferLength = asset.indices?.length ?? 0;
|
|
110
|
+
for (let i = 0; i < submeshes.length; i++) {
|
|
111
|
+
const sm = submeshes[i];
|
|
112
|
+
if (sm === undefined) continue;
|
|
113
|
+
const topology = sm.topology;
|
|
114
|
+
if (
|
|
115
|
+
!Number.isInteger(sm.materialSlot) ||
|
|
116
|
+
sm.materialSlot < 0 ||
|
|
117
|
+
sm.materialSlot >= materialSlots.length
|
|
118
|
+
) {
|
|
119
|
+
return new AssetError({
|
|
120
|
+
code: 'mesh-asset-material-slot-index-out-of-range',
|
|
121
|
+
expected: `submesh[${i}].materialSlot in [0, ${materialSlots.length})`,
|
|
122
|
+
hint: ASSET_ERROR_HINTS['mesh-asset-material-slot-index-out-of-range'],
|
|
123
|
+
detail: {
|
|
124
|
+
meshAssetGuid: '<no-guid>',
|
|
125
|
+
submeshIndex: i,
|
|
126
|
+
materialSlot: sm.materialSlot,
|
|
127
|
+
materialSlotCount: materialSlots.length,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if ((topology === 'line-strip' || topology === 'triangle-strip') && !hasIndices) {
|
|
132
|
+
return new AssetError({
|
|
133
|
+
code: 'asset-invalid-value',
|
|
134
|
+
expected: `submesh[${i}] strip topology carries an index buffer`,
|
|
135
|
+
hint: 'line-strip / triangle-strip meshes must provide indices; add MeshAsset.indices or use line-list / triangle-list',
|
|
136
|
+
detail: {
|
|
137
|
+
field: `submeshes[${i}].topology`,
|
|
138
|
+
value: topology,
|
|
139
|
+
reason: 'strip-topology-without-indices',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (asset.vertices.length === 0 && topology !== 'triangle-list') {
|
|
144
|
+
return new AssetError({
|
|
145
|
+
code: 'asset-invalid-value',
|
|
146
|
+
expected: `submesh[${i}]: empty geometry uses 'triangle-list'`,
|
|
147
|
+
hint: 'a zero-vertex mesh has nothing to draw; change submesh topology to triangle-list or provide vertices',
|
|
148
|
+
detail: {
|
|
149
|
+
field: `submeshes[${i}].topology`,
|
|
150
|
+
value: topology,
|
|
151
|
+
reason: 'empty-geometry-non-default-topology',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// feat-20260608 M2 / w9: index-range-out-of-bounds per submesh
|
|
156
|
+
if (sm.indexOffset + sm.indexCount > indexBufferLength) {
|
|
157
|
+
// TypesMeshAsset POD does not carry an inline GUID; the registry assigns
|
|
158
|
+
// one on register, but at validation time we only have the payload. Emit
|
|
159
|
+
// a stable sentinel so the closed AssetErrorDetail union still narrows.
|
|
160
|
+
const guid = '<no-guid>';
|
|
161
|
+
return new AssetError({
|
|
162
|
+
code: 'mesh-submesh-index-range-out-of-bounds',
|
|
163
|
+
expected: `submesh[${i}].indexOffset + indexCount <= index buffer length (${indexBufferLength})`,
|
|
164
|
+
hint: ASSET_ERROR_HINTS['mesh-submesh-index-range-out-of-bounds'],
|
|
165
|
+
detail: {
|
|
166
|
+
submeshIndex: i,
|
|
167
|
+
indexOffset: sm.indexOffset,
|
|
168
|
+
indexCount: sm.indexCount,
|
|
169
|
+
indexBufferLength,
|
|
170
|
+
meshAssetGuid: guid,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// indices is optional (vertex-only meshes omit it); read defensively. The
|
|
177
|
+
// stride invariant below stays null-safe for vertex-only meshes (D-A4).
|
|
178
|
+
if (asset.vertices.length === 0 && (asset.indices?.length ?? 0) === 0) return null;
|
|
179
|
+
|
|
180
|
+
// Skin-aware stride: when MeshAsset.attributes carries skinIndex + skinWeight,
|
|
181
|
+
// the bridge promotes the interleaved buffer to 18 floats/vertex (12 base +
|
|
182
|
+
// 4 uint16x4 packed via aliased Uint16 view at slots 12-13 + 4 float weights
|
|
183
|
+
// at slots 14-17).
|
|
184
|
+
// feat-20260629 multi-uv: extra UV sets (uv1..uv7) add 2 floats each to the
|
|
185
|
+
// interleaved stride, pushed after skin data (canonical order).
|
|
186
|
+
const attrs = (asset as TypesMeshAsset).attributes;
|
|
187
|
+
const isSkinned =
|
|
188
|
+
attrs !== undefined && attrs.skinIndex !== undefined && attrs.skinWeight !== undefined;
|
|
189
|
+
const extraUvSets = countExtraUvSets(attrs);
|
|
190
|
+
const baseFloatsPerVertex = isSkinned ? 18 : PROCEDURAL_FLOATS_PER_VERTEX;
|
|
191
|
+
const floatsPerVertex = baseFloatsPerVertex + extraUvSets * 2;
|
|
192
|
+
|
|
193
|
+
if (asset.vertices.length % floatsPerVertex !== 0) {
|
|
194
|
+
return new AssetError({
|
|
195
|
+
code: 'mesh-vertex-stride-mismatch',
|
|
196
|
+
expected: isSkinned
|
|
197
|
+
? '18 floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4 + skinIndex u16x4 + skinWeight vec4)'
|
|
198
|
+
: `${PROCEDURAL_FLOATS_PER_VERTEX} floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4)`,
|
|
199
|
+
hint: ASSET_ERROR_HINTS['mesh-vertex-stride-mismatch'],
|
|
200
|
+
detail: {
|
|
201
|
+
vertexCount: 0,
|
|
202
|
+
floatsPerVertex: asset.vertices.length / floatsPerVertex,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const vertexCount = asset.vertices.length / floatsPerVertex;
|
|
208
|
+
// Vertex-only meshes (no indices) skip the maxIndex-vs-vertexCount invariant:
|
|
209
|
+
// there is no index buffer to bound-check against the vertex array (D-A4).
|
|
210
|
+
const indices = asset.indices;
|
|
211
|
+
if (indices === undefined || indices.length === 0) return null;
|
|
212
|
+
let maxIndex = 0;
|
|
213
|
+
for (let i = 0; i < indices.length; i++) {
|
|
214
|
+
const idx = indices[i];
|
|
215
|
+
if (idx !== undefined && idx > maxIndex) maxIndex = idx;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (maxIndex + 1 !== vertexCount) {
|
|
219
|
+
return new AssetError({
|
|
220
|
+
code: 'mesh-vertex-stride-mismatch',
|
|
221
|
+
expected: isSkinned
|
|
222
|
+
? '18 floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4 + skinIndex u16x4 + skinWeight vec4)'
|
|
223
|
+
: `${PROCEDURAL_FLOATS_PER_VERTEX} floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4)`,
|
|
224
|
+
hint: ASSET_ERROR_HINTS['mesh-vertex-stride-mismatch'],
|
|
225
|
+
detail: {
|
|
226
|
+
vertexCount: maxIndex + 1,
|
|
227
|
+
floatsPerVertex: vertexCount > 0 ? asset.vertices.length / (maxIndex + 1) : 0,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// === Tileset / Tilemap / TileLayer validators (feat-20260608 M0 baseline rebuild) ===
|
|
236
|
+
//
|
|
237
|
+
// R-6 first-error path (plan-strategy §R-6 ordering, M1 extended):
|
|
238
|
+
// (1) `atlases.length >= 1` -> 'tileset-tile-entry-malformed'
|
|
239
|
+
// .field='atlases' .scope='tileset-asset' (M1)
|
|
240
|
+
// (2) region rectangle stays in atlas -> 'tileset-region-index-out-of-range' (M0)
|
|
241
|
+
// (3) `region.atlasIndex` in atlases range -> 'tileset-tile-entry-malformed'
|
|
242
|
+
// .field='atlasIndex' .scope='tileset-asset' (M1)
|
|
243
|
+
// (4) `tiles[i].regionIndex` in regions -> 'tileset-region-index-out-of-range' (M0)
|
|
244
|
+
// (5) tile entry widthCells / heightCells -> 'tileset-tile-entry-malformed'
|
|
245
|
+
// / pivotX / pivotY / collider .field=<field> .scope='tile-entry'
|
|
246
|
+
// .tileEntryIndex=i (M1)
|
|
247
|
+
//
|
|
248
|
+
// Tilemap / TileLayer register-time invariants use `AssetError
|
|
249
|
+
// 'asset-invalid-value'` with `.detail = { field, value, reason }` so the
|
|
250
|
+
// closed AssetErrorDetail discriminated union narrows uniformly (charter
|
|
251
|
+
// P4 consistent abstraction with `validateMeshPayload`).
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Optional atlas extent for `validateTilesetPayload`. When omitted, the
|
|
255
|
+
* region-rectangle bounds-check is skipped — only the
|
|
256
|
+
* `tiles[].regionIndex` in `[0, regions.length)` invariant runs.
|
|
257
|
+
*/
|
|
258
|
+
export interface TilesetValidateOptions {
|
|
259
|
+
readonly atlasWidth?: number;
|
|
260
|
+
readonly atlasHeight?: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Construct an `AssetError` for the M1 `tileset-tile-entry-malformed`
|
|
265
|
+
* code with structured `.detail` (closed 7-variant `.field` enum +
|
|
266
|
+
* 2-variant `.scope` + optional `.tileEntryIndex`). The helper centralises
|
|
267
|
+
* the boilerplate so each call site stays a one-liner (charter F1 single
|
|
268
|
+
* affordance) and the `.detail` shape stays SSOT-aligned with
|
|
269
|
+
* `AssetTilesetTileEntryMalformedDetail`.
|
|
270
|
+
*/
|
|
271
|
+
function tileEntryMalformed(args: {
|
|
272
|
+
tilesetGuid: string;
|
|
273
|
+
field: 'widthCells' | 'heightCells' | 'pivotX' | 'pivotY' | 'collider' | 'atlases' | 'atlasIndex';
|
|
274
|
+
scope: 'tileset-asset' | 'tile-entry';
|
|
275
|
+
tileEntryIndex?: number;
|
|
276
|
+
expected: string;
|
|
277
|
+
}): AssetError {
|
|
278
|
+
const detail: AssetErrorDetail = {
|
|
279
|
+
code: 'tileset-tile-entry-malformed',
|
|
280
|
+
field: args.field,
|
|
281
|
+
scope: args.scope,
|
|
282
|
+
tilesetGuid: args.tilesetGuid,
|
|
283
|
+
...(args.tileEntryIndex !== undefined ? { tileEntryIndex: args.tileEntryIndex } : {}),
|
|
284
|
+
expected: args.expected,
|
|
285
|
+
hint: ASSET_ERROR_HINTS['tileset-tile-entry-malformed'],
|
|
286
|
+
};
|
|
287
|
+
return new AssetError({
|
|
288
|
+
code: 'tileset-tile-entry-malformed',
|
|
289
|
+
expected: args.expected,
|
|
290
|
+
hint: ASSET_ERROR_HINTS['tileset-tile-entry-malformed'],
|
|
291
|
+
detail,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validate the shape of a `TilesetTileCollider` value (R-6 step 5d).
|
|
297
|
+
* Returns `null` when the collider is well-formed, otherwise an
|
|
298
|
+
* `AssetError` with code `'tileset-tile-entry-malformed'` and
|
|
299
|
+
* `.detail.field = 'collider'` (charter P3 closed schema).
|
|
300
|
+
*
|
|
301
|
+
* Rules per variant:
|
|
302
|
+
* - `'none'` -- always accepted.
|
|
303
|
+
* - `'rect'` -- `rect.length === 4`; each component in `[0, 1]`;
|
|
304
|
+
* `w > 0`, `h > 0`; `x + w <= 1`, `y + h <= 1`.
|
|
305
|
+
* - `'polygon'` -- `points.length >= 3`; each point's `x` and `y` in
|
|
306
|
+
* `[0, 1]`.
|
|
307
|
+
* - any other `type` discriminator surfaces the same `.field='collider'`
|
|
308
|
+
* fail-fast (unreachable through the typed surface but defends
|
|
309
|
+
* against unchecked JSON loaders, charter P4 fail-fast in depth).
|
|
310
|
+
*/
|
|
311
|
+
function validateColliderShape(
|
|
312
|
+
collider: NonNullable<TilesetTileEntryColliderField>,
|
|
313
|
+
tilesetGuid: string,
|
|
314
|
+
tileEntryIndex: number,
|
|
315
|
+
): AssetError | null {
|
|
316
|
+
if (collider.type === 'none') return null;
|
|
317
|
+
if (collider.type === 'rect') {
|
|
318
|
+
const rect = collider.rect;
|
|
319
|
+
if (!Array.isArray(rect) || rect.length !== 4) {
|
|
320
|
+
return tileEntryMalformed({
|
|
321
|
+
tilesetGuid,
|
|
322
|
+
field: 'collider',
|
|
323
|
+
scope: 'tile-entry',
|
|
324
|
+
tileEntryIndex,
|
|
325
|
+
expected: `tiles[${tileEntryIndex}].collider.rect length === 4`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const [rx, ry, rw, rh] = rect;
|
|
329
|
+
const valid =
|
|
330
|
+
typeof rx === 'number' &&
|
|
331
|
+
typeof ry === 'number' &&
|
|
332
|
+
typeof rw === 'number' &&
|
|
333
|
+
typeof rh === 'number' &&
|
|
334
|
+
rx >= 0 &&
|
|
335
|
+
ry >= 0 &&
|
|
336
|
+
rw > 0 &&
|
|
337
|
+
rh > 0 &&
|
|
338
|
+
rx + rw <= 1 &&
|
|
339
|
+
ry + rh <= 1;
|
|
340
|
+
if (!valid) {
|
|
341
|
+
return tileEntryMalformed({
|
|
342
|
+
tilesetGuid,
|
|
343
|
+
field: 'collider',
|
|
344
|
+
scope: 'tile-entry',
|
|
345
|
+
tileEntryIndex,
|
|
346
|
+
expected: `tiles[${tileEntryIndex}].collider.rect in [0, 1]^2 with w > 0, h > 0, x + w <= 1, y + h <= 1`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (collider.type === 'polygon') {
|
|
352
|
+
const points = collider.points;
|
|
353
|
+
if (!Array.isArray(points) || points.length < 3) {
|
|
354
|
+
return tileEntryMalformed({
|
|
355
|
+
tilesetGuid,
|
|
356
|
+
field: 'collider',
|
|
357
|
+
scope: 'tile-entry',
|
|
358
|
+
tileEntryIndex,
|
|
359
|
+
expected: `tiles[${tileEntryIndex}].collider.points length >= 3`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
for (let j = 0; j < points.length; j++) {
|
|
363
|
+
const p = points[j];
|
|
364
|
+
if (
|
|
365
|
+
!Array.isArray(p) ||
|
|
366
|
+
p.length !== 2 ||
|
|
367
|
+
typeof p[0] !== 'number' ||
|
|
368
|
+
typeof p[1] !== 'number' ||
|
|
369
|
+
p[0] < 0 ||
|
|
370
|
+
p[0] > 1 ||
|
|
371
|
+
p[1] < 0 ||
|
|
372
|
+
p[1] > 1
|
|
373
|
+
) {
|
|
374
|
+
return tileEntryMalformed({
|
|
375
|
+
tilesetGuid,
|
|
376
|
+
field: 'collider',
|
|
377
|
+
scope: 'tile-entry',
|
|
378
|
+
tileEntryIndex,
|
|
379
|
+
expected: `tiles[${tileEntryIndex}].collider.points[${j}] in [0, 1]^2`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
// Unknown discriminator (only reachable via unchecked JSON loaders bypassing
|
|
386
|
+
// the typed surface). Closed enum -> fail-fast (charter P3).
|
|
387
|
+
return tileEntryMalformed({
|
|
388
|
+
tilesetGuid,
|
|
389
|
+
field: 'collider',
|
|
390
|
+
scope: 'tile-entry',
|
|
391
|
+
tileEntryIndex,
|
|
392
|
+
expected: `tiles[${tileEntryIndex}].collider.type in {'none', 'rect', 'polygon'}`,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Internal alias for the optional `collider` field on `TilesetTileEntry`.
|
|
398
|
+
* Localised to keep the validator helper signature math-free.
|
|
399
|
+
*/
|
|
400
|
+
type TilesetTileEntryColliderField = TilesetAsset['tiles'][number]['collider'];
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Validate a `TilesetAsset` payload at register time (M0 baseline rebuild
|
|
404
|
+
* + M1 schema extension). Returns `null` on success or an `AssetError`
|
|
405
|
+
* carrying the first-error details (charter P3 fail-fast). R-6 ordering:
|
|
406
|
+
*
|
|
407
|
+
* 1. `atlases.length >= 1` -- 'tileset-tile-entry-malformed' .field='atlases'.
|
|
408
|
+
* 2. Region rectangle escapes atlas extent (when extent is supplied) --
|
|
409
|
+
* M0 code 'tileset-region-index-out-of-range'.
|
|
410
|
+
* 3. `regions[i].atlasIndex` in `[0, atlases.length)` -- 'tileset-tile-entry-malformed'
|
|
411
|
+
* .field='atlasIndex'.
|
|
412
|
+
* 4. `tiles[i].regionIndex` in `[0, regions.length)` -- M0 code.
|
|
413
|
+
* 5. Per-tile-entry boundaries -- 'tileset-tile-entry-malformed'
|
|
414
|
+
* .field=widthCells | heightCells | pivotX | pivotY | collider,
|
|
415
|
+
* in that order, .scope='tile-entry' .tileEntryIndex=i.
|
|
416
|
+
*/
|
|
417
|
+
export function validateTilesetPayload(
|
|
418
|
+
asset: TilesetAsset,
|
|
419
|
+
opts: TilesetValidateOptions = {},
|
|
420
|
+
): AssetError | null {
|
|
421
|
+
const assetGuid = '<no-guid>';
|
|
422
|
+
// (1) atlases empty fail-fast (M1 R-6 top-level invariant).
|
|
423
|
+
if (asset.atlases.length < 1) {
|
|
424
|
+
return tileEntryMalformed({
|
|
425
|
+
tilesetGuid: assetGuid,
|
|
426
|
+
field: 'atlases',
|
|
427
|
+
scope: 'tileset-asset',
|
|
428
|
+
expected: 'atlases.length >= 1',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const regionCount = asset.regions.length;
|
|
433
|
+
const atlasesLength = asset.atlases.length;
|
|
434
|
+
// (2) region rectangle bounds-check. Negative coords / non-positive size are
|
|
435
|
+
// rejected regardless of whether an atlas extent is supplied; the optional
|
|
436
|
+
// atlasWidth / atlasHeight tightens the upper bound when present.
|
|
437
|
+
const atlasWidth = opts.atlasWidth;
|
|
438
|
+
const atlasHeight = opts.atlasHeight;
|
|
439
|
+
for (let i = 0; i < regionCount; i++) {
|
|
440
|
+
const region = asset.regions[i];
|
|
441
|
+
if (region === undefined) continue;
|
|
442
|
+
const negativeOrZero = region.x < 0 || region.y < 0 || region.width <= 0 || region.height <= 0;
|
|
443
|
+
const exceedsAtlas =
|
|
444
|
+
typeof atlasWidth === 'number' &&
|
|
445
|
+
typeof atlasHeight === 'number' &&
|
|
446
|
+
(region.x + region.width > atlasWidth || region.y + region.height > atlasHeight);
|
|
447
|
+
if (negativeOrZero || exceedsAtlas) {
|
|
448
|
+
return new AssetError({
|
|
449
|
+
code: 'tileset-region-index-out-of-range',
|
|
450
|
+
expected: `regions[${i}] rectangle (x/y >= 0, width/height > 0${
|
|
451
|
+
typeof atlasWidth === 'number' && typeof atlasHeight === 'number'
|
|
452
|
+
? `, x + width <= ${atlasWidth}, y + height <= ${atlasHeight}`
|
|
453
|
+
: ''
|
|
454
|
+
})`,
|
|
455
|
+
hint: ASSET_ERROR_HINTS['tileset-region-index-out-of-range'],
|
|
456
|
+
detail: {
|
|
457
|
+
code: 'tileset-region-index-out-of-range',
|
|
458
|
+
tilesetGuid: assetGuid,
|
|
459
|
+
tileId: 0,
|
|
460
|
+
regionIndex: i,
|
|
461
|
+
regionCount,
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// (3) per-region atlasIndex bounds-check (M1; optional field defaults 0).
|
|
466
|
+
if (region.atlasIndex !== undefined) {
|
|
467
|
+
const ai = region.atlasIndex;
|
|
468
|
+
if (!Number.isInteger(ai) || ai < 0 || ai >= atlasesLength) {
|
|
469
|
+
return tileEntryMalformed({
|
|
470
|
+
tilesetGuid: assetGuid,
|
|
471
|
+
field: 'atlasIndex',
|
|
472
|
+
scope: 'tileset-asset',
|
|
473
|
+
expected: `regions[${i}].atlasIndex in [0, ${atlasesLength})`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// (4) tile entry regionIndex in [0, regions.length) (M0 code).
|
|
479
|
+
for (let i = 0; i < asset.tiles.length; i++) {
|
|
480
|
+
const entry = asset.tiles[i];
|
|
481
|
+
if (entry === undefined) continue;
|
|
482
|
+
const ri = entry.regionIndex;
|
|
483
|
+
if (!Number.isInteger(ri) || ri < 0 || ri >= regionCount) {
|
|
484
|
+
return new AssetError({
|
|
485
|
+
code: 'tileset-region-index-out-of-range',
|
|
486
|
+
expected: `tiles[${i}].regionIndex in [0, ${regionCount})`,
|
|
487
|
+
hint: ASSET_ERROR_HINTS['tileset-region-index-out-of-range'],
|
|
488
|
+
detail: {
|
|
489
|
+
code: 'tileset-region-index-out-of-range',
|
|
490
|
+
tilesetGuid: assetGuid,
|
|
491
|
+
tileId: i + 1,
|
|
492
|
+
regionIndex: ri,
|
|
493
|
+
regionCount,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// (5) per-tile-entry M1 boundaries -- widthCells > heightCells > pivotX >
|
|
499
|
+
// pivotY > collider, per plan-strategy §R-6 first-error order. Each loop
|
|
500
|
+
// iteration runs the full sub-order on entry i before advancing to i+1 so
|
|
501
|
+
// the deterministic global order is (i=0 widthCells, i=0 heightCells, ...,
|
|
502
|
+
// i=0 collider, i=1 widthCells, ...). Tests in tileset-asset-validate.test.ts
|
|
503
|
+
// R-6 block exercise the sub-order on a single entry.
|
|
504
|
+
for (let i = 0; i < asset.tiles.length; i++) {
|
|
505
|
+
const entry = asset.tiles[i];
|
|
506
|
+
if (entry === undefined) continue;
|
|
507
|
+
if (entry.widthCells !== undefined) {
|
|
508
|
+
const w = entry.widthCells;
|
|
509
|
+
if (!Number.isFinite(w) || w <= 0 || w > 64) {
|
|
510
|
+
return tileEntryMalformed({
|
|
511
|
+
tilesetGuid: assetGuid,
|
|
512
|
+
field: 'widthCells',
|
|
513
|
+
scope: 'tile-entry',
|
|
514
|
+
tileEntryIndex: i,
|
|
515
|
+
expected: `tiles[${i}].widthCells in (0, 64]`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (entry.heightCells !== undefined) {
|
|
520
|
+
const h = entry.heightCells;
|
|
521
|
+
if (!Number.isFinite(h) || h <= 0 || h > 64) {
|
|
522
|
+
return tileEntryMalformed({
|
|
523
|
+
tilesetGuid: assetGuid,
|
|
524
|
+
field: 'heightCells',
|
|
525
|
+
scope: 'tile-entry',
|
|
526
|
+
tileEntryIndex: i,
|
|
527
|
+
expected: `tiles[${i}].heightCells in (0, 64]`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (entry.pivotX !== undefined) {
|
|
532
|
+
const px = entry.pivotX;
|
|
533
|
+
if (!Number.isFinite(px) || px < 0 || px > 1) {
|
|
534
|
+
return tileEntryMalformed({
|
|
535
|
+
tilesetGuid: assetGuid,
|
|
536
|
+
field: 'pivotX',
|
|
537
|
+
scope: 'tile-entry',
|
|
538
|
+
tileEntryIndex: i,
|
|
539
|
+
expected: `tiles[${i}].pivotX in [0, 1]`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (entry.pivotY !== undefined) {
|
|
544
|
+
const py = entry.pivotY;
|
|
545
|
+
if (!Number.isFinite(py) || py < 0 || py > 1) {
|
|
546
|
+
return tileEntryMalformed({
|
|
547
|
+
tilesetGuid: assetGuid,
|
|
548
|
+
field: 'pivotY',
|
|
549
|
+
scope: 'tile-entry',
|
|
550
|
+
tileEntryIndex: i,
|
|
551
|
+
expected: `tiles[${i}].pivotY in [0, 1]`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (entry.collider !== undefined) {
|
|
556
|
+
const colliderErr = validateColliderShape(entry.collider, assetGuid, i);
|
|
557
|
+
if (colliderErr !== null) return colliderErr;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Compute the implicit atlas extent of a `TilesetAsset` from its grid metadata
|
|
565
|
+
* (`columns * tileWidth` x `rows * tileHeight`). Used by the register-time
|
|
566
|
+
* region-bounds check when the caller does not supply explicit extents.
|
|
567
|
+
*/
|
|
568
|
+
export function inferAtlasExtent(asset: TilesetAsset): { atlasWidth: number; atlasHeight: number } {
|
|
569
|
+
return {
|
|
570
|
+
atlasWidth: asset.columns * asset.tileWidth,
|
|
571
|
+
atlasHeight: asset.rows * asset.tileHeight,
|
|
572
|
+
};
|
|
573
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Plugin } from '@forgeax/engine-plugin';
|
|
2
|
+
import type { Loader } from '@forgeax/engine-types';
|
|
3
|
+
|
|
4
|
+
import type { AssetRegistry } from './asset-registry';
|
|
5
|
+
import type { PackLoader } from './loader-registry';
|
|
6
|
+
|
|
7
|
+
declare module '@forgeax/engine-plugin' {
|
|
8
|
+
interface EngineContextServices {
|
|
9
|
+
assets?: AssetRegistry;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Provide one renderer-owned asset registry to the current Cordis realm. */
|
|
14
|
+
export function assetsPlugin(assets: AssetRegistry): Plugin {
|
|
15
|
+
return {
|
|
16
|
+
name: 'assets',
|
|
17
|
+
provide: 'assets',
|
|
18
|
+
apply(ctx) {
|
|
19
|
+
ctx.provide('assets', assets);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Reversibly register one runtime asset loader. */
|
|
25
|
+
export function assetLoaderPlugin(loader: Loader<unknown>): Plugin {
|
|
26
|
+
return {
|
|
27
|
+
name: `asset-loader:${loader.kind}`,
|
|
28
|
+
inject: ['assets'],
|
|
29
|
+
apply(ctx) {
|
|
30
|
+
const assets = ctx.assets;
|
|
31
|
+
if (assets === undefined) throw new Error('asset loader plugin requires the assets service');
|
|
32
|
+
ctx.effect(() => assets.loaders.register(loader), `assets/loader:${loader.kind}`);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Reversibly register one pack loader. */
|
|
38
|
+
export function packLoaderPlugin(loader: PackLoader): Plugin {
|
|
39
|
+
return {
|
|
40
|
+
name: `pack-loader:${loader.kind}`,
|
|
41
|
+
inject: ['assets'],
|
|
42
|
+
apply(ctx) {
|
|
43
|
+
const assets = ctx.assets;
|
|
44
|
+
if (assets === undefined) throw new Error('pack loader plugin requires the assets service');
|
|
45
|
+
ctx.effect(() => assets.loaders.registerPackLoader(loader), `assets/pack:${loader.kind}`);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|