@forgeax/engine-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 +49 -83
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-serialize.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-serialize.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts +2 -0
- package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts.map +1 -0
- package/dist/__tests__/asset-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/asset-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-guid-reverse.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-guid-reverse.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-mounts.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-mounts.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.browser.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.browser.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.dawn.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.listCatalog.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.listCatalog.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.mesh-bin.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.mesh-bin.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.raw-container-failfast.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.raw-container-failfast.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.recursive.spec.d.ts +2 -0
- package/dist/__tests__/asset-registry.recursive.spec.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.unit.test.d.ts +2 -0
- package/dist/__tests__/asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/audio-load-by-guid.unit.test.d.ts +2 -0
- package/dist/__tests__/audio-load-by-guid.unit.test.d.ts.map +1 -0
- package/dist/__tests__/backend-selection.integration.test.d.ts +2 -0
- package/dist/__tests__/backend-selection.integration.test.d.ts.map +1 -0
- package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts +2 -0
- package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/builtin-asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts +1 -2
- package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/collect-scene-mount-collapse.test.d.ts +2 -0
- package/dist/__tests__/collect-scene-mount-collapse.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts +1 -1
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -1
- package/dist/__tests__/collect-transient-skip.test.d.ts +1 -2
- package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -1
- package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts +2 -0
- package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/draw-published.d.ts +2 -2
- package/dist/__tests__/draw-published.d.ts.map +1 -1
- 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.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-binary-decompress.test.d.ts +2 -0
- package/dist/__tests__/fetch-binary-decompress.test.d.ts.map +1 -0
- package/dist/__tests__/four-path-compression.integration.test.d.ts +2 -0
- package/dist/__tests__/four-path-compression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/guid-for-asset.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-for-asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle-migration.test.d.ts +2 -0
- package/dist/__tests__/handle-migration.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/host-custom-kind-contract.test-d.d.ts +2 -0
- package/dist/__tests__/host-custom-kind-contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/host-custom-kind-contract.test.d.ts +2 -0
- package/dist/__tests__/host-custom-kind-contract.test.d.ts.map +1 -0
- package/dist/__tests__/inspect-name.test.d.ts +2 -0
- package/dist/__tests__/inspect-name.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts +2 -0
- package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts.map +1 -0
- package/dist/__tests__/loadbyguid-payload.unit.test.d.ts +2 -0
- package/dist/__tests__/loadbyguid-payload.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts +2 -0
- package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-extract-graceful.test.d.ts +2 -0
- package/dist/__tests__/loader-extract-graceful.test.d.ts.map +1 -0
- package/dist/__tests__/loader-name-survival.test.d.ts +2 -0
- package/dist/__tests__/loader-name-survival.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/new-kind-refs-contract.test.d.ts +2 -0
- package/dist/__tests__/new-kind-refs-contract.test.d.ts.map +1 -0
- package/dist/__tests__/package-promotion.test.d.ts +2 -0
- package/dist/__tests__/package-promotion.test.d.ts.map +1 -0
- package/dist/__tests__/package-rename.test.d.ts +2 -0
- package/dist/__tests__/package-rename.test.d.ts.map +1 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +1 -1
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts +10 -0
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-test-utils.d.ts +10 -0
- package/dist/__tests__/renderer-test-utils.d.ts.map +1 -0
- package/dist/__tests__/renderer.test-d.d.ts +2 -0
- package/dist/__tests__/renderer.test-d.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle-err.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle-err.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__/resolveName-fallback.test.d.ts +2 -0
- package/dist/__tests__/resolveName-fallback.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName-null-package.test.d.ts +2 -0
- package/dist/__tests__/resolveName-null-package.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName-unified-entry.test.d.ts +2 -0
- package/dist/__tests__/resolveName-unified-entry.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName.test.d.ts +2 -0
- package/dist/__tests__/resolveName.test.d.ts.map +1 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts +1 -1
- package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/roundtrip-primitive.test.d.ts +2 -0
- package/dist/__tests__/roundtrip-primitive.test.d.ts.map +1 -0
- package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts +2 -0
- package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-authoring.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-authoring.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-fixed-point.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-fixed-point.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-sugar-instantiate.test.d.ts +2 -0
- package/dist/__tests__/scene-sugar-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts +1 -1
- package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/template-game-default-api-contract.test.d.ts +2 -0
- package/dist/__tests__/template-game-default-api-contract.test.d.ts.map +1 -0
- package/dist/__tests__/tileset-asset-validate.test.d.ts +2 -0
- package/dist/__tests__/tileset-asset-validate.test.d.ts.map +1 -0
- package/dist/__tests__/transcode-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/transcode-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transient-runtime-behavior.test.d.ts +2 -0
- package/dist/__tests__/transient-runtime-behavior.test.d.ts.map +1 -0
- package/dist/__tests__/video-loader-consumption.test.d.ts +2 -0
- package/dist/__tests__/video-loader-consumption.test.d.ts.map +1 -0
- package/dist/__tests__/video-loader-smoke.dawn.test.d.ts +2 -0
- package/dist/__tests__/video-loader-smoke.dawn.test.d.ts.map +1 -0
- package/dist/backend-selection.d.ts +13 -0
- package/dist/backend-selection.d.ts.map +1 -0
- package/dist/collect-scene-asset.d.ts +3 -3
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/createRenderer.d.ts +14 -2
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/errors/environment.d.ts +1 -1
- package/dist/errors/environment.d.ts.map +1 -1
- package/dist/index.d.ts +19 -81
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +105 -108
- package/dist/index.mjs.map +1 -1
- package/dist/renderer-host.d.ts +9 -0
- package/dist/renderer-host.d.ts.map +1 -0
- package/dist/renderer-host.mjs +62 -0
- package/dist/renderer-host.mjs.map +1 -0
- package/dist/scene-instances/index.d.ts +2 -0
- package/dist/scene-instances/index.d.ts.map +1 -0
- package/dist/scene-utils/mount-override-fold.d.ts +1 -2
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/dist/systems/sprite-animation-tick.d.ts +1 -1
- package/dist/systems/sprite-animation-tick.d.ts.map +1 -1
- package/dist/systems/transparent-sort.d.ts +1 -2
- package/dist/systems/transparent-sort.d.ts.map +1 -1
- package/dist/tilemap-register-validate.d.ts.map +1 -1
- package/package.json +34 -33
- package/src/__tests__/active-camera.unit.test.ts +19 -9
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +213 -0
- package/src/__tests__/animation-graph-serialize.unit.test.ts +171 -0
- package/src/__tests__/append-injection.test.ts +1 -1
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +102 -0
- package/src/__tests__/asset-error-exhaustive.test-d.ts +62 -0
- package/src/__tests__/asset-registry-guid-reverse.test.ts +575 -0
- package/src/__tests__/asset-registry-hdr-equirect.test.ts +78 -0
- package/src/__tests__/asset-registry-mesh-pack-payload.dawn.test.ts +232 -0
- package/src/__tests__/asset-registry-mounts.test.ts +806 -0
- package/src/__tests__/asset-registry.browser.test.ts +81 -0
- package/src/__tests__/asset-registry.dawn.test.ts +94 -0
- package/src/__tests__/asset-registry.listCatalog.test.ts +170 -0
- package/src/__tests__/asset-registry.mesh-bin.test.ts +316 -0
- package/src/__tests__/asset-registry.pack-cache.test.ts +969 -0
- package/src/__tests__/asset-registry.raw-container-failfast.test.ts +225 -0
- package/src/__tests__/asset-registry.recursive.spec.ts +1397 -0
- package/src/__tests__/asset-registry.unit.test.ts +1030 -0
- package/src/__tests__/asset.unit.test.ts +7107 -0
- package/src/__tests__/audio-load-by-guid.unit.test.ts +126 -0
- package/src/__tests__/backend-selection.integration.test.ts +11 -0
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +320 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +4 -4
- package/src/__tests__/block-upload-math.unit.test.ts +2 -2
- package/src/__tests__/builtin-asset-registry.unit.test.ts +112 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
- package/src/__tests__/cache-false-eviction.test.ts +2 -1
- package/src/__tests__/cache-isolation-matrix.test.ts +2 -1
- package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +5 -6
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +29 -19
- package/src/__tests__/collect-scene-asset.test.ts +49 -52
- package/src/__tests__/collect-scene-mount-collapse.test.ts +391 -0
- package/src/__tests__/collect-subtree.unit.test.ts +1 -2
- package/src/__tests__/collect-transient-children-absent.test.ts +24 -10
- package/src/__tests__/collect-transient-roundtrip.test.ts +44 -20
- package/src/__tests__/collect-transient-skip.test.ts +24 -13
- package/src/__tests__/components.test-d.ts +1 -6
- package/src/__tests__/components.unit.test.ts +20 -19
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +59 -27
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +139 -0
- package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -29
- package/src/__tests__/createRenderer-dispose.test.ts +35 -402
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +25 -30
- package/src/__tests__/createRenderer.unit.test.ts +1 -1
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +27 -63
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +56 -47
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +49 -29
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +26 -15
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +13 -14
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +23 -31
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +0 -18
- package/src/__tests__/dawn/{mesh-update.dawn.test.ts → mesh-update-dawn.test.ts} +42 -134
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +14 -7
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +16 -28
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +23 -39
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +198 -0
- package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +12 -14
- package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +27 -22
- package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +25 -28
- package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +10 -11
- package/src/__tests__/demo-sortscope-migration.unit.test.ts +5 -5
- package/src/__tests__/derive-bgl-integration.test.ts +34 -44
- package/src/__tests__/derive-render-data-skin.unit.test.ts +1 -1
- package/src/__tests__/draw-published.ts +11 -4
- package/src/__tests__/dynamic-texture-store.unit.test.ts +190 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +144 -21
- package/src/__tests__/errors.unit.test.ts +1203 -0
- package/src/__tests__/extract-frames-graceful-degradation.test.ts +3 -7
- package/src/__tests__/extract-frames-merge.test.ts +35 -22
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +5 -16
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +322 -0
- package/src/__tests__/fetch-binary-decompress.test.ts +356 -0
- package/src/__tests__/four-path-compression.integration.test.ts +395 -0
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +1 -1
- package/src/__tests__/fullscreen-feature-host.dawn.test.ts +80 -0
- package/src/__tests__/fullscreen-post-bgl-depth.test.ts +4 -2
- package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +3 -3
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +39 -23
- package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +4 -2
- package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +28 -17
- package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +29 -18
- package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +16 -12
- package/src/__tests__/geometry.unit.test.ts +525 -159
- package/src/__tests__/glyph-tilemap-worldid.test.ts +1 -1
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +6 -4
- package/src/__tests__/guid-for-asset.unit.test.ts +102 -0
- package/src/__tests__/handle-migration.test.ts +136 -0
- package/src/__tests__/helpers/register-runtime-components.ts +4 -2
- package/src/__tests__/helpers/register-scene-components.ts +1 -1
- package/src/__tests__/host-custom-kind-contract.test-d.ts +47 -0
- package/src/__tests__/host-custom-kind-contract.test.ts +578 -0
- package/src/__tests__/inspect-name.test.ts +139 -0
- package/src/__tests__/instances.test-d.ts +1 -1
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +112 -0
- package/src/__tests__/light-casters-9-light.browser.test.ts +73 -76
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +7 -10
- package/src/__tests__/light-snapshot.test-d.ts +2 -2
- package/src/__tests__/lights-vec-negative.unit.test.ts +1 -1
- package/src/__tests__/lights.unit.test.ts +16 -500
- package/src/__tests__/loadbyguid-payload.unit.test.ts +117 -0
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +213 -0
- package/src/__tests__/loader-extract-graceful.test.ts +146 -0
- package/src/__tests__/loader-name-survival.test.ts +213 -0
- package/src/__tests__/m3-vec-negative.unit.test.ts +3 -2
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +101 -31
- package/src/__tests__/materials.unit.test.ts +10 -27
- package/src/__tests__/mesh-bin-contract.test.ts +28 -0
- package/src/__tests__/mesh-bin-skin-roundtrip.unit.test.ts +149 -0
- package/src/__tests__/mesh-bin.test.ts +38 -0
- package/src/__tests__/mesh-loader-skin-contract.unit.test.ts +120 -0
- package/src/__tests__/mesh-ssbo-grow.unit.test.ts +3 -3
- package/src/__tests__/mip-gate.unit.test.ts +1 -1
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +29 -68
- package/src/__tests__/new-kind-refs-contract.test.ts +132 -0
- package/src/__tests__/node-shims.d.ts +4 -6
- package/src/__tests__/package-promotion.test.ts +88 -0
- package/src/__tests__/package-rename.test.ts +103 -0
- package/src/__tests__/pass-selector.unit.test.ts +2 -2
- package/src/__tests__/pbr-pipeline.unit.test.ts +14 -21
- package/src/__tests__/pipeline-builder-msaa.unit.test.ts +6 -9
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -9
- package/src/__tests__/pipeline-spec.test.ts +16 -177
- package/src/__tests__/pipeline.unit.test.ts +154 -1693
- package/src/__tests__/point-light-shadow.dawn.test.ts +33 -42
- package/src/__tests__/point-light-shadow.unit.test.ts +19 -60
- package/src/__tests__/post-process-error-codes.unit.test.ts +4 -1
- package/src/__tests__/post-process-params-component.unit.test.ts +1 -1
- package/src/__tests__/post-process-params-extract.unit.test.ts +2 -5
- package/src/__tests__/postprocess-camera-consistency.test.ts +4 -7
- package/src/__tests__/profiler-assembly.integration.test.ts +1 -1
- package/src/__tests__/propagate-transforms.dawn.test.ts +12 -11
- package/src/__tests__/record-worldid-isolation.test.ts +7 -29
- package/src/__tests__/recover-error-exhaustive.test-d.ts +1 -1
- package/src/__tests__/render-error-exhaustive.test-d.ts +81 -71
- package/src/__tests__/render-feature-assembly.test-d.ts +20 -9
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +8 -6
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +60 -558
- package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +57 -544
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +104 -0
- package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +40 -248
- package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +83 -340
- package/src/__tests__/render-feature-recover.integration.test.ts +9 -49
- package/src/__tests__/render-feature-runtime-surface.test-d.ts +17 -20
- package/src/__tests__/render-schema-consumer.integration.test.ts +20 -13
- package/src/__tests__/render-schema-owner-boundary.unit.test.ts +2 -6
- package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +5 -5
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +16 -31
- package/src/__tests__/render-system-extract.test.ts +36 -51
- package/src/__tests__/render-system-extract.unit.test.ts +360 -0
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +3 -5
- package/src/__tests__/render-system-fold-fallback.unit.test.ts +6 -3
- package/src/__tests__/render-system-fold-material.unit.test.ts +3 -5
- package/src/__tests__/render-system-fold-metrics.unit.test.ts +5 -5
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +6 -6
- package/src/__tests__/render-system-fold.dawn.test.ts +7 -5
- package/src/__tests__/render-system-mega.test.ts +292 -190
- package/src/__tests__/render-system-owner-split-contract.test.ts +10 -32
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +59 -45
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +23 -29
- package/src/__tests__/render-system-record.test.ts +398 -235
- package/src/__tests__/render-system-record.unit.test.ts +25 -26
- package/src/__tests__/render-system-skin-bg.unit.test.ts +8 -12
- package/src/__tests__/renderer-consumer.test-d.ts +16 -3
- package/src/__tests__/renderer-health.unit.test.ts +26 -916
- package/src/__tests__/renderer-lifecycle.integration.test.ts +19 -6
- package/src/__tests__/renderer-recover.unit.test.ts +49 -1069
- package/src/__tests__/renderer-result.test-d.ts +19 -9
- package/src/__tests__/renderer-surface.unit.test.ts +3 -5
- package/src/__tests__/renderer-test-utils.ts +56 -0
- package/src/__tests__/renderer.test-d.ts +59 -0
- package/src/__tests__/resolve-asset-handle-err.test.ts +111 -0
- package/src/__tests__/resolve-asset-handle.unit.test.ts +64 -0
- package/src/__tests__/resolve-depth-only-view.test.ts +1 -1
- package/src/__tests__/resolveName-fallback.test.ts +64 -0
- package/src/__tests__/resolveName-null-package.test.ts +38 -0
- package/src/__tests__/resolveName-unified-entry.test.ts +65 -0
- package/src/__tests__/resolveName.test.ts +64 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +43 -261
- package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +41 -169
- package/src/__tests__/roots-to-scene-asset.test.ts +199 -93
- package/src/__tests__/roundtrip-primitive.test.ts +351 -0
- package/src/__tests__/scene-bare-guid-refs-instantiate.test.ts +123 -0
- package/src/__tests__/scene-equirect-roundtrip.test.ts +103 -16
- package/src/__tests__/scene-instance-authoring.integration.test.ts +8 -0
- package/src/__tests__/scene-instance-bridge.integration.test.ts +1 -1
- package/src/__tests__/scene-instance-component.test.ts +1 -1
- package/src/__tests__/scene-mount-fixed-point.test.ts +257 -0
- package/src/__tests__/scene-mount-roundtrip.test.ts +987 -0
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +202 -0
- package/src/__tests__/scene-sugar-instantiate.test.ts +324 -0
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +533 -0
- package/src/__tests__/serialize-scene-asset.test.ts +219 -22
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -3
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +37 -17
- package/src/__tests__/shadow-csm-component.test.ts +3 -5
- package/src/__tests__/shadow-csm-extract.test.ts +2 -100
- package/src/__tests__/shadow-csm-orthoz.test.ts +2 -16
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +19 -14
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +32 -26
- package/src/__tests__/shadow-csm-ubo.dawn.test.ts +8 -49
- package/src/__tests__/shadow-csm-ubo.test.ts +3 -25
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +1 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +67 -56
- package/src/__tests__/shadow-m1.dawn.test.ts +8 -9
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +11 -27
- package/src/__tests__/skin-errors.unit.test.ts +1 -1
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +7 -5
- package/src/__tests__/skylight-lazy-projection.test.ts +36 -15
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +5 -5
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +47 -64
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +12 -11
- package/src/__tests__/sprite-lit-extract-branch.test.ts +1 -1
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
- package/src/__tests__/ssao-bgl.test.ts +16 -9
- package/src/__tests__/ssao-buffers.test.ts +2 -2
- package/src/__tests__/ssao-data.test.ts +2 -2
- package/src/__tests__/ssao-errors.test.ts +1 -1
- package/src/__tests__/ssao-passes.test.ts +64 -392
- package/src/__tests__/ssao-pipelines.test.ts +1 -1
- package/src/__tests__/ssao-uniform-layout.test.ts +8 -6
- package/src/__tests__/stencil-outline-pixel.dawn.test.ts +25 -17
- package/src/__tests__/swap-chain-format.unit.test.ts +5 -7
- package/src/__tests__/systems.unit.test.ts +536 -1326
- package/src/__tests__/template-game-default-api-contract.test.ts +362 -0
- package/src/__tests__/text.unit.test.ts +56 -33
- package/src/__tests__/tilemap-childed-despawn.test.ts +8 -14
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +8 -12
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +2 -1
- package/src/__tests__/tilemap-chunk-extract.test.ts +13 -17
- package/src/__tests__/tilemap-dirty-rebuild.test.ts +7 -11
- package/src/__tests__/tilemap-material-cache.test.ts +8 -13
- package/src/__tests__/tilemap-multi-atlas.test.ts +12 -32
- package/src/__tests__/tilemap-owner-boundary.unit.test.ts +5 -2
- package/src/__tests__/tilemap-region-pack.test.ts +8 -12
- package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +8 -12
- package/src/__tests__/tilemap-uv-inset.test.ts +11 -14
- package/src/__tests__/tilemap-validate.test.ts +1 -1
- package/src/__tests__/tileset-asset-validate.test.ts +638 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +207 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +36 -28
- package/src/__tests__/transient-fields-collect.test.ts +28 -21
- package/src/__tests__/transient-runtime-behavior.test.ts +227 -0
- package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +4 -4
- package/src/__tests__/variant-selection.unit.test.ts +1 -1
- package/src/__tests__/video-extract-bindgroup.dawn.test.ts +89 -136
- package/src/__tests__/video-loader-consumption.test.ts +86 -0
- package/src/__tests__/video-loader-smoke.dawn.test.ts +60 -0
- package/src/__tests__/view-ubo-layout.browser.test.ts +0 -22
- package/src/__tests__/world-entity-key.test.ts +1 -1
- package/src/__tests__/world-scene-despawn.test.ts +1 -1
- package/src/__tests__/world-scene-instantiate.test.ts +2 -36
- package/src/__tests__/world-scene-nested.test.ts +1 -1
- package/src/__tests__/world-scene-override.test.ts +1 -1
- package/src/backend-selection.ts +69 -0
- package/src/collect-scene-asset.ts +28 -23
- package/src/components/__tests__/camera.test-d.ts +1 -1
- package/src/components/__tests__/camera.test.ts +1 -1
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/components/__tests__/tile-layer.test-d.ts +1 -1
- package/src/components/__tests__/tile-layer.unit.test.ts +2 -2
- package/src/createRenderer.ts +24 -36
- package/src/errors/environment.ts +1 -1
- package/src/index.ts +47 -113
- package/src/renderer-host.ts +33 -0
- package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +1 -1
- package/src/scene-instances/index.ts +4 -0
- package/src/scene-utils/mount-override-fold.ts +3 -7
- package/src/systems/__tests__/transparent-sort.bench.ts +3 -3
- package/src/systems/sprite-animation-tick.ts +1 -1
- package/src/systems/transparent-sort.ts +38 -38
- package/src/tilemap-register-validate.ts +4 -4
- package/dist/__tests__/bind-group-cache-binding.test.d.ts +0 -2
- package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +0 -1
- package/dist/__tests__/bind-group-cache-frame.test.d.ts +0 -2
- package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +0 -1
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts +0 -2
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-manifest.d.ts +0 -131
- package/dist/__tests__/fog-producer-manifest.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel-probe.d.ts +0 -42
- package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +0 -2
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +0 -2
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-real-fixture.d.ts +0 -5
- package/dist/__tests__/fog-producer-real-fixture.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-vfx-effect.d.ts +0 -3
- package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +0 -1
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +0 -1
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +0 -2
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +0 -1
- package/dist/__tests__/gpu-resource.test.d.ts +0 -2
- package/dist/__tests__/gpu-resource.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +0 -2
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts +0 -2
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +0 -2
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +0 -2
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +0 -2
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +0 -1
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +0 -2
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +0 -2
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +0 -1
- package/dist/__tests__/helpers/primitive-world.d.ts +0 -8
- package/dist/__tests__/helpers/primitive-world.d.ts.map +0 -1
- package/dist/__tests__/helpers/render-asset-port.d.ts +0 -15
- package/dist/__tests__/helpers/render-asset-port.d.ts.map +0 -1
- package/dist/__tests__/helpers/scene-asset-port.d.ts +0 -9
- package/dist/__tests__/helpers/scene-asset-port.d.ts.map +0 -1
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +0 -2
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline-layout-selection.test.d.ts +0 -2
- package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +0 -1
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +0 -2
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +0 -1
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +0 -2
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +0 -2
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +0 -1
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +0 -2
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-pssm.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-viewport.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m2.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3-calibrate.d.ts +0 -83
- package/dist/__tests__/shadow-m3-calibrate.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/temporal-off-path.unit.test.d.ts +0 -2
- package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +0 -1
- package/dist/__tests__/video-cache-regression.unit.test.d.ts +0 -2
- package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +0 -1
- package/dist/asset-kind.d.ts +0 -4
- package/dist/asset-kind.d.ts.map +0 -1
- package/dist/errors/sprite-animation.d.ts +0 -3
- package/dist/errors/sprite-animation.d.ts.map +0 -1
- package/dist/scene-asset-port.d.ts +0 -17
- package/dist/scene-asset-port.d.ts.map +0 -1
- package/dist/scene-collect-errors.d.ts +0 -22
- package/dist/scene-collect-errors.d.ts.map +0 -1
- package/dist/shadow-m3-calibrate.d.ts +0 -40
- package/dist/shadow-m3-calibrate.d.ts.map +0 -1
- package/src/__tests__/bind-group-cache-binding.test.ts +0 -2513
- package/src/__tests__/bind-group-cache-frame.test.ts +0 -1511
- package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +0 -204
- package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +0 -297
- package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +0 -271
- package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +0 -321
- package/src/__tests__/fog-producer-manifest.ts +0 -162
- package/src/__tests__/fog-producer-pixel-probe.ts +0 -263
- package/src/__tests__/fog-producer-pixel.browser.test.ts +0 -26
- package/src/__tests__/fog-producer-pixel.dawn.test.ts +0 -55
- package/src/__tests__/fog-producer-real-fixture.ts +0 -805
- package/src/__tests__/fog-producer-vfx-effect.ts +0 -272
- package/src/__tests__/fog-scene-asset.integration.test.ts +0 -123
- package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +0 -207
- package/src/__tests__/gpu-resource.test.ts +0 -1206
- package/src/__tests__/hdrp-bgl-slots.browser.test.ts +0 -50
- package/src/__tests__/hdrp-buffers.unit.test.ts +0 -70
- package/src/__tests__/hdrp-execute-filter.unit.test.ts +0 -200
- package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +0 -64
- package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +0 -139
- package/src/__tests__/hdrp-ssao-boundary.test.ts +0 -237
- package/src/__tests__/hdrp-ssao-falsification.test.ts +0 -289
- package/src/__tests__/helpers/primitive-world.ts +0 -91
- package/src/__tests__/helpers/render-asset-port.ts +0 -65
- package/src/__tests__/helpers/scene-asset-port.ts +0 -23
- package/src/__tests__/material-derived-coordinates.integration.test.ts +0 -100
- package/src/__tests__/pipeline-layout-selection.test.ts +0 -298
- package/src/__tests__/post-process-reads-narrowing.test-d.ts +0 -52
- package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +0 -230
- package/src/__tests__/render-feature-zero-feature.browser.test.ts +0 -152
- package/src/__tests__/render-pipeline-trivial.dawn.test.ts +0 -525
- package/src/__tests__/shadow-csm-caster-membership.test.ts +0 -232
- package/src/__tests__/shadow-csm-pssm.test.ts +0 -142
- package/src/__tests__/shadow-csm-temporal.browser.test.ts +0 -145
- package/src/__tests__/shadow-csm-viewport.test.ts +0 -74
- package/src/__tests__/shadow-m2.dawn.test.ts +0 -394
- package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +0 -248
- package/src/__tests__/shadow-m3-calibrate.ts +0 -203
- package/src/__tests__/shadow-m3.dawn.test.ts +0 -288
- package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +0 -264
- package/src/__tests__/shadow-quality-metrics.dawn.test.ts +0 -324
- package/src/__tests__/temporal-off-path.unit.test.ts +0 -69
- package/src/__tests__/video-cache-regression.unit.test.ts +0 -186
- package/src/asset-kind.ts +0 -7
- package/src/errors/sprite-animation.ts +0 -5
- package/src/scene-asset-port.ts +0 -32
- package/src/scene-collect-errors.ts +0 -30
- package/src/shadow-m3-calibrate.ts +0 -171
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
// asset-registry.recursive.spec — M1/SceneAsset recursive loadByGuid (TDD red
|
|
2
|
+
// phase for AC-01 / AC-06 / AC-07 / AC-08). The test file compiles and the
|
|
3
|
+
// "red" stage is intentional (type=test task).
|
|
4
|
+
//
|
|
5
|
+
// Coverage:
|
|
6
|
+
// AC-01 — scene + N material + 1 mesh: recursive loadByGuid<SceneAsset>
|
|
7
|
+
// registers all transitive sub-assets
|
|
8
|
+
// AC-06 — idempotency: second loadByGuid(g) returns same handle, zero fetch
|
|
9
|
+
// AC-07 — transitive failure: missing sub-asset → err with breadcrumb hint
|
|
10
|
+
// AC-08 — in-flight dedup + cycle: concurrent Promise.all 3-way dedup;
|
|
11
|
+
// cycle A→B→A no stack overflow
|
|
12
|
+
|
|
13
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
14
|
+
import { type Component, defineComponent } from '@forgeax/engine-ecs';
|
|
15
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
16
|
+
import type {
|
|
17
|
+
AssetError,
|
|
18
|
+
LocalEntityId,
|
|
19
|
+
MaterialAsset,
|
|
20
|
+
SceneAsset,
|
|
21
|
+
MeshAsset as TypesMeshAsset,
|
|
22
|
+
} from '@forgeax/engine-types';
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
24
|
+
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
25
|
+
|
|
26
|
+
// ── test GUIDs ──────────────────────────────────────────────────────────────
|
|
27
|
+
const SCENE_GUID = 'a0000000-0000-4000-a000-000000000001';
|
|
28
|
+
const MATERIAL_A_GUID = 'a0000000-0000-4000-a000-000000000002';
|
|
29
|
+
const MATERIAL_B_GUID = 'a0000000-0000-4000-a000-000000000003';
|
|
30
|
+
const MESH_GUID = 'a0000000-0000-4000-a000-000000000004';
|
|
31
|
+
const MISSING_SUB_GUID = 'a0000000-0000-4000-a000-000000009999';
|
|
32
|
+
const CYCLE_A_GUID = 'b0000000-0000-4000-b000-000000000001';
|
|
33
|
+
const CYCLE_B_GUID = 'b0000000-0000-4000-b000-000000000002';
|
|
34
|
+
const WRONG_DEFAULT_KIND_GUID = 'a0000000-0000-4000-a000-000000000005';
|
|
35
|
+
|
|
36
|
+
// ECS component reflection is World/owner-local after the ECS architecture
|
|
37
|
+
// update. The recursive prod-path breadcrumb still needs the scene schema, so
|
|
38
|
+
// inject the test vocabulary into AssetRegistry explicitly.
|
|
39
|
+
const BreadcrumbTransform = defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
40
|
+
const BreadcrumbMeshFilter = defineComponent('MeshFilter', {
|
|
41
|
+
assetHandle: 'shared<MeshAsset>',
|
|
42
|
+
});
|
|
43
|
+
const BreadcrumbMeshRenderer = defineComponent('MeshRenderer', {
|
|
44
|
+
materials: 'array<shared<MaterialAsset>>',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function parseGuid(s: string): AssetGuid {
|
|
48
|
+
const r = AssetGuid.parse(s);
|
|
49
|
+
if (!r.ok) throw new Error(`invalid test GUID: ${s}`);
|
|
50
|
+
return r.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function localId(n: number): LocalEntityId {
|
|
54
|
+
return n as LocalEntityId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function makeMesh(): TypesMeshAsset {
|
|
60
|
+
return {
|
|
61
|
+
kind: 'mesh',
|
|
62
|
+
vertices: new Float32Array([
|
|
63
|
+
-0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5, 0, -0.5,
|
|
64
|
+
0, 1, 0, 1, 1, 1, 0, 0, 1,
|
|
65
|
+
]),
|
|
66
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
67
|
+
attributes: {},
|
|
68
|
+
submeshes: [
|
|
69
|
+
{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeMaterialAsset(): MaterialAsset {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'material',
|
|
79
|
+
passes: [
|
|
80
|
+
{
|
|
81
|
+
name: 'forward',
|
|
82
|
+
program: { module: 'test::dummy' },
|
|
83
|
+
renderState: { tags: { LightMode: 'Forward' } },
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
values: {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeTestSceneAsset(subRefs: { meshGuid: string; materialGuids: string[] }): SceneAsset {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'scene',
|
|
93
|
+
entities: [
|
|
94
|
+
{
|
|
95
|
+
localId: localId(0),
|
|
96
|
+
components: {
|
|
97
|
+
Transform: { pos: [0, 0, 0] },
|
|
98
|
+
MeshFilter: { assetHandle: subRefs.meshGuid },
|
|
99
|
+
MeshRenderer: { materials: subRefs.materialGuids },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── registry setup ──────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function makeRegistry(): AssetRegistry {
|
|
109
|
+
return new AssetRegistry(
|
|
110
|
+
makeMockShaderRegistry(),
|
|
111
|
+
undefined,
|
|
112
|
+
undefined,
|
|
113
|
+
undefined,
|
|
114
|
+
undefined,
|
|
115
|
+
new Map<string, Component>([
|
|
116
|
+
[BreadcrumbTransform.name, BreadcrumbTransform],
|
|
117
|
+
[BreadcrumbMeshFilter.name, BreadcrumbMeshFilter],
|
|
118
|
+
[BreadcrumbMeshRenderer.name, BreadcrumbMeshRenderer],
|
|
119
|
+
]),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function preregisterMesh(reg: AssetRegistry): void {
|
|
124
|
+
reg.catalog(parseGuid(MESH_GUID), makeMesh());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function preregisterMaterial(reg: AssetRegistry, guidStr: string): void {
|
|
128
|
+
reg.catalog(parseGuid(guidStr), makeMaterialAsset());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── AC-01: scene + N material + 1 mesh recursive resolution ────────────────
|
|
132
|
+
|
|
133
|
+
describe('AC-01 — scene recursive loadByGuid', () => {
|
|
134
|
+
it('loadByGuid<SceneAsset> in dev mode returns ok when sub-assets pre-registered', async () => {
|
|
135
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
136
|
+
defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
|
|
137
|
+
defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
|
|
138
|
+
|
|
139
|
+
const reg = makeRegistry();
|
|
140
|
+
preregisterMesh(reg);
|
|
141
|
+
preregisterMaterial(reg, MATERIAL_A_GUID);
|
|
142
|
+
preregisterMaterial(reg, MATERIAL_B_GUID);
|
|
143
|
+
|
|
144
|
+
const scene = makeTestSceneAsset({
|
|
145
|
+
meshGuid: MESH_GUID,
|
|
146
|
+
materialGuids: [MATERIAL_A_GUID, MATERIAL_B_GUID],
|
|
147
|
+
});
|
|
148
|
+
reg.catalog(parseGuid(SCENE_GUID), scene);
|
|
149
|
+
|
|
150
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
|
|
151
|
+
expect(result.ok).toBe(true);
|
|
152
|
+
if (result.ok) {
|
|
153
|
+
expect(result.value.kind).toBe('scene');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── AC-06: idempotency ─────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe('AC-06 — idempotency', () => {
|
|
161
|
+
it('two sequential loadByGuid(g) return the same payload in dev mode', async () => {
|
|
162
|
+
const reg = makeRegistry();
|
|
163
|
+
const meshGuid = parseGuid(MESH_GUID);
|
|
164
|
+
const mesh = makeMesh();
|
|
165
|
+
const cat1 = reg.catalog(meshGuid, mesh);
|
|
166
|
+
expect(cat1.ok).toBe(true);
|
|
167
|
+
|
|
168
|
+
const r1 = await reg.loadByGuid<TypesMeshAsset>(meshGuid);
|
|
169
|
+
expect(r1.ok).toBe(true);
|
|
170
|
+
const r2 = await reg.loadByGuid<TypesMeshAsset>(meshGuid);
|
|
171
|
+
expect(r2.ok).toBe(true);
|
|
172
|
+
|
|
173
|
+
if (r1.ok && r2.ok && cat1.ok) {
|
|
174
|
+
expect(r1.value).toBe(r2.value);
|
|
175
|
+
expect(r1.value).toBe(cat1.value);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('loadByGuid returns the same payload for a pre-registered scene', async () => {
|
|
180
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
181
|
+
defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
|
|
182
|
+
defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
|
|
183
|
+
|
|
184
|
+
const reg = makeRegistry();
|
|
185
|
+
preregisterMesh(reg);
|
|
186
|
+
preregisterMaterial(reg, MATERIAL_A_GUID);
|
|
187
|
+
|
|
188
|
+
const scene = makeTestSceneAsset({
|
|
189
|
+
meshGuid: MESH_GUID,
|
|
190
|
+
materialGuids: [MATERIAL_A_GUID],
|
|
191
|
+
});
|
|
192
|
+
reg.catalog(parseGuid(SCENE_GUID), scene);
|
|
193
|
+
|
|
194
|
+
const r1 = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
|
|
195
|
+
const r2 = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
|
|
196
|
+
|
|
197
|
+
expect(r1.ok).toBe(true);
|
|
198
|
+
expect(r2.ok).toBe(true);
|
|
199
|
+
if (r1.ok && r2.ok) {
|
|
200
|
+
expect(r1.value).toBe(r2.value);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── AC-07: transitive failure attribution ───────────────────────────────────
|
|
206
|
+
|
|
207
|
+
describe('AC-07 — transitive failure attribution', () => {
|
|
208
|
+
it('dev-mode loadByGuid returns asset-not-found for missing GUID', async () => {
|
|
209
|
+
const reg = makeRegistry();
|
|
210
|
+
const rMissing = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MISSING_SUB_GUID));
|
|
211
|
+
expect(rMissing.ok).toBe(false);
|
|
212
|
+
if (!rMissing.ok) {
|
|
213
|
+
const err = rMissing.error as AssetError;
|
|
214
|
+
expect(err.code).toBe('asset-not-found');
|
|
215
|
+
expect(typeof err.hint).toBe('string');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('prod-path recursive error includes sub-asset GUID + parent scene GUID in hint', async () => {
|
|
220
|
+
// Construct a catalog where the scene pack and mesh pack exist but the
|
|
221
|
+
// material sub-ref GUID is missing. After M1 recursion is wired in ddcLoad,
|
|
222
|
+
// loadByGuid<SceneAsset> should fail with a breadcrumb hint containing both
|
|
223
|
+
// the missing GUID and the parent scene GUID.
|
|
224
|
+
//
|
|
225
|
+
// Pack fixture format matches the existing load-by-guid-prod fixtures
|
|
226
|
+
// (pack-index is a flat array, pack file uses schemaVersion + assets[]).
|
|
227
|
+
|
|
228
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
229
|
+
defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
|
|
230
|
+
defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
|
|
231
|
+
|
|
232
|
+
const reg = makeRegistry();
|
|
233
|
+
|
|
234
|
+
// Pack-index: scene + mesh present, material missing.
|
|
235
|
+
const packIndex = [
|
|
236
|
+
{ guid: SCENE_GUID, packageUrl: '/packs/scene.pack.json', kind: 'scene' },
|
|
237
|
+
{
|
|
238
|
+
guid: MESH_GUID,
|
|
239
|
+
packageUrl: '/packs/mesh.pack.json',
|
|
240
|
+
kind: 'mesh',
|
|
241
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
242
|
+
},
|
|
243
|
+
// MISSING_SUB_GUID intentionally absent
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
// Scene pack payload references MESH_GUID + MISSING_SUB_GUID.
|
|
247
|
+
// feat-20260622 M4 / w14: refs[] is now the recursion SSOT (D-5). Handle
|
|
248
|
+
// fields carry refs[] indices (resolved by parseScenePayload), and the
|
|
249
|
+
// scene envelope's refs[] (GUID-string projection) drives the unified
|
|
250
|
+
// recursive for-loop. MESH_GUID -> refs[0], MISSING_SUB_GUID -> refs[1].
|
|
251
|
+
const scenePack = {
|
|
252
|
+
schemaVersion: '2.0.0',
|
|
253
|
+
kind: 'internal-text-package',
|
|
254
|
+
assets: [
|
|
255
|
+
{
|
|
256
|
+
guid: SCENE_GUID,
|
|
257
|
+
kind: 'scene',
|
|
258
|
+
payload: {
|
|
259
|
+
entities: [
|
|
260
|
+
{
|
|
261
|
+
localId: 0,
|
|
262
|
+
components: {
|
|
263
|
+
Transform: { pos: [0, 0, 0] },
|
|
264
|
+
MeshFilter: { assetHandle: 0 },
|
|
265
|
+
MeshRenderer: { materials: [1] },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
refs: [MESH_GUID, MISSING_SUB_GUID],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const meshPack = {
|
|
276
|
+
schemaVersion: '2.0.0',
|
|
277
|
+
kind: 'internal-text-package',
|
|
278
|
+
assets: [
|
|
279
|
+
{
|
|
280
|
+
guid: MESH_GUID,
|
|
281
|
+
kind: 'mesh',
|
|
282
|
+
payload: {
|
|
283
|
+
vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
|
|
284
|
+
indices: [0],
|
|
285
|
+
attributes: {},
|
|
286
|
+
},
|
|
287
|
+
refs: [],
|
|
288
|
+
submeshes: [
|
|
289
|
+
{
|
|
290
|
+
indexOffset: 0,
|
|
291
|
+
indexCount: 0,
|
|
292
|
+
vertexCount: 0,
|
|
293
|
+
topology: 'triangle-list',
|
|
294
|
+
materialSlot: 0,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
|
|
298
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
304
|
+
|
|
305
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
306
|
+
if (url === `/pack-index.json`) {
|
|
307
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
308
|
+
}
|
|
309
|
+
if (url === '/packs/scene.pack.json') {
|
|
310
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
|
|
311
|
+
}
|
|
312
|
+
if (url === '/packs/mesh.pack.json') {
|
|
313
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
|
|
314
|
+
}
|
|
315
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
316
|
+
});
|
|
317
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
|
|
321
|
+
|
|
322
|
+
// After M1 impl, this should fail because MISSING_SUB_GUID is not in the
|
|
323
|
+
// catalog. The error hint should contain the missing GUID and the parent
|
|
324
|
+
// scene GUID as a breadcrumb.
|
|
325
|
+
expect(result.ok).toBe(false);
|
|
326
|
+
if (!result.ok) {
|
|
327
|
+
const err = result.error as AssetError;
|
|
328
|
+
expect(
|
|
329
|
+
['asset-not-found', 'asset-fetch-failed', 'asset-not-imported'].includes(err.code),
|
|
330
|
+
).toBe(true);
|
|
331
|
+
expect(typeof err.hint).toBe('string');
|
|
332
|
+
// Breadcrumb (D-7 / B-8): hint contains the missing sub-asset GUID,
|
|
333
|
+
// the parent scene GUID, and the entity localId + component.field path
|
|
334
|
+
// (4-segment chain: scene → entity → component.field → sub-asset).
|
|
335
|
+
expect(err.hint).toContain(MISSING_SUB_GUID);
|
|
336
|
+
expect(err.hint).toContain(SCENE_GUID);
|
|
337
|
+
expect(err.hint).toContain('entity 0');
|
|
338
|
+
expect(err.hint).toContain('MeshRenderer.materials');
|
|
339
|
+
// feat-20260622 verify r1: the breadcrumb provenance is ALSO exposed in
|
|
340
|
+
// structured form on `.detail` so an AI user locates the broken edge by
|
|
341
|
+
// property access, not by parsing the hint (charter P3, requirements
|
|
342
|
+
// error-self-recovery). Only asserted when the propagated error did not
|
|
343
|
+
// carry a more-specific detail of its own.
|
|
344
|
+
const detail = err.detail as
|
|
345
|
+
| {
|
|
346
|
+
referencedByGuid?: string;
|
|
347
|
+
subAssetGuid?: string;
|
|
348
|
+
sceneEntityId?: number;
|
|
349
|
+
sourceField?: { componentName?: string; fieldName?: string; arrayIndex?: number };
|
|
350
|
+
}
|
|
351
|
+
| undefined;
|
|
352
|
+
// Unconditional (verify r2): the structured breadcrumb MUST be delivered
|
|
353
|
+
// on this scene->missing-sub-asset prod path — a guarded assertion would
|
|
354
|
+
// pass vacuously if the provenance silently regressed to hint-only.
|
|
355
|
+
expect(detail?.referencedByGuid?.toLowerCase()).toBe(SCENE_GUID.toLowerCase());
|
|
356
|
+
expect(detail?.subAssetGuid?.toLowerCase()).toBe(MISSING_SUB_GUID.toLowerCase());
|
|
357
|
+
expect(detail?.sceneEntityId).toBe(0);
|
|
358
|
+
expect(detail?.sourceField?.componentName).toBe('MeshRenderer');
|
|
359
|
+
expect(detail?.sourceField?.fieldName).toBe('materials');
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
363
|
+
delete (globalThis as any).fetch;
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('mesh default material ready boundary', () => {
|
|
369
|
+
it.each([
|
|
370
|
+
['empty refs', []],
|
|
371
|
+
['unrelated refs', [WRONG_DEFAULT_KIND_GUID]],
|
|
372
|
+
])('rejects a payload default omitted from %s before ready', async (_label, refs) => {
|
|
373
|
+
const reg = makeRegistry();
|
|
374
|
+
if (refs.length > 0) {
|
|
375
|
+
reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
|
|
376
|
+
kind: 'texture' as const,
|
|
377
|
+
width: 1,
|
|
378
|
+
height: 1,
|
|
379
|
+
format: 'rgba8unorm' as const,
|
|
380
|
+
data: new Uint8Array([255, 255, 255, 255]),
|
|
381
|
+
colorSpace: 'srgb' as const,
|
|
382
|
+
mipmap: false,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const omittedDefaultGuid = 'b0000000-0000-4000-b000-000000000099';
|
|
386
|
+
reg.configurePackIndex('/pack-index.json');
|
|
387
|
+
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
|
388
|
+
if (url === '/pack-index.json') {
|
|
389
|
+
return Promise.resolve({
|
|
390
|
+
ok: true,
|
|
391
|
+
json: () =>
|
|
392
|
+
Promise.resolve([
|
|
393
|
+
{ guid: MESH_GUID, packageUrl: '/packs/missing-edge.pack.json', kind: 'mesh' },
|
|
394
|
+
]),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (url === '/packs/missing-edge.pack.json') {
|
|
398
|
+
return Promise.resolve({
|
|
399
|
+
ok: true,
|
|
400
|
+
json: () =>
|
|
401
|
+
Promise.resolve({
|
|
402
|
+
schemaVersion: '2.0.0',
|
|
403
|
+
kind: 'internal-text-package',
|
|
404
|
+
assets: [
|
|
405
|
+
{
|
|
406
|
+
guid: MESH_GUID,
|
|
407
|
+
kind: 'mesh',
|
|
408
|
+
refs,
|
|
409
|
+
payload: {
|
|
410
|
+
vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
|
|
411
|
+
indices: [0, 0, 0],
|
|
412
|
+
attributes: {},
|
|
413
|
+
submeshes: [
|
|
414
|
+
{
|
|
415
|
+
indexOffset: 0,
|
|
416
|
+
indexCount: 3,
|
|
417
|
+
vertexCount: 1,
|
|
418
|
+
topology: 'triangle-list',
|
|
419
|
+
materialSlot: 0,
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
materialSlots: [{ slotName: 'Body', defaultMaterial: omittedDefaultGuid }],
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
430
|
+
}) as typeof globalThis.fetch;
|
|
431
|
+
|
|
432
|
+
const result = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MESH_GUID));
|
|
433
|
+
expect(result.ok).toBe(false);
|
|
434
|
+
if (!result.ok) {
|
|
435
|
+
expect((result.error as AssetError).detail).toMatchObject({
|
|
436
|
+
meshAssetGuid: MESH_GUID,
|
|
437
|
+
defaultMaterialGuid: omittedDefaultGuid,
|
|
438
|
+
actualKind: 'missing-ref-edge',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
expect(reg.inspect().assets.some((entry) => entry.guid === MESH_GUID)).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('rejects a loaded non-material default before the Mesh becomes ready', async () => {
|
|
445
|
+
const reg = makeRegistry();
|
|
446
|
+
reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
|
|
447
|
+
kind: 'texture' as const,
|
|
448
|
+
width: 1,
|
|
449
|
+
height: 1,
|
|
450
|
+
format: 'rgba8unorm' as const,
|
|
451
|
+
data: new Uint8Array([255, 255, 255, 255]),
|
|
452
|
+
colorSpace: 'srgb' as const,
|
|
453
|
+
mipmap: false,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const packIndex = [
|
|
457
|
+
{ guid: MESH_GUID, packageUrl: '/packs/wrong-default.pack.json', kind: 'mesh' },
|
|
458
|
+
];
|
|
459
|
+
const meshPack = {
|
|
460
|
+
schemaVersion: '2.0.0',
|
|
461
|
+
kind: 'internal-text-package',
|
|
462
|
+
assets: [
|
|
463
|
+
{
|
|
464
|
+
guid: MESH_GUID,
|
|
465
|
+
kind: 'mesh',
|
|
466
|
+
payload: {
|
|
467
|
+
vertices: [
|
|
468
|
+
-0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5,
|
|
469
|
+
0, -0.5, 0, 1, 0, 1, 1, 1, 0, 0, 1,
|
|
470
|
+
],
|
|
471
|
+
indices: [0, 1, 2],
|
|
472
|
+
attributes: {},
|
|
473
|
+
submeshes: [
|
|
474
|
+
{
|
|
475
|
+
indexOffset: 0,
|
|
476
|
+
indexCount: 3,
|
|
477
|
+
vertexCount: 3,
|
|
478
|
+
topology: 'triangle-list',
|
|
479
|
+
materialSlot: 0,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
materialSlots: [
|
|
483
|
+
{
|
|
484
|
+
slotName: 'Body',
|
|
485
|
+
sourceKey: 'gltf:material:0',
|
|
486
|
+
defaultMaterial: WRONG_DEFAULT_KIND_GUID,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
refs: [WRONG_DEFAULT_KIND_GUID],
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
reg.configurePackIndex('/pack-index.json');
|
|
496
|
+
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
|
|
497
|
+
if (url === '/pack-index.json') {
|
|
498
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
499
|
+
}
|
|
500
|
+
if (url === '/packs/wrong-default.pack.json') {
|
|
501
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
|
|
502
|
+
}
|
|
503
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
504
|
+
}) as typeof globalThis.fetch;
|
|
505
|
+
|
|
506
|
+
const result = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MESH_GUID));
|
|
507
|
+
expect(result.ok).toBe(false);
|
|
508
|
+
if (!result.ok) {
|
|
509
|
+
const error = result.error as AssetError;
|
|
510
|
+
expect(error.code).toBe('asset-parse-failed');
|
|
511
|
+
expect(error.detail).toEqual({
|
|
512
|
+
meshAssetGuid: MESH_GUID,
|
|
513
|
+
slotIndex: 0,
|
|
514
|
+
slotName: 'Body',
|
|
515
|
+
defaultMaterialGuid: WRONG_DEFAULT_KIND_GUID,
|
|
516
|
+
actualKind: 'texture',
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
expect(reg.inspect().assets.some((entry) => entry.guid === MESH_GUID)).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// ── M2 GUIDs ───────────────────────────────────────────────────────────────
|
|
524
|
+
const TEXTURE_A_GUID = 'c0000000-0000-4000-c000-000000000001';
|
|
525
|
+
const TEXTURE_B_GUID = 'c0000000-0000-4000-c000-000000000002';
|
|
526
|
+
const TEXTURE_C_GUID = 'c0000000-0000-4000-c000-000000000003';
|
|
527
|
+
const MATERIAL_PARENT_GUID = 'c0000000-0000-4000-c000-000000000004';
|
|
528
|
+
const SKELETON_GUID = 'd0000000-0000-4000-d000-000000000001';
|
|
529
|
+
const SKIN_GUID = 'd0000000-0000-4000-d000-000000000002';
|
|
530
|
+
const SCENE_GLTF_GUID = 'e0000000-0000-4000-e000-000000000001';
|
|
531
|
+
|
|
532
|
+
// ── AC-08: in-flight dedup + cycle ─────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
describe('AC-08 — in-flight dedup + cycle', () => {
|
|
535
|
+
let originalFetch: typeof globalThis.fetch | undefined;
|
|
536
|
+
|
|
537
|
+
beforeEach(() => {
|
|
538
|
+
originalFetch = globalThis.fetch;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
afterEach(() => {
|
|
542
|
+
if (originalFetch !== undefined) {
|
|
543
|
+
globalThis.fetch = originalFetch;
|
|
544
|
+
} else {
|
|
545
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
546
|
+
delete (globalThis as any).fetch;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('concurrent loadByGuid(g) 3-way shares in-flight promise — only 1 pack fetch', async () => {
|
|
551
|
+
const reg = makeRegistry();
|
|
552
|
+
|
|
553
|
+
const packIndex = [
|
|
554
|
+
{
|
|
555
|
+
guid: MESH_GUID,
|
|
556
|
+
packageUrl: '/packs/mesh.pack.json',
|
|
557
|
+
kind: 'mesh',
|
|
558
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
const meshPack = {
|
|
563
|
+
schemaVersion: '2.0.0',
|
|
564
|
+
kind: 'internal-text-package',
|
|
565
|
+
assets: [
|
|
566
|
+
{
|
|
567
|
+
guid: MESH_GUID,
|
|
568
|
+
kind: 'mesh',
|
|
569
|
+
payload: {
|
|
570
|
+
vertices: [
|
|
571
|
+
-0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5,
|
|
572
|
+
0, -0.5, 0, 1, 0, 1, 1, 1, 0, 0, 1,
|
|
573
|
+
],
|
|
574
|
+
indices: [0, 1, 2],
|
|
575
|
+
attributes: {},
|
|
576
|
+
},
|
|
577
|
+
refs: [],
|
|
578
|
+
submeshes: [
|
|
579
|
+
{
|
|
580
|
+
indexOffset: 0,
|
|
581
|
+
indexCount: 3,
|
|
582
|
+
vertexCount: 3,
|
|
583
|
+
topology: 'triangle-list',
|
|
584
|
+
materialSlot: 0,
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
|
|
588
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
594
|
+
|
|
595
|
+
let packFetchCount = 0;
|
|
596
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
597
|
+
if (url === `/pack-index.json`) {
|
|
598
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
599
|
+
}
|
|
600
|
+
if (url === '/packs/mesh.pack.json') {
|
|
601
|
+
packFetchCount++;
|
|
602
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
|
|
603
|
+
}
|
|
604
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
605
|
+
});
|
|
606
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const meshGuid = parseGuid(MESH_GUID);
|
|
610
|
+
const [r1, r2, r3] = await Promise.all([
|
|
611
|
+
reg.loadByGuid<TypesMeshAsset>(meshGuid),
|
|
612
|
+
reg.loadByGuid<TypesMeshAsset>(meshGuid),
|
|
613
|
+
reg.loadByGuid<TypesMeshAsset>(meshGuid),
|
|
614
|
+
]);
|
|
615
|
+
|
|
616
|
+
expect(r1.ok).toBe(true);
|
|
617
|
+
expect(r2.ok).toBe(true);
|
|
618
|
+
expect(r3.ok).toBe(true);
|
|
619
|
+
|
|
620
|
+
if (r1.ok && r2.ok && r3.ok) {
|
|
621
|
+
expect(r1.value).toBe(r2.value);
|
|
622
|
+
expect(r2.value).toBe(r3.value);
|
|
623
|
+
|
|
624
|
+
// In-flight dedup: only 1 pack-index fetch + 1 pack fetch = 2 total
|
|
625
|
+
// (pack-index might be fetched N times since fetchMock is per-call,
|
|
626
|
+
// but packFetchCount should be 1 thanks to inFlight dedup)
|
|
627
|
+
expect(packFetchCount).toBe(1);
|
|
628
|
+
}
|
|
629
|
+
} finally {
|
|
630
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
631
|
+
delete (globalThis as any).fetch;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('cycle A→B→A no stack overflow, both registered', async () => {
|
|
636
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
637
|
+
defineComponent('SceneCycler', { refScene: 'shared<unknown>' });
|
|
638
|
+
|
|
639
|
+
const reg = makeRegistry();
|
|
640
|
+
|
|
641
|
+
// Two SceneAssets that reference each other via a refs[] edge.
|
|
642
|
+
// feat-20260622 M4 / w14: the recursion source is envelope.refs (D-5).
|
|
643
|
+
// A's envelope.refs = [CYCLE_B_GUID], B's = [CYCLE_A_GUID].
|
|
644
|
+
// With register-before-recurse + inFlight dedup: A registers then recurses
|
|
645
|
+
// into B; B registers then recurses into A; A is already catalogued
|
|
646
|
+
// (fast-path hit) / inFlight Promise satisfies it → no stack overflow.
|
|
647
|
+
|
|
648
|
+
const packIndex = [
|
|
649
|
+
{ guid: CYCLE_A_GUID, packageUrl: '/packs/a.pack.json', kind: 'scene' },
|
|
650
|
+
{ guid: CYCLE_B_GUID, packageUrl: '/packs/b.pack.json', kind: 'scene' },
|
|
651
|
+
];
|
|
652
|
+
|
|
653
|
+
const sceneAPack = {
|
|
654
|
+
schemaVersion: '2.0.0',
|
|
655
|
+
kind: 'internal-text-package',
|
|
656
|
+
assets: [
|
|
657
|
+
{
|
|
658
|
+
guid: CYCLE_A_GUID,
|
|
659
|
+
kind: 'scene',
|
|
660
|
+
payload: {
|
|
661
|
+
entities: [
|
|
662
|
+
{
|
|
663
|
+
localId: 0,
|
|
664
|
+
components: {
|
|
665
|
+
Transform: { pos: [0, 0, 0] },
|
|
666
|
+
SceneCycler: { refScene: 0 },
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
refs: [CYCLE_B_GUID],
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
const sceneBPack = {
|
|
677
|
+
schemaVersion: '2.0.0',
|
|
678
|
+
kind: 'internal-text-package',
|
|
679
|
+
assets: [
|
|
680
|
+
{
|
|
681
|
+
guid: CYCLE_B_GUID,
|
|
682
|
+
kind: 'scene',
|
|
683
|
+
payload: {
|
|
684
|
+
entities: [
|
|
685
|
+
{
|
|
686
|
+
localId: 0,
|
|
687
|
+
components: {
|
|
688
|
+
Transform: { pos: [0, 0, 0] },
|
|
689
|
+
SceneCycler: { refScene: 0 },
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
},
|
|
694
|
+
refs: [CYCLE_A_GUID],
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
700
|
+
|
|
701
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
702
|
+
if (url === `/pack-index.json`) {
|
|
703
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
704
|
+
}
|
|
705
|
+
if (url === '/packs/a.pack.json') {
|
|
706
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(sceneAPack) });
|
|
707
|
+
}
|
|
708
|
+
if (url === '/packs/b.pack.json') {
|
|
709
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(sceneBPack) });
|
|
710
|
+
}
|
|
711
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
712
|
+
});
|
|
713
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(CYCLE_A_GUID));
|
|
717
|
+
expect(result.ok).toBe(true);
|
|
718
|
+
|
|
719
|
+
const resB = reg.lookup(parseGuid(CYCLE_B_GUID));
|
|
720
|
+
expect(resB).not.toBe(undefined);
|
|
721
|
+
} finally {
|
|
722
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
723
|
+
delete (globalThis as any).fetch;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// ── AC-02: material recursive (D-8 — values string-only walker) ─────────
|
|
729
|
+
|
|
730
|
+
describe('AC-02 — material recursive loadByGuid', () => {
|
|
731
|
+
it('loadByGuid<MaterialAsset> recurses into texture sub-assets pre-registered in dev mode', async () => {
|
|
732
|
+
// Verify that the recursive loadByGuid<MaterialAsset> walk reaches
|
|
733
|
+
// texture GUIDs from values. The textures are pre-registered in dev
|
|
734
|
+
// mode (fast-path hit), simulating the typical dev workflow where leaf
|
|
735
|
+
// assets are registered before the material. The material itself is
|
|
736
|
+
// loaded via the prod path, and the recursive walk triggers loadByGuid
|
|
737
|
+
// for each texture GUID — each hitting the fast-path cache.
|
|
738
|
+
const reg = makeRegistry();
|
|
739
|
+
|
|
740
|
+
// Pre-register textures in dev mode (fast-path compatible)
|
|
741
|
+
reg.catalog(parseGuid(TEXTURE_A_GUID), {
|
|
742
|
+
kind: 'texture' as const,
|
|
743
|
+
width: 4,
|
|
744
|
+
height: 4,
|
|
745
|
+
format: 'rgba8unorm' as const,
|
|
746
|
+
data: new Uint8Array(64),
|
|
747
|
+
colorSpace: 'srgb' as const,
|
|
748
|
+
mipmap: false,
|
|
749
|
+
});
|
|
750
|
+
reg.catalog(parseGuid(TEXTURE_B_GUID), {
|
|
751
|
+
kind: 'texture' as const,
|
|
752
|
+
width: 4,
|
|
753
|
+
height: 4,
|
|
754
|
+
format: 'rgba8unorm' as const,
|
|
755
|
+
data: new Uint8Array(64),
|
|
756
|
+
colorSpace: 'srgb' as const,
|
|
757
|
+
mipmap: false,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const packIndex = [
|
|
761
|
+
{ guid: MATERIAL_PARENT_GUID, packageUrl: '/packs/material.pack.json', kind: 'material' },
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
const materialPack = {
|
|
765
|
+
schemaVersion: '2.0.0',
|
|
766
|
+
kind: 'internal-text-package',
|
|
767
|
+
assets: [
|
|
768
|
+
{
|
|
769
|
+
guid: MATERIAL_PARENT_GUID,
|
|
770
|
+
kind: 'material',
|
|
771
|
+
payload: {
|
|
772
|
+
passes: [
|
|
773
|
+
{
|
|
774
|
+
name: 'forward',
|
|
775
|
+
program: { module: 'test::dummy' },
|
|
776
|
+
renderState: { tags: { LightMode: 'Forward' } },
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
values: {
|
|
780
|
+
u_albedoMap: TEXTURE_A_GUID,
|
|
781
|
+
u_normalMap: TEXTURE_B_GUID,
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
refs: [],
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
790
|
+
|
|
791
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
792
|
+
if (url === `/pack-index.json`) {
|
|
793
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
794
|
+
}
|
|
795
|
+
if (url === '/packs/material.pack.json') {
|
|
796
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(materialPack) });
|
|
797
|
+
}
|
|
798
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
799
|
+
});
|
|
800
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
const result = await reg.loadByGuid<MaterialAsset>(parseGuid(MATERIAL_PARENT_GUID));
|
|
804
|
+
|
|
805
|
+
expect(result.ok).toBe(true);
|
|
806
|
+
if (result.ok) {
|
|
807
|
+
// Both texture GUIDs should be resolvable (fast-path hit from dev pre-registration)
|
|
808
|
+
expect(reg.lookup(parseGuid(TEXTURE_A_GUID))).not.toBe(undefined);
|
|
809
|
+
expect(reg.lookup(parseGuid(TEXTURE_B_GUID))).not.toBe(undefined);
|
|
810
|
+
}
|
|
811
|
+
} finally {
|
|
812
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
813
|
+
delete (globalThis as any).fetch;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// ── AC-04: skin recursive (D-10 — single skeletonGuid) ─────────────────────-
|
|
819
|
+
|
|
820
|
+
describe('AC-04 — skin recursive loadByGuid', () => {
|
|
821
|
+
it('loadByGuid<SkinAsset> registers skeleton sub-asset via recursive walk', async () => {
|
|
822
|
+
const reg = makeRegistry();
|
|
823
|
+
|
|
824
|
+
// Pre-register skeleton in dev mode (fast-path compatible); the skeleton
|
|
825
|
+
// loader requires Float32Array which does not survive JSON round-trip.
|
|
826
|
+
reg.catalog(parseGuid(SKELETON_GUID), {
|
|
827
|
+
kind: 'skeleton',
|
|
828
|
+
inverseBindMatrices: new Float32Array(32),
|
|
829
|
+
jointCount: 2,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const packIndex = [{ guid: SKIN_GUID, packageUrl: '/packs/skin.pack.json', kind: 'skin' }];
|
|
833
|
+
|
|
834
|
+
const skinPack = {
|
|
835
|
+
schemaVersion: '2.0.0',
|
|
836
|
+
kind: 'internal-text-package',
|
|
837
|
+
assets: [
|
|
838
|
+
{
|
|
839
|
+
guid: SKIN_GUID,
|
|
840
|
+
kind: 'skin',
|
|
841
|
+
payload: {
|
|
842
|
+
jointPaths: ['Root', 'Root/Spine'],
|
|
843
|
+
skeletonGuid: SKELETON_GUID,
|
|
844
|
+
},
|
|
845
|
+
refs: [],
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
851
|
+
|
|
852
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
853
|
+
if (url === `/pack-index.json`) {
|
|
854
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
855
|
+
}
|
|
856
|
+
if (url === '/packs/skin.pack.json') {
|
|
857
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(skinPack) });
|
|
858
|
+
}
|
|
859
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
860
|
+
});
|
|
861
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
const result = await reg.loadByGuid<import('@forgeax/engine-types').SkinAsset>(
|
|
865
|
+
parseGuid(SKIN_GUID),
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
expect(result.ok).toBe(true);
|
|
869
|
+
if (result.ok) {
|
|
870
|
+
// Skeleton GUID should be resolvable (fast-path hit from dev pre-registration)
|
|
871
|
+
expect(reg.lookup(parseGuid(SKELETON_GUID))).not.toBe(undefined);
|
|
872
|
+
}
|
|
873
|
+
} finally {
|
|
874
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
875
|
+
delete (globalThis as any).fetch;
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// ── AC-03: gltf-shaped scene composite (D-9 — via SceneAsset entry) ─────────
|
|
881
|
+
|
|
882
|
+
describe('AC-03 — gltf-shaped scene composite (via SceneAsset, D-9)', () => {
|
|
883
|
+
it('loadByGuid<SceneAsset> registers mesh + material + texture-by-material chain', async () => {
|
|
884
|
+
// D-9: runtime Asset union contains no GltfAsset POD; gltf importer
|
|
885
|
+
// output (mesh + material + texture) is covered via SceneAsset recursion.
|
|
886
|
+
// This fixture mirrors that output: a scene whose entities reference mesh
|
|
887
|
+
// + material, and the material's values reference a texture GUID.
|
|
888
|
+
// The recursive walk must reach all three kinds.
|
|
889
|
+
//
|
|
890
|
+
// Texture is pre-registered in dev mode (fast-path compatible); the
|
|
891
|
+
// material envelope's refs[] carries the texture GUIDs from values,
|
|
892
|
+
// and the recursive loadByGuid hits the dev fast-path.
|
|
893
|
+
|
|
894
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
895
|
+
defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
|
|
896
|
+
defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
|
|
897
|
+
|
|
898
|
+
const reg = makeRegistry();
|
|
899
|
+
|
|
900
|
+
// Pre-register texture in dev mode.
|
|
901
|
+
reg.catalog(parseGuid(TEXTURE_C_GUID), {
|
|
902
|
+
kind: 'texture' as const,
|
|
903
|
+
width: 4,
|
|
904
|
+
height: 4,
|
|
905
|
+
format: 'rgba8unorm' as const,
|
|
906
|
+
data: new Uint8Array(64),
|
|
907
|
+
colorSpace: 'srgb' as const,
|
|
908
|
+
mipmap: false,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const packIndex = [
|
|
912
|
+
{ guid: SCENE_GLTF_GUID, packageUrl: '/packs/scene-gltf.pack.json', kind: 'scene' },
|
|
913
|
+
{
|
|
914
|
+
guid: MESH_GUID,
|
|
915
|
+
packageUrl: '/packs/mesh.pack.json',
|
|
916
|
+
kind: 'mesh',
|
|
917
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
guid: MATERIAL_A_GUID,
|
|
921
|
+
packageUrl: '/packs/mat.pack.json',
|
|
922
|
+
kind: 'material',
|
|
923
|
+
},
|
|
924
|
+
];
|
|
925
|
+
|
|
926
|
+
const scenePack = {
|
|
927
|
+
schemaVersion: '2.0.0',
|
|
928
|
+
kind: 'internal-text-package',
|
|
929
|
+
assets: [
|
|
930
|
+
{
|
|
931
|
+
guid: SCENE_GLTF_GUID,
|
|
932
|
+
kind: 'scene',
|
|
933
|
+
payload: {
|
|
934
|
+
entities: [
|
|
935
|
+
{
|
|
936
|
+
localId: 0,
|
|
937
|
+
components: {
|
|
938
|
+
Transform: { pos: [0, 0, 0] },
|
|
939
|
+
MeshFilter: { assetHandle: 0 },
|
|
940
|
+
MeshRenderer: { materials: [1] },
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
// feat-20260622 M4 / w14: refs[] is the recursion SSOT (D-5).
|
|
946
|
+
// MESH_GUID -> refs[0], MATERIAL_A_GUID -> refs[1].
|
|
947
|
+
refs: [MESH_GUID, MATERIAL_A_GUID],
|
|
948
|
+
},
|
|
949
|
+
],
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
const meshPack = {
|
|
953
|
+
schemaVersion: '2.0.0',
|
|
954
|
+
kind: 'internal-text-package',
|
|
955
|
+
assets: [
|
|
956
|
+
{
|
|
957
|
+
guid: MESH_GUID,
|
|
958
|
+
kind: 'mesh',
|
|
959
|
+
payload: {
|
|
960
|
+
vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
|
|
961
|
+
indices: [0],
|
|
962
|
+
attributes: {},
|
|
963
|
+
},
|
|
964
|
+
refs: [],
|
|
965
|
+
submeshes: [
|
|
966
|
+
{
|
|
967
|
+
indexOffset: 0,
|
|
968
|
+
indexCount: 0,
|
|
969
|
+
vertexCount: 0,
|
|
970
|
+
topology: 'triangle-list',
|
|
971
|
+
materialSlot: 0,
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
|
|
975
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const materialPack = {
|
|
981
|
+
schemaVersion: '2.0.0',
|
|
982
|
+
kind: 'internal-text-package',
|
|
983
|
+
assets: [
|
|
984
|
+
{
|
|
985
|
+
guid: MATERIAL_A_GUID,
|
|
986
|
+
kind: 'material',
|
|
987
|
+
payload: {
|
|
988
|
+
passes: [
|
|
989
|
+
{
|
|
990
|
+
name: 'forward',
|
|
991
|
+
program: { module: 'test::dummy' },
|
|
992
|
+
renderState: { tags: { LightMode: 'Forward' } },
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
// feat-20260622 M4 / w14: texture handle fields carry refs[] indices
|
|
996
|
+
// (resolved by materialLoader); the texture edge rides material
|
|
997
|
+
// refs[] so the unified for-loop recurses into it.
|
|
998
|
+
values: {
|
|
999
|
+
u_baseColorMap: 0,
|
|
1000
|
+
u_normalMap: 0, // same texture via two params (diamond)
|
|
1001
|
+
u_metallic: 0.5, // non-GUID value → skipped
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
refs: [TEXTURE_C_GUID],
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
1010
|
+
|
|
1011
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
1012
|
+
if (url === `/pack-index.json`) {
|
|
1013
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
1014
|
+
}
|
|
1015
|
+
if (url === '/packs/scene-gltf.pack.json') {
|
|
1016
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
|
|
1017
|
+
}
|
|
1018
|
+
if (url === '/packs/mesh.pack.json') {
|
|
1019
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
|
|
1020
|
+
}
|
|
1021
|
+
if (url === '/packs/mat.pack.json') {
|
|
1022
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(materialPack) });
|
|
1023
|
+
}
|
|
1024
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
1025
|
+
});
|
|
1026
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GLTF_GUID));
|
|
1030
|
+
|
|
1031
|
+
expect(result.ok).toBe(true);
|
|
1032
|
+
if (result.ok) {
|
|
1033
|
+
// All three kinds should be registered:
|
|
1034
|
+
// - mesh (via scene entity MeshFilter, prod path)
|
|
1035
|
+
// - material (via scene entity MeshRenderer, prod path)
|
|
1036
|
+
// - texture (via material values, dev fast-path hit)
|
|
1037
|
+
expect(reg.lookup(parseGuid(MESH_GUID))).not.toBe(undefined);
|
|
1038
|
+
expect(reg.lookup(parseGuid(MATERIAL_A_GUID))).not.toBe(undefined);
|
|
1039
|
+
expect(reg.lookup(parseGuid(TEXTURE_C_GUID))).not.toBe(undefined);
|
|
1040
|
+
}
|
|
1041
|
+
} finally {
|
|
1042
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
1043
|
+
delete (globalThis as any).fetch;
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// ── M3: leaf coverage (AC-05) ──────────────────────────────────────────────
|
|
1049
|
+
//
|
|
1050
|
+
// The inline-payload leaf kinds in the closed Asset union carry no sub-asset
|
|
1051
|
+
// edges; the importer fills envelope.refs = [] for them. The spec-enumerated
|
|
1052
|
+
// leaves (mesh / texture / animation-clip / audio / font) plus extras
|
|
1053
|
+
// (sampler / shader / skeleton / render-pipeline) -- tested together for union
|
|
1054
|
+
// completeness. (equirect rides the Pack v2 artifact path, covered by
|
|
1055
|
+
// asset-registry-hdr-equirect.test.ts, not the inline-payload roundtrip here.)
|
|
1056
|
+
//
|
|
1057
|
+
// AC-05 per-leaf assertions:
|
|
1058
|
+
// loadByGuid (fast-path) returns the catalogued payload
|
|
1059
|
+
// lookup after catalog returns the payload
|
|
1060
|
+
// inspect().assets count incremented by 1 from catalog (zero extra mutations)
|
|
1061
|
+
|
|
1062
|
+
const LEAF_TEST_GUIDS: Record<string, string> = {
|
|
1063
|
+
mesh: 'f0000000-0000-4000-f000-000000000001',
|
|
1064
|
+
texture: 'f0000000-0000-4000-f000-000000000002',
|
|
1065
|
+
sampler: 'f0000000-0000-4000-f000-000000000004',
|
|
1066
|
+
program: 'f0000000-0000-4000-f000-000000000005',
|
|
1067
|
+
skeleton: 'f0000000-0000-4000-f000-000000000006',
|
|
1068
|
+
'animation-clip': 'f0000000-0000-4000-f000-000000000007',
|
|
1069
|
+
audio: 'f0000000-0000-4000-f000-000000000008',
|
|
1070
|
+
font: 'f0000000-0000-4000-f000-000000000009',
|
|
1071
|
+
'render-pipeline': 'f0000000-0000-4000-f000-00000000000a',
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
interface LeafFixture {
|
|
1075
|
+
readonly label: string;
|
|
1076
|
+
readonly kind: string;
|
|
1077
|
+
readonly makeAsset: () => import('@forgeax/engine-types').Asset;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const LEAF_FIXTURES: readonly LeafFixture[] = [
|
|
1081
|
+
{
|
|
1082
|
+
label: 'mesh',
|
|
1083
|
+
kind: 'mesh',
|
|
1084
|
+
makeAsset: () => ({
|
|
1085
|
+
kind: 'mesh' as const,
|
|
1086
|
+
vertices: new Float32Array(12),
|
|
1087
|
+
indices: new Uint16Array([0]),
|
|
1088
|
+
attributes: {},
|
|
1089
|
+
submeshes: [
|
|
1090
|
+
{
|
|
1091
|
+
indexOffset: 0,
|
|
1092
|
+
indexCount: 0,
|
|
1093
|
+
vertexCount: 0,
|
|
1094
|
+
topology: 'triangle-list' as const,
|
|
1095
|
+
materialSlot: 0,
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
1099
|
+
}),
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
label: 'texture',
|
|
1103
|
+
kind: 'texture',
|
|
1104
|
+
makeAsset: () => ({
|
|
1105
|
+
kind: 'texture' as const,
|
|
1106
|
+
width: 4,
|
|
1107
|
+
height: 4,
|
|
1108
|
+
format: 'rgba8unorm' as const,
|
|
1109
|
+
data: new Uint8Array(64),
|
|
1110
|
+
colorSpace: 'srgb' as const,
|
|
1111
|
+
mipmap: false,
|
|
1112
|
+
}),
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
label: 'sampler',
|
|
1116
|
+
kind: 'sampler',
|
|
1117
|
+
makeAsset: () => ({
|
|
1118
|
+
kind: 'sampler' as const,
|
|
1119
|
+
magFilter: 'linear' as const,
|
|
1120
|
+
minFilter: 'linear' as const,
|
|
1121
|
+
}),
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
label: 'skeleton',
|
|
1125
|
+
kind: 'skeleton',
|
|
1126
|
+
makeAsset: () => ({
|
|
1127
|
+
kind: 'skeleton' as const,
|
|
1128
|
+
inverseBindMatrices: new Float32Array(16),
|
|
1129
|
+
jointCount: 1,
|
|
1130
|
+
}),
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
label: 'animation-clip',
|
|
1134
|
+
kind: 'animation-clip',
|
|
1135
|
+
makeAsset: () => ({
|
|
1136
|
+
kind: 'animation-clip' as const,
|
|
1137
|
+
duration: 1.0,
|
|
1138
|
+
channels: [],
|
|
1139
|
+
}),
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
label: 'audio',
|
|
1143
|
+
kind: 'audio',
|
|
1144
|
+
makeAsset: () => ({
|
|
1145
|
+
kind: 'audio' as const,
|
|
1146
|
+
sourceKey: 'fixture-audio',
|
|
1147
|
+
mediaType: 'audio/ogg' as const,
|
|
1148
|
+
bytes: new Uint8Array(),
|
|
1149
|
+
}),
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
label: 'font',
|
|
1153
|
+
kind: 'font',
|
|
1154
|
+
makeAsset: () => ({
|
|
1155
|
+
kind: 'font' as const,
|
|
1156
|
+
// D-19: FontAsset.atlas/sampler are embedded AssetGuids (not handles).
|
|
1157
|
+
atlas: parseGuid('a0000000-0000-4000-a000-00000000f001'),
|
|
1158
|
+
sampler: parseGuid('a0000000-0000-4000-a000-00000000f002'),
|
|
1159
|
+
glyphs: {},
|
|
1160
|
+
common: {
|
|
1161
|
+
lineHeight: 0,
|
|
1162
|
+
base: 0,
|
|
1163
|
+
distanceRange: 0,
|
|
1164
|
+
pxRange: 0,
|
|
1165
|
+
atlasWidth: 0,
|
|
1166
|
+
atlasHeight: 0,
|
|
1167
|
+
},
|
|
1168
|
+
}),
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
label: 'render-pipeline',
|
|
1172
|
+
kind: 'render-pipeline',
|
|
1173
|
+
makeAsset: () => ({
|
|
1174
|
+
kind: 'render-pipeline' as const,
|
|
1175
|
+
pipelineId: 'forgeax::urp' as const,
|
|
1176
|
+
}),
|
|
1177
|
+
},
|
|
1178
|
+
];
|
|
1179
|
+
|
|
1180
|
+
describe('AC-05 -- leaf assets (no sub-asset edges)', () => {
|
|
1181
|
+
// Leaf kinds carry no recursion edges; the importer fills envelope.refs = []
|
|
1182
|
+
// and loadByGuid does not recurse for them. These tests confirm the registry
|
|
1183
|
+
// lifecycle (catalog -> fast-path loadByGuid -> lookup) per leaf kind.
|
|
1184
|
+
|
|
1185
|
+
it.each(
|
|
1186
|
+
LEAF_FIXTURES.map((f) => [f.label, f]),
|
|
1187
|
+
)('loadByGuid<%s> returns ok and asset is catalogued via fast-path', async (_label, fixture) => {
|
|
1188
|
+
const reg = makeRegistry();
|
|
1189
|
+
const guidStr = LEAF_TEST_GUIDS[fixture.label];
|
|
1190
|
+
if (guidStr === undefined) throw new Error(`missing leaf test GUID for ${fixture.label}`);
|
|
1191
|
+
const guid = parseGuid(guidStr);
|
|
1192
|
+
const asset = fixture.makeAsset();
|
|
1193
|
+
|
|
1194
|
+
// Record pre-catalogue state
|
|
1195
|
+
const assetsBefore = reg.inspect().assets.length;
|
|
1196
|
+
|
|
1197
|
+
// Catalogue the leaf
|
|
1198
|
+
const cat = reg.catalog(guid, asset);
|
|
1199
|
+
expect(cat.ok).toBe(true);
|
|
1200
|
+
|
|
1201
|
+
// Verify catalog added exactly one entry
|
|
1202
|
+
const assetsAfterCatalog = reg.inspect().assets.length;
|
|
1203
|
+
expect(assetsAfterCatalog).toBe(assetsBefore + 1);
|
|
1204
|
+
|
|
1205
|
+
// loadByGuid fast-path (already catalogued) returns the payload
|
|
1206
|
+
const result = await reg.loadByGuid(guid);
|
|
1207
|
+
|
|
1208
|
+
expect(result.ok).toBe(true);
|
|
1209
|
+
if (result.ok && cat.ok) {
|
|
1210
|
+
expect(result.value).toBe(cat.value);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// No extra registry mutations from loadByGuid
|
|
1214
|
+
const assetsAfterLoad = reg.inspect().assets.length;
|
|
1215
|
+
expect(assetsAfterLoad).toBe(assetsBefore + 1);
|
|
1216
|
+
|
|
1217
|
+
// lookup confirms catalogue
|
|
1218
|
+
expect(reg.lookup(guid)).not.toBe(undefined);
|
|
1219
|
+
});
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// ── feat-20260612 M2 fixup: SceneAsset.skinGuids browser-async-load chain ──
|
|
1223
|
+
//
|
|
1224
|
+
// REGRESSION COVERAGE: before the fix, browser-path `loadByGuid<SceneAsset>`
|
|
1225
|
+
// recursively followed entity-component handle fields (mesh / material /
|
|
1226
|
+
// texture / skeleton) but had no cross-edge to SkinAssets -- SkinAsset is a
|
|
1227
|
+
// sibling identified by matching `skeletonGuid`, not by any handle field on a
|
|
1228
|
+
// SceneEntity component. After loadByGuid<SceneAsset> resolved, instantiate()
|
|
1229
|
+
// invoked `postSpawnResolveJoints` which silently `continue`d when
|
|
1230
|
+
// `resolver.resolveSkinAsset` returned undefined. The result: `Skin.joints`
|
|
1231
|
+
// stayed length=0, and the M2-introduced `JointCountMismatchError` fail-fast
|
|
1232
|
+
// in render-system-extract triggered every frame on the browser path. dawn
|
|
1233
|
+
// smoke missed it because dawn pre-registers every sub-asset before
|
|
1234
|
+
// instantiate (sync register-handle path), bypassing the recursive walk.
|
|
1235
|
+
//
|
|
1236
|
+
// The fix:
|
|
1237
|
+
// 1. SceneAsset POD now carries `skinGuids?: readonly string[]` (a list of
|
|
1238
|
+
// SkinAsset GUIDs the scene's skinned entities reference). gltfImporter
|
|
1239
|
+
// populates it; on-disk pack JSON stores it as either string GUIDs or
|
|
1240
|
+
// refs[] indices (parseScenePayload accepts both).
|
|
1241
|
+
// 2. The scene envelope's `refs[]` includes each SkinAsset GUID so the
|
|
1242
|
+
// recursive `loadByGuid` walk pulls every SkinAsset.
|
|
1243
|
+
// 3. `postSpawnResolveJoints` upgraded its silent `continue` on missing
|
|
1244
|
+
// SkinAsset to a fail-fast `skin-asset-unresolved` error, so a future
|
|
1245
|
+
// regression of the cross-edge wiring surfaces immediately rather than
|
|
1246
|
+
// hiding behind a frame-1 JointCountMismatchError.
|
|
1247
|
+
const SKIN_FIXUP_SCENE_GUID = 'f0000000-0000-4000-f000-000000000001';
|
|
1248
|
+
const SKIN_FIXUP_SKIN_GUID = 'f0000000-0000-4000-f000-000000000002';
|
|
1249
|
+
const SKIN_FIXUP_SKELETON_GUID = 'f0000000-0000-4000-f000-000000000003';
|
|
1250
|
+
|
|
1251
|
+
describe('feat-20260612 M2 fixup — SceneAsset.skinGuids cross-edge', () => {
|
|
1252
|
+
let originalFetch: typeof globalThis.fetch | undefined;
|
|
1253
|
+
|
|
1254
|
+
beforeEach(() => {
|
|
1255
|
+
originalFetch = globalThis.fetch;
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
afterEach(() => {
|
|
1259
|
+
if (originalFetch !== undefined) {
|
|
1260
|
+
globalThis.fetch = originalFetch;
|
|
1261
|
+
} else {
|
|
1262
|
+
// biome-ignore lint/suspicious/noExplicitAny: test teardown
|
|
1263
|
+
delete (globalThis as any).fetch;
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it('loadByGuid<SceneAsset> recursively loads SkinAsset via skinGuids cross-edge (browser pack-fetch path)', async () => {
|
|
1268
|
+
// Reproduces the browser-async-pack-fetch path: SkinAsset is NOT
|
|
1269
|
+
// pre-registered; loadByGuid<SceneAsset> must reach it through the
|
|
1270
|
+
// skinGuids cross-edge encoded in the scene pack body. Without the fix,
|
|
1271
|
+
// the SkinAsset is never loaded and no resolveGuid lookup succeeds.
|
|
1272
|
+
|
|
1273
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
1274
|
+
defineComponent('Skin', {
|
|
1275
|
+
skeleton: 'shared<SkeletonAsset>',
|
|
1276
|
+
joints: 'array<entity>',
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
const reg = makeRegistry();
|
|
1280
|
+
|
|
1281
|
+
// Pre-register Skeleton in dev mode (Float32Array does not survive JSON
|
|
1282
|
+
// round-trip; the skeleton-loader's dual contract handles array form,
|
|
1283
|
+
// but this fixture keeps the focus on skin cross-edge wiring).
|
|
1284
|
+
reg.catalog(parseGuid(SKIN_FIXUP_SKELETON_GUID), {
|
|
1285
|
+
kind: 'skeleton',
|
|
1286
|
+
inverseBindMatrices: new Float32Array(64),
|
|
1287
|
+
jointCount: 1,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const packIndex = [
|
|
1291
|
+
{ guid: SKIN_FIXUP_SCENE_GUID, packageUrl: '/packs/scene.pack.json', kind: 'scene' },
|
|
1292
|
+
{ guid: SKIN_FIXUP_SKIN_GUID, packageUrl: '/packs/skin.pack.json', kind: 'skin' },
|
|
1293
|
+
];
|
|
1294
|
+
|
|
1295
|
+
const scenePack = {
|
|
1296
|
+
schemaVersion: '2.0.0',
|
|
1297
|
+
kind: 'internal-text-package',
|
|
1298
|
+
assets: [
|
|
1299
|
+
{
|
|
1300
|
+
guid: SKIN_FIXUP_SCENE_GUID,
|
|
1301
|
+
kind: 'scene',
|
|
1302
|
+
payload: {
|
|
1303
|
+
entities: [
|
|
1304
|
+
{
|
|
1305
|
+
localId: 0,
|
|
1306
|
+
components: {
|
|
1307
|
+
Transform: { pos: [0, 0, 0] },
|
|
1308
|
+
Skin: { skeleton: 0 },
|
|
1309
|
+
},
|
|
1310
|
+
},
|
|
1311
|
+
],
|
|
1312
|
+
// refs[]-index form (browser JSON-roundtrip shape)
|
|
1313
|
+
skinGuids: [1],
|
|
1314
|
+
},
|
|
1315
|
+
// refs ordering matches indices used by both the Skin.skeleton
|
|
1316
|
+
// handle field (resolved via HANDLE_FIELD_NAMES) and the
|
|
1317
|
+
// skinGuids[] integer form.
|
|
1318
|
+
refs: [SKIN_FIXUP_SKELETON_GUID, SKIN_FIXUP_SKIN_GUID],
|
|
1319
|
+
},
|
|
1320
|
+
],
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const skinPack = {
|
|
1324
|
+
schemaVersion: '2.0.0',
|
|
1325
|
+
kind: 'internal-text-package',
|
|
1326
|
+
assets: [
|
|
1327
|
+
{
|
|
1328
|
+
guid: SKIN_FIXUP_SKIN_GUID,
|
|
1329
|
+
kind: 'skin',
|
|
1330
|
+
payload: {
|
|
1331
|
+
jointPaths: ['Root'],
|
|
1332
|
+
skeletonGuid: SKIN_FIXUP_SKELETON_GUID,
|
|
1333
|
+
},
|
|
1334
|
+
refs: [SKIN_FIXUP_SKELETON_GUID],
|
|
1335
|
+
},
|
|
1336
|
+
],
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
reg.configurePackIndex(`/pack-index.json`);
|
|
1340
|
+
|
|
1341
|
+
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
|
1342
|
+
if (url === `/pack-index.json`) {
|
|
1343
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
|
|
1344
|
+
}
|
|
1345
|
+
if (url === '/packs/scene.pack.json') {
|
|
1346
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
|
|
1347
|
+
}
|
|
1348
|
+
if (url === '/packs/skin.pack.json') {
|
|
1349
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(skinPack) });
|
|
1350
|
+
}
|
|
1351
|
+
return Promise.resolve({ ok: false, status: 404 });
|
|
1352
|
+
});
|
|
1353
|
+
globalThis.fetch = fetchMock as typeof globalThis.fetch;
|
|
1354
|
+
|
|
1355
|
+
// BEFORE fix: SkinAsset not catalogued -> lookup fails.
|
|
1356
|
+
// AFTER fix: the scene envelope's refs[] carries the SkinAsset GUID ->
|
|
1357
|
+
// loadByGuid recursion pulls it -> lookup succeeds.
|
|
1358
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(SKIN_FIXUP_SCENE_GUID));
|
|
1359
|
+
expect(result.ok).toBe(true);
|
|
1360
|
+
|
|
1361
|
+
expect(reg.lookup(parseGuid(SKIN_FIXUP_SKIN_GUID))).not.toBe(undefined);
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
it('parseScenePayload accepts skinGuids as inline strings (in-memory form)', async () => {
|
|
1365
|
+
// dawn-smoke / direct register path stores skinGuids as raw GUID strings
|
|
1366
|
+
// (not refs[] indices). parseScenePayload's resolveSkinGuids must accept
|
|
1367
|
+
// both shapes for round-trip symmetry with the browser pack-fetch path.
|
|
1368
|
+
|
|
1369
|
+
defineComponent('Transform', { pos: 'array<f32, 3>' });
|
|
1370
|
+
|
|
1371
|
+
const reg = makeRegistry();
|
|
1372
|
+
reg.catalog(parseGuid(SKIN_FIXUP_SKELETON_GUID), {
|
|
1373
|
+
kind: 'skeleton',
|
|
1374
|
+
inverseBindMatrices: new Float32Array(64),
|
|
1375
|
+
jointCount: 1,
|
|
1376
|
+
});
|
|
1377
|
+
reg.catalog(parseGuid(SKIN_FIXUP_SKIN_GUID), {
|
|
1378
|
+
kind: 'skin',
|
|
1379
|
+
jointPaths: ['Root'],
|
|
1380
|
+
skeletonGuid: SKIN_FIXUP_SKELETON_GUID,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// Build a SceneAsset POD with inline skinGuids (no refs[] indices).
|
|
1384
|
+
const scene: SceneAsset = {
|
|
1385
|
+
kind: 'scene',
|
|
1386
|
+
entities: [{ localId: localId(0), components: { Transform: { pos: [0, 0, 0] } } }],
|
|
1387
|
+
skinGuids: [SKIN_FIXUP_SKIN_GUID],
|
|
1388
|
+
};
|
|
1389
|
+
reg.catalog(parseGuid(SKIN_FIXUP_SCENE_GUID), scene);
|
|
1390
|
+
|
|
1391
|
+
const result = await reg.loadByGuid<SceneAsset>(parseGuid(SKIN_FIXUP_SCENE_GUID));
|
|
1392
|
+
expect(result.ok).toBe(true);
|
|
1393
|
+
|
|
1394
|
+
// Inline-string skinGuids form resolves through the recursive walk too.
|
|
1395
|
+
expect(reg.lookup(parseGuid(SKIN_FIXUP_SKIN_GUID))).not.toBe(undefined);
|
|
1396
|
+
});
|
|
1397
|
+
});
|