@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,987 @@
|
|
|
1
|
+
import * as SceneOwner from '@forgeax/engine-scene';
|
|
2
|
+
|
|
3
|
+
// scene-mount-roundtrip.test.ts — M4 round-trip acceptance tests
|
|
4
|
+
// (feat-20260703-collect-nested-sceneinstance-to-mount-roundtrip).
|
|
5
|
+
//
|
|
6
|
+
// Coverage:
|
|
7
|
+
// m4-t1: single-layer mount round-trip (collect -> serialize -> reload -> collect)
|
|
8
|
+
// m4-t2: double-layer nested mount round-trip
|
|
9
|
+
// m4-t3: fixed-point — post-normalization second collect equals third collect
|
|
10
|
+
//
|
|
11
|
+
// These tests exercise the full AC-04 chain:
|
|
12
|
+
// instantiate -> rootsToSceneAsset -> serializeSceneAssetToPack ->
|
|
13
|
+
// parse back -> catalog -> registry.instantiate (reload) ->
|
|
14
|
+
// rootsToSceneAsset -> structural equivalence
|
|
15
|
+
|
|
16
|
+
import { AnimationPlayer } from '@forgeax/engine-animation';
|
|
17
|
+
import type { Asset } from '@forgeax/engine-assets-runtime';
|
|
18
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
19
|
+
import { type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
20
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
21
|
+
import { SceneInstance } from '@forgeax/engine-render';
|
|
22
|
+
import type { Handle, MountOverride, SceneAsset } from '@forgeax/engine-types';
|
|
23
|
+
import { describe, expect, it } from 'vitest';
|
|
24
|
+
import { rootsToSceneAsset, serializeSceneAssetToPack } from '../collect-scene-asset';
|
|
25
|
+
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
26
|
+
import { registerSceneComponents } from './helpers/register-scene-components';
|
|
27
|
+
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Helpers
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
function mkReg(): AssetRegistry {
|
|
33
|
+
return new AssetRegistry(makeMockShaderRegistry());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pg(s: string): AssetGuid {
|
|
37
|
+
const r = AssetGuid.parse(s);
|
|
38
|
+
if (!r.ok) throw new Error(`bad GUID: ${s}`);
|
|
39
|
+
return r.value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cat(reg: AssetRegistry, g: string, p: SceneAsset): void {
|
|
43
|
+
reg.catalog(pg(g), p as Asset);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rs(w: World, a: SceneAsset): Handle<'SceneAsset', 'shared'> {
|
|
47
|
+
registerSceneComponents(w, [AnimationPlayer]);
|
|
48
|
+
return w.allocSharedRef('SceneAsset', a);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Access parseAssetPayload via type assertion (same pattern as M3 tests). */
|
|
52
|
+
function accessParseScenePayload(reg: AssetRegistry) {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
const internal = reg as unknown as {
|
|
55
|
+
parseAssetPayload(kind: string, payload: Record<string, unknown>, refs?: string[]): unknown;
|
|
56
|
+
};
|
|
57
|
+
return (
|
|
58
|
+
kind: string,
|
|
59
|
+
payload: Record<string, unknown>,
|
|
60
|
+
refs?: readonly string[] | undefined,
|
|
61
|
+
): SceneAsset | undefined => {
|
|
62
|
+
const result = internal.parseAssetPayload(kind, payload, refs as string[] | undefined);
|
|
63
|
+
if (result === undefined) return undefined;
|
|
64
|
+
if (
|
|
65
|
+
typeof result === 'object' &&
|
|
66
|
+
result !== null &&
|
|
67
|
+
'kind' in result &&
|
|
68
|
+
(result as { kind: string }).kind === 'scene'
|
|
69
|
+
) {
|
|
70
|
+
return result as SceneAsset;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Extract payload and refs from serializeSceneAssetToPack result. */
|
|
77
|
+
function unpackSerialized(
|
|
78
|
+
pack: Record<string, unknown>,
|
|
79
|
+
): { payload: Record<string, unknown>; refs: readonly string[] | undefined } | undefined {
|
|
80
|
+
const assets = pack.assets as readonly Record<string, unknown>[] | undefined;
|
|
81
|
+
if (!assets || assets.length === 0) return undefined;
|
|
82
|
+
const a = assets[0];
|
|
83
|
+
if (!a) return undefined;
|
|
84
|
+
const payload = a.payload as Record<string, unknown> | undefined;
|
|
85
|
+
if (!payload) return undefined;
|
|
86
|
+
return { payload, refs: a.refs as readonly string[] | undefined };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Deep-structural comparison of two SceneAsset objects.
|
|
91
|
+
* Returns true if mounts (source, memberFirst, memberCount, parent) and
|
|
92
|
+
* entities (component types, field values) are structurally equivalent.
|
|
93
|
+
* localId values are compared as-is (they should be stable across collects).
|
|
94
|
+
*/
|
|
95
|
+
function scenesAreStructurallyEquivalent(a: SceneAsset, b: SceneAsset): boolean {
|
|
96
|
+
const am = a.mounts ?? [];
|
|
97
|
+
const bm = b.mounts ?? [];
|
|
98
|
+
if (am.length !== bm.length) return false;
|
|
99
|
+
for (let i = 0; i < am.length; i++) {
|
|
100
|
+
const ma = am[i];
|
|
101
|
+
const mb = bm[i];
|
|
102
|
+
if (!ma || !mb) return false;
|
|
103
|
+
if (ma.source !== mb.source) return false;
|
|
104
|
+
if (ma.memberFirst !== mb.memberFirst) return false;
|
|
105
|
+
if (ma.memberCount !== mb.memberCount) return false;
|
|
106
|
+
if (ma.parent !== mb.parent) return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ae = a.entities;
|
|
110
|
+
const be = b.entities;
|
|
111
|
+
if (ae.length !== be.length) return false;
|
|
112
|
+
for (let i = 0; i < ae.length; i++) {
|
|
113
|
+
const ea = ae[i];
|
|
114
|
+
const eb = be[i];
|
|
115
|
+
if (!ea || !eb) return false;
|
|
116
|
+
if (ea.localId !== eb.localId) return false;
|
|
117
|
+
|
|
118
|
+
const compsA = ea.components as Record<string, Record<string, unknown>>;
|
|
119
|
+
const compsB = eb.components as Record<string, Record<string, unknown>>;
|
|
120
|
+
const keysA = Object.keys(compsA).sort();
|
|
121
|
+
const keysB = Object.keys(compsB).sort();
|
|
122
|
+
if (keysA.length !== keysB.length) return false;
|
|
123
|
+
for (let j = 0; j < keysA.length; j++) {
|
|
124
|
+
if (keysA[j] !== keysB[j]) return false;
|
|
125
|
+
}
|
|
126
|
+
for (const key of keysA) {
|
|
127
|
+
const fieldsA = compsA[key];
|
|
128
|
+
const fieldsB = compsB[key];
|
|
129
|
+
if (!fieldsA || !fieldsB) return false;
|
|
130
|
+
const fKeysA = Object.keys(fieldsA).sort();
|
|
131
|
+
const fKeysB = Object.keys(fieldsB).sort();
|
|
132
|
+
if (fKeysA.length !== fKeysB.length) return false;
|
|
133
|
+
for (let j = 0; j < fKeysA.length; j++) {
|
|
134
|
+
if (fKeysA[j] !== fKeysB[j]) return false;
|
|
135
|
+
const va = fieldsA[fKeysA[j] as string];
|
|
136
|
+
const vb = fieldsB[fKeysB[j] as string];
|
|
137
|
+
if (JSON.stringify(va) !== JSON.stringify(vb)) return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Compute the totalSlots of a SceneAsset as _instantiateSceneAsset would:
|
|
146
|
+
* countBaseline = entities.length + mounts.length + sum(mounts.memberCount).
|
|
147
|
+
*/
|
|
148
|
+
function computeTotalSlots(scene: SceneAsset): number {
|
|
149
|
+
const ms = scene.mounts ?? [];
|
|
150
|
+
const memberSum = ms.reduce((s, m) => s + m.memberCount, 0);
|
|
151
|
+
const countBaseline = scene.entities.length + ms.length + memberSum;
|
|
152
|
+
let maxLocalId = -1;
|
|
153
|
+
for (const e of scene.entities) {
|
|
154
|
+
maxLocalId = Math.max(maxLocalId, e.localId as unknown as number);
|
|
155
|
+
}
|
|
156
|
+
for (const m of ms) {
|
|
157
|
+
maxLocalId = Math.max(maxLocalId, m.localId as unknown as number);
|
|
158
|
+
const last = (m.memberFirst as unknown as number) + m.memberCount - 1;
|
|
159
|
+
maxLocalId = Math.max(maxLocalId, last);
|
|
160
|
+
}
|
|
161
|
+
return Math.max(countBaseline, maxLocalId + 1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const G1 = 'cbe42beb-8975-5096-b3a1-3dda4cb4c077';
|
|
165
|
+
const G2 = 'f6af7007-158f-4d92-9e47-93bf2f213e1f';
|
|
166
|
+
const G3 = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
167
|
+
const G4 = 'b2c3d4e5-f6a7-8901-bcde-f12345678901';
|
|
168
|
+
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
170
|
+
// m4-t1: single-layer mount round-trip
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
172
|
+
|
|
173
|
+
describe('m4-t1 — single-layer mount round-trip', () => {
|
|
174
|
+
it('full round-trip: collect -> serialize -> reload -> collect — mounts and GUIDs preserved', () => {
|
|
175
|
+
const reg = mkReg();
|
|
176
|
+
const w = new World();
|
|
177
|
+
|
|
178
|
+
// Step 1: Build child scene.
|
|
179
|
+
const child: SceneAsset = {
|
|
180
|
+
kind: 'scene',
|
|
181
|
+
entities: [
|
|
182
|
+
{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
|
|
183
|
+
{ localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
cat(reg, G1, child);
|
|
187
|
+
|
|
188
|
+
// Step 2: Build parent scene with mount (source = GUID string, post-parse shape).
|
|
189
|
+
const parent: SceneAsset = {
|
|
190
|
+
kind: 'scene',
|
|
191
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
192
|
+
mounts: [
|
|
193
|
+
{
|
|
194
|
+
localId: 1 as never,
|
|
195
|
+
source: G1,
|
|
196
|
+
memberFirst: 2 as never,
|
|
197
|
+
memberCount: 2,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
cat(reg, G2, parent);
|
|
202
|
+
const ph = rs(w, parent);
|
|
203
|
+
|
|
204
|
+
// Step 3: Instantiate parent via registry.
|
|
205
|
+
const inst = reg.instantiate(ph, w);
|
|
206
|
+
expect(inst.ok).toBe(true);
|
|
207
|
+
if (!inst.ok) return;
|
|
208
|
+
const root = inst.value;
|
|
209
|
+
|
|
210
|
+
// Verify root exists and has descendants including a child SceneInstance.
|
|
211
|
+
let descendantCount = 0;
|
|
212
|
+
let hasChildSceneInstance = false;
|
|
213
|
+
for (const c of w.iterDescendants(root)) {
|
|
214
|
+
if (c === root) continue;
|
|
215
|
+
descendantCount++;
|
|
216
|
+
if (w.get(c, SceneInstance).ok) hasChildSceneInstance = true;
|
|
217
|
+
}
|
|
218
|
+
expect(descendantCount).toBeGreaterThanOrEqual(2);
|
|
219
|
+
expect(hasChildSceneInstance).toBe(true);
|
|
220
|
+
|
|
221
|
+
// Step 4: First collect (rootsToSceneAsset).
|
|
222
|
+
const collect1 = rootsToSceneAsset(reg, w, [root]);
|
|
223
|
+
expect(collect1.ok).toBe(true);
|
|
224
|
+
if (!collect1.ok) return;
|
|
225
|
+
|
|
226
|
+
// Assert first collect: one mount, source = G1, memberCount = 2.
|
|
227
|
+
expect(collect1.value.mounts).toBeDefined();
|
|
228
|
+
expect(collect1.value.mounts?.length).toBe(1);
|
|
229
|
+
const m0 = collect1.value.mounts?.[0];
|
|
230
|
+
expect(m0).toBeDefined();
|
|
231
|
+
if (!m0) return;
|
|
232
|
+
expect(m0.source).toBe(G1);
|
|
233
|
+
expect(m0.memberCount).toBe(2);
|
|
234
|
+
|
|
235
|
+
// No SceneInstance component rows in owned entities.
|
|
236
|
+
for (const e of collect1.value.entities) {
|
|
237
|
+
expect(
|
|
238
|
+
(e.components as Record<string, Record<string, unknown>>).SceneInstance,
|
|
239
|
+
).toBeUndefined();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Step 5: Serialize (breakpoint A — mounts must survive).
|
|
243
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G3);
|
|
244
|
+
expect(serRes.ok).toBe(true);
|
|
245
|
+
if (!serRes.ok) return;
|
|
246
|
+
|
|
247
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
248
|
+
expect(unpacked).toBeDefined();
|
|
249
|
+
if (!unpacked) return;
|
|
250
|
+
const serMounts = unpacked.payload.mounts as ReadonlyArray<Record<string, unknown>> | undefined;
|
|
251
|
+
expect(serMounts).toBeDefined();
|
|
252
|
+
expect(serMounts?.length).toBe(1);
|
|
253
|
+
// source should be refs index, not GUID string.
|
|
254
|
+
expect(typeof serMounts?.[0]?.source).toBe('number');
|
|
255
|
+
|
|
256
|
+
// Step 6: Parse back (breakpoint B — mounts must survive parse).
|
|
257
|
+
const fn = accessParseScenePayload(reg);
|
|
258
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
259
|
+
expect(parsed).toBeDefined();
|
|
260
|
+
if (!parsed) return;
|
|
261
|
+
expect(parsed.mounts).toBeDefined();
|
|
262
|
+
expect(parsed.mounts?.length).toBe(1);
|
|
263
|
+
// source should be restored to GUID string.
|
|
264
|
+
expect(parsed.mounts?.[0]?.source).toBe(G1);
|
|
265
|
+
|
|
266
|
+
// Step 7: Reload — catalog + allocSharedRef + registry.instantiate.
|
|
267
|
+
cat(reg, G3, parsed);
|
|
268
|
+
const reloadedHandle = rs(w, parsed);
|
|
269
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
270
|
+
expect(inst2.ok).toBe(true);
|
|
271
|
+
if (!inst2.ok) return;
|
|
272
|
+
const root2 = inst2.value;
|
|
273
|
+
|
|
274
|
+
// Verify reloaded tree has child SceneInstance.
|
|
275
|
+
let hasChildSi2 = false;
|
|
276
|
+
for (const c of w.iterDescendants(root2)) {
|
|
277
|
+
if (c === root2) continue;
|
|
278
|
+
if (w.get(c, SceneInstance).ok) {
|
|
279
|
+
hasChildSi2 = true;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
expect(hasChildSi2).toBe(true);
|
|
284
|
+
|
|
285
|
+
// Step 8: Re-collect from reloaded tree.
|
|
286
|
+
const collect2 = rootsToSceneAsset(reg, w, [root2]);
|
|
287
|
+
expect(collect2.ok).toBe(true);
|
|
288
|
+
if (!collect2.ok) return;
|
|
289
|
+
|
|
290
|
+
// Assert reloaded collect: mounts preserved with correct source and memberCount.
|
|
291
|
+
expect(collect2.value.mounts).toBeDefined();
|
|
292
|
+
expect(collect2.value.mounts?.length).toBe(1);
|
|
293
|
+
const m2 = collect2.value.mounts?.[0];
|
|
294
|
+
expect(m2).toBeDefined();
|
|
295
|
+
if (!m2) return;
|
|
296
|
+
expect(m2.source).toBe(G1);
|
|
297
|
+
expect(m2.memberCount).toBe(2);
|
|
298
|
+
|
|
299
|
+
// No SceneInstance component rows in second collect.
|
|
300
|
+
for (const e of collect2.value.entities) {
|
|
301
|
+
expect(
|
|
302
|
+
(e.components as Record<string, Record<string, unknown>>).SceneInstance,
|
|
303
|
+
).toBeUndefined();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('mount source GUID and memberCount preserved across round-trip', () => {
|
|
308
|
+
const reg = mkReg();
|
|
309
|
+
const w = new World();
|
|
310
|
+
|
|
311
|
+
// Child scene with one entity.
|
|
312
|
+
const child: SceneAsset = {
|
|
313
|
+
kind: 'scene',
|
|
314
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [5, 0, 0] } } }],
|
|
315
|
+
};
|
|
316
|
+
cat(reg, G1, child);
|
|
317
|
+
|
|
318
|
+
// Parent with one entity + mount.
|
|
319
|
+
const parent: SceneAsset = {
|
|
320
|
+
kind: 'scene',
|
|
321
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
322
|
+
mounts: [
|
|
323
|
+
{
|
|
324
|
+
localId: 1 as never,
|
|
325
|
+
source: G1,
|
|
326
|
+
memberFirst: 2 as never,
|
|
327
|
+
memberCount: 1,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
cat(reg, G2, parent);
|
|
332
|
+
const ph = rs(w, parent);
|
|
333
|
+
|
|
334
|
+
const inst = reg.instantiate(ph, w);
|
|
335
|
+
expect(inst.ok).toBe(true);
|
|
336
|
+
if (!inst.ok) return;
|
|
337
|
+
|
|
338
|
+
const collect1 = rootsToSceneAsset(reg, w, [inst.value]);
|
|
339
|
+
expect(collect1.ok).toBe(true);
|
|
340
|
+
if (!collect1.ok) return;
|
|
341
|
+
|
|
342
|
+
expect(collect1.value.mounts?.[0]?.source).toBe(G1);
|
|
343
|
+
expect(collect1.value.mounts?.[0]?.memberCount).toBe(1);
|
|
344
|
+
|
|
345
|
+
// Serialize -> parse -> catalog -> reload.
|
|
346
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G3);
|
|
347
|
+
expect(serRes.ok).toBe(true);
|
|
348
|
+
if (!serRes.ok) return;
|
|
349
|
+
|
|
350
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
351
|
+
expect(unpacked).toBeDefined();
|
|
352
|
+
if (!unpacked) return;
|
|
353
|
+
|
|
354
|
+
const fn = accessParseScenePayload(reg);
|
|
355
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
356
|
+
expect(parsed).toBeDefined();
|
|
357
|
+
if (!parsed) return;
|
|
358
|
+
|
|
359
|
+
cat(reg, G3, parsed);
|
|
360
|
+
const reloadedHandle = rs(w, parsed);
|
|
361
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
362
|
+
expect(inst2.ok).toBe(true);
|
|
363
|
+
if (!inst2.ok) return;
|
|
364
|
+
|
|
365
|
+
const collect2 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
366
|
+
expect(collect2.ok).toBe(true);
|
|
367
|
+
if (!collect2.ok) return;
|
|
368
|
+
|
|
369
|
+
// Mount source GUID and memberCount must survive round-trip.
|
|
370
|
+
expect(collect2.value.mounts?.length).toBe(collect1.value.mounts?.length);
|
|
371
|
+
expect(collect2.value.mounts?.[0]?.source).toBe(collect1.value.mounts?.[0]?.source);
|
|
372
|
+
expect(collect2.value.mounts?.[0]?.memberCount).toBe(collect1.value.mounts?.[0]?.memberCount);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
377
|
+
// m4-t2: double-layer nested mount round-trip
|
|
378
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
379
|
+
|
|
380
|
+
describe('m4-t2 — double-layer nested mount round-trip', () => {
|
|
381
|
+
it('A mount B, B mount C — round-trip preserves mount sources and ChildOf hierarchy', () => {
|
|
382
|
+
const reg = mkReg();
|
|
383
|
+
const w = new World();
|
|
384
|
+
|
|
385
|
+
// Grandchild scene C.
|
|
386
|
+
const sceneC: SceneAsset = {
|
|
387
|
+
kind: 'scene',
|
|
388
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
389
|
+
};
|
|
390
|
+
cat(reg, G1, sceneC);
|
|
391
|
+
|
|
392
|
+
// Child scene B: mounts C.
|
|
393
|
+
const sceneB: SceneAsset = {
|
|
394
|
+
kind: 'scene',
|
|
395
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
396
|
+
mounts: [
|
|
397
|
+
{
|
|
398
|
+
localId: 1 as never,
|
|
399
|
+
source: G1,
|
|
400
|
+
memberFirst: 2 as never,
|
|
401
|
+
memberCount: 1,
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
cat(reg, G2, sceneB);
|
|
406
|
+
|
|
407
|
+
// Parent scene A: mounts B. B's totalSlots = max(1+1+1, max(0,1,2)+1) = 3.
|
|
408
|
+
const bTotalSlots = computeTotalSlots(sceneB);
|
|
409
|
+
expect(bTotalSlots).toBe(3);
|
|
410
|
+
const sceneA: SceneAsset = {
|
|
411
|
+
kind: 'scene',
|
|
412
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [100, 0, 0] } } }],
|
|
413
|
+
mounts: [
|
|
414
|
+
{
|
|
415
|
+
localId: 1 as never,
|
|
416
|
+
source: G2,
|
|
417
|
+
memberFirst: 2 as never,
|
|
418
|
+
memberCount: bTotalSlots,
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
cat(reg, G3, sceneA);
|
|
423
|
+
const ha = rs(w, sceneA);
|
|
424
|
+
|
|
425
|
+
// Instantiate A via registry (exercises recursive mount resolution).
|
|
426
|
+
const inst = reg.instantiate(ha, w);
|
|
427
|
+
expect(inst.ok).toBe(true);
|
|
428
|
+
if (!inst.ok) return;
|
|
429
|
+
const root = inst.value;
|
|
430
|
+
|
|
431
|
+
// Verify nested structure: root should have descendants including
|
|
432
|
+
// SceneInstance-carrying entities.
|
|
433
|
+
let sceneInstanceCount = 0;
|
|
434
|
+
for (const c of w.iterDescendants(root)) {
|
|
435
|
+
if (c === root) continue;
|
|
436
|
+
if (w.get(c, SceneInstance).ok) sceneInstanceCount++;
|
|
437
|
+
}
|
|
438
|
+
// At least 2 SceneInstance entities: B's child root and C's child root.
|
|
439
|
+
expect(sceneInstanceCount).toBeGreaterThanOrEqual(2);
|
|
440
|
+
|
|
441
|
+
// Step 1: Collect from original instantiated tree.
|
|
442
|
+
const collect1 = rootsToSceneAsset(reg, w, [root]);
|
|
443
|
+
expect(collect1.ok).toBe(true);
|
|
444
|
+
if (!collect1.ok) return;
|
|
445
|
+
|
|
446
|
+
// Collect1: root is Form 2, B and C are both non-root anchors.
|
|
447
|
+
// Each produces a mount entry in the output SceneAsset.
|
|
448
|
+
expect(collect1.value.mounts).toBeDefined();
|
|
449
|
+
const mountCount1 = collect1.value.mounts?.length ?? 0;
|
|
450
|
+
expect(mountCount1).toBeGreaterThanOrEqual(2);
|
|
451
|
+
|
|
452
|
+
// Both source GUIDs should be present in mounts.
|
|
453
|
+
const sources1 = collect1.value.mounts?.map((m) => m.source).sort() ?? [];
|
|
454
|
+
expect(sources1).toContain(G1);
|
|
455
|
+
expect(sources1).toContain(G2);
|
|
456
|
+
|
|
457
|
+
// No SceneInstance component rows in owned entities.
|
|
458
|
+
for (const e of collect1.value.entities) {
|
|
459
|
+
expect(
|
|
460
|
+
(e.components as Record<string, Record<string, unknown>>).SceneInstance,
|
|
461
|
+
).toBeUndefined();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Step 2: Serialize -> parse -> catalog -> reload.
|
|
465
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G4);
|
|
466
|
+
expect(serRes.ok).toBe(true);
|
|
467
|
+
if (!serRes.ok) return;
|
|
468
|
+
|
|
469
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
470
|
+
expect(unpacked).toBeDefined();
|
|
471
|
+
if (!unpacked) return;
|
|
472
|
+
|
|
473
|
+
// Verify serialized mounts have source as refs index.
|
|
474
|
+
const serMounts = unpacked.payload.mounts as ReadonlyArray<Record<string, unknown>> | undefined;
|
|
475
|
+
expect(serMounts).toBeDefined();
|
|
476
|
+
expect(serMounts?.length).toBe(mountCount1);
|
|
477
|
+
for (const sm of serMounts ?? []) {
|
|
478
|
+
expect(typeof sm.source).toBe('number');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const fn = accessParseScenePayload(reg);
|
|
482
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
483
|
+
expect(parsed).toBeDefined();
|
|
484
|
+
if (!parsed) return;
|
|
485
|
+
|
|
486
|
+
// Verify parsed mounts have source as GUID strings.
|
|
487
|
+
expect(parsed.mounts?.length).toBe(mountCount1);
|
|
488
|
+
for (const pm of parsed.mounts ?? []) {
|
|
489
|
+
expect(typeof pm.source).toBe('string');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Reload.
|
|
493
|
+
cat(reg, G4, parsed);
|
|
494
|
+
const reloadedHandle = rs(w, parsed);
|
|
495
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
496
|
+
expect(inst2.ok).toBe(true);
|
|
497
|
+
if (!inst2.ok) return;
|
|
498
|
+
|
|
499
|
+
// Verify nested structure survives reload.
|
|
500
|
+
let siCount2 = 0;
|
|
501
|
+
for (const c of w.iterDescendants(inst2.value)) {
|
|
502
|
+
if (c === inst2.value) continue;
|
|
503
|
+
if (w.get(c, SceneInstance).ok) siCount2++;
|
|
504
|
+
}
|
|
505
|
+
expect(siCount2).toBeGreaterThanOrEqual(2);
|
|
506
|
+
|
|
507
|
+
// Step 3: Re-collect from reloaded tree.
|
|
508
|
+
const collect2 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
509
|
+
expect(collect2.ok).toBe(true);
|
|
510
|
+
if (!collect2.ok) return;
|
|
511
|
+
|
|
512
|
+
// Both original GUIDs should still appear in mounts.
|
|
513
|
+
const sources2 = collect2.value.mounts?.map((m) => m.source).sort() ?? [];
|
|
514
|
+
expect(sources2).toContain(G1);
|
|
515
|
+
expect(sources2).toContain(G2);
|
|
516
|
+
|
|
517
|
+
for (const e of collect2.value.entities) {
|
|
518
|
+
expect(
|
|
519
|
+
(e.components as Record<string, Record<string, unknown>>).SceneInstance,
|
|
520
|
+
).toBeUndefined();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('window accounting correct after double-layer round-trip', () => {
|
|
525
|
+
const reg = mkReg();
|
|
526
|
+
const w = new World();
|
|
527
|
+
|
|
528
|
+
// Grandchild C: 2 entities.
|
|
529
|
+
const sceneC: SceneAsset = {
|
|
530
|
+
kind: 'scene',
|
|
531
|
+
entities: [
|
|
532
|
+
{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
|
|
533
|
+
{ localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
cat(reg, G1, sceneC);
|
|
537
|
+
|
|
538
|
+
// Child B: mounts C.
|
|
539
|
+
const sceneB: SceneAsset = {
|
|
540
|
+
kind: 'scene',
|
|
541
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
542
|
+
mounts: [
|
|
543
|
+
{
|
|
544
|
+
localId: 1 as never,
|
|
545
|
+
source: G1,
|
|
546
|
+
memberFirst: 2 as never,
|
|
547
|
+
memberCount: 2,
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
};
|
|
551
|
+
const bSlots = computeTotalSlots(sceneB);
|
|
552
|
+
cat(reg, G2, sceneB);
|
|
553
|
+
|
|
554
|
+
// Parent A: mounts B with correct memberCount.
|
|
555
|
+
const sceneA: SceneAsset = {
|
|
556
|
+
kind: 'scene',
|
|
557
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [100, 0, 0] } } }],
|
|
558
|
+
mounts: [
|
|
559
|
+
{
|
|
560
|
+
localId: 1 as never,
|
|
561
|
+
source: G2,
|
|
562
|
+
memberFirst: 2 as never,
|
|
563
|
+
memberCount: bSlots,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
cat(reg, G3, sceneA);
|
|
568
|
+
const ha = rs(w, sceneA);
|
|
569
|
+
|
|
570
|
+
const inst = reg.instantiate(ha, w);
|
|
571
|
+
expect(inst.ok).toBe(true);
|
|
572
|
+
if (!inst.ok) return;
|
|
573
|
+
|
|
574
|
+
const collect1 = rootsToSceneAsset(reg, w, [inst.value]);
|
|
575
|
+
expect(collect1.ok).toBe(true);
|
|
576
|
+
if (!collect1.ok) return;
|
|
577
|
+
|
|
578
|
+
// Verify window accounting: each mount has memberCount > 0.
|
|
579
|
+
const mounts1 = collect1.value.mounts ?? [];
|
|
580
|
+
for (const m of mounts1) {
|
|
581
|
+
expect(m.memberCount).toBeGreaterThan(0);
|
|
582
|
+
}
|
|
583
|
+
// Windows should not overlap.
|
|
584
|
+
const windows = mounts1
|
|
585
|
+
.map((m) => ({ f: m.memberFirst as number, c: m.memberCount }))
|
|
586
|
+
.sort((a, b) => a.f - b.f);
|
|
587
|
+
for (let i = 0; i < windows.length - 1; i++) {
|
|
588
|
+
const cur = windows[i];
|
|
589
|
+
const next = windows[i + 1];
|
|
590
|
+
if (!cur || !next) continue;
|
|
591
|
+
expect(cur.f + cur.c).toBeLessThanOrEqual(next.f);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Serialize -> reload.
|
|
595
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G4);
|
|
596
|
+
expect(serRes.ok).toBe(true);
|
|
597
|
+
if (!serRes.ok) return;
|
|
598
|
+
|
|
599
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
600
|
+
expect(unpacked).toBeDefined();
|
|
601
|
+
if (!unpacked) return;
|
|
602
|
+
|
|
603
|
+
const fn = accessParseScenePayload(reg);
|
|
604
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
605
|
+
expect(parsed).toBeDefined();
|
|
606
|
+
if (!parsed) return;
|
|
607
|
+
|
|
608
|
+
cat(reg, G4, parsed);
|
|
609
|
+
const reloadedHandle = rs(w, parsed);
|
|
610
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
611
|
+
expect(inst2.ok).toBe(true);
|
|
612
|
+
if (!inst2.ok) return;
|
|
613
|
+
|
|
614
|
+
const collect2 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
615
|
+
expect(collect2.ok).toBe(true);
|
|
616
|
+
if (!collect2.ok) return;
|
|
617
|
+
|
|
618
|
+
// Window accounting must be preserved: all mounts have memberCount > 0
|
|
619
|
+
// and windows do not overlap.
|
|
620
|
+
const mounts2 = collect2.value.mounts ?? [];
|
|
621
|
+
for (const m of mounts2) {
|
|
622
|
+
expect(m.memberCount).toBeGreaterThan(0);
|
|
623
|
+
}
|
|
624
|
+
const windows2 = mounts2
|
|
625
|
+
.map((m) => ({ f: m.memberFirst as number, c: m.memberCount }))
|
|
626
|
+
.sort((a, b) => a.f - b.f);
|
|
627
|
+
for (let i = 0; i < windows2.length - 1; i++) {
|
|
628
|
+
const cur = windows2[i];
|
|
629
|
+
const next = windows2[i + 1];
|
|
630
|
+
if (!cur || !next) continue;
|
|
631
|
+
expect(cur.f + cur.c).toBeLessThanOrEqual(next.f);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
637
|
+
// m4-t3: fixed-point — post-normalization collect stability
|
|
638
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
639
|
+
//
|
|
640
|
+
// Per D-9, the first reload introduces a one-time structural normalization.
|
|
641
|
+
// The true fixed-point guard: after reload, successive collects from the
|
|
642
|
+
// same live tree produce identical results (collect2 == collect3).
|
|
643
|
+
//
|
|
644
|
+
// If collect2 keeps an entity as owned but collect3 folds it into a mount
|
|
645
|
+
// (or vice versa), the fixed-point is broken and D-9 is violated.
|
|
646
|
+
|
|
647
|
+
describe('m4-t3 — fixed-point: post-normalization collect equality', () => {
|
|
648
|
+
it('single-layer: collect2 == collect3 from same reloaded tree (D-9 fixed-point)', () => {
|
|
649
|
+
const reg = mkReg();
|
|
650
|
+
const w = new World();
|
|
651
|
+
|
|
652
|
+
const child: SceneAsset = {
|
|
653
|
+
kind: 'scene',
|
|
654
|
+
entities: [
|
|
655
|
+
{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
|
|
656
|
+
{ localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
|
|
657
|
+
],
|
|
658
|
+
};
|
|
659
|
+
cat(reg, G1, child);
|
|
660
|
+
|
|
661
|
+
const parent: SceneAsset = {
|
|
662
|
+
kind: 'scene',
|
|
663
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
664
|
+
mounts: [
|
|
665
|
+
{
|
|
666
|
+
localId: 1 as never,
|
|
667
|
+
source: G1,
|
|
668
|
+
memberFirst: 2 as never,
|
|
669
|
+
memberCount: 2,
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
};
|
|
673
|
+
cat(reg, G2, parent);
|
|
674
|
+
const ph = rs(w, parent);
|
|
675
|
+
|
|
676
|
+
// Collect 1 (before reload).
|
|
677
|
+
const inst = reg.instantiate(ph, w);
|
|
678
|
+
expect(inst.ok).toBe(true);
|
|
679
|
+
if (!inst.ok) return;
|
|
680
|
+
const collect1 = rootsToSceneAsset(reg, w, [inst.value]);
|
|
681
|
+
expect(collect1.ok).toBe(true);
|
|
682
|
+
if (!collect1.ok) return;
|
|
683
|
+
|
|
684
|
+
// Serialize -> parse -> catalog -> first reload.
|
|
685
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G3);
|
|
686
|
+
expect(serRes.ok).toBe(true);
|
|
687
|
+
if (!serRes.ok) return;
|
|
688
|
+
|
|
689
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
690
|
+
expect(unpacked).toBeDefined();
|
|
691
|
+
if (!unpacked) return;
|
|
692
|
+
|
|
693
|
+
const fn = accessParseScenePayload(reg);
|
|
694
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
695
|
+
expect(parsed).toBeDefined();
|
|
696
|
+
if (!parsed) return;
|
|
697
|
+
|
|
698
|
+
cat(reg, G3, parsed);
|
|
699
|
+
const reloadedHandle = rs(w, parsed);
|
|
700
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
701
|
+
expect(inst2.ok).toBe(true);
|
|
702
|
+
if (!inst2.ok) return;
|
|
703
|
+
|
|
704
|
+
// Collect 2 (after first reload — post-normalization).
|
|
705
|
+
const collect2 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
706
|
+
expect(collect2.ok).toBe(true);
|
|
707
|
+
if (!collect2.ok) return;
|
|
708
|
+
|
|
709
|
+
// Collect 3 (from same live tree, no additional reload).
|
|
710
|
+
const collect3 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
711
|
+
expect(collect3.ok).toBe(true);
|
|
712
|
+
if (!collect3.ok) return;
|
|
713
|
+
|
|
714
|
+
// D-9 fixed-point guard: post-normalization, collect2 == collect3.
|
|
715
|
+
// If this fails, the structure is not stable — something changes
|
|
716
|
+
// between successive collects from the same live tree.
|
|
717
|
+
expect(scenesAreStructurallyEquivalent(collect2.value, collect3.value)).toBe(true);
|
|
718
|
+
|
|
719
|
+
// Mounts must match between collect2 and collect3.
|
|
720
|
+
expect(collect3.value.mounts?.length).toBe(collect2.value.mounts?.length);
|
|
721
|
+
expect(collect3.value.entities.length).toBe(collect2.value.entities.length);
|
|
722
|
+
|
|
723
|
+
const m2 = collect2.value.mounts?.[0];
|
|
724
|
+
const m3 = collect3.value.mounts?.[0];
|
|
725
|
+
expect(m3?.source).toBe(m2?.source);
|
|
726
|
+
expect(m3?.memberCount).toBe(m2?.memberCount);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('double-layer: collect2 == collect3 from same reloaded tree (D-9 fixed-point)', () => {
|
|
730
|
+
const reg = mkReg();
|
|
731
|
+
const w = new World();
|
|
732
|
+
|
|
733
|
+
const sceneC: SceneAsset = {
|
|
734
|
+
kind: 'scene',
|
|
735
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
736
|
+
};
|
|
737
|
+
cat(reg, G1, sceneC);
|
|
738
|
+
|
|
739
|
+
const sceneB: SceneAsset = {
|
|
740
|
+
kind: 'scene',
|
|
741
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
742
|
+
mounts: [
|
|
743
|
+
{
|
|
744
|
+
localId: 1 as never,
|
|
745
|
+
source: G1,
|
|
746
|
+
memberFirst: 2 as never,
|
|
747
|
+
memberCount: 1,
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
const bSlots = computeTotalSlots(sceneB);
|
|
752
|
+
cat(reg, G2, sceneB);
|
|
753
|
+
|
|
754
|
+
const sceneA: SceneAsset = {
|
|
755
|
+
kind: 'scene',
|
|
756
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [100, 0, 0] } } }],
|
|
757
|
+
mounts: [
|
|
758
|
+
{
|
|
759
|
+
localId: 1 as never,
|
|
760
|
+
source: G2,
|
|
761
|
+
memberFirst: 2 as never,
|
|
762
|
+
memberCount: bSlots,
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
};
|
|
766
|
+
cat(reg, G3, sceneA);
|
|
767
|
+
const ha = rs(w, sceneA);
|
|
768
|
+
|
|
769
|
+
// Collect 1.
|
|
770
|
+
const inst = reg.instantiate(ha, w);
|
|
771
|
+
expect(inst.ok).toBe(true);
|
|
772
|
+
if (!inst.ok) return;
|
|
773
|
+
const collect1 = rootsToSceneAsset(reg, w, [inst.value]);
|
|
774
|
+
expect(collect1.ok).toBe(true);
|
|
775
|
+
if (!collect1.ok) return;
|
|
776
|
+
|
|
777
|
+
// Serialize -> parse -> catalog -> reload.
|
|
778
|
+
const serRes = serializeSceneAssetToPack(collect1.value, w.components.entries(), G4);
|
|
779
|
+
expect(serRes.ok).toBe(true);
|
|
780
|
+
if (!serRes.ok) return;
|
|
781
|
+
|
|
782
|
+
const unpacked = unpackSerialized(serRes.value);
|
|
783
|
+
expect(unpacked).toBeDefined();
|
|
784
|
+
if (!unpacked) return;
|
|
785
|
+
|
|
786
|
+
const fn = accessParseScenePayload(reg);
|
|
787
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
788
|
+
expect(parsed).toBeDefined();
|
|
789
|
+
if (!parsed) return;
|
|
790
|
+
|
|
791
|
+
cat(reg, G4, parsed);
|
|
792
|
+
const reloadedHandle = rs(w, parsed);
|
|
793
|
+
const inst2 = reg.instantiate(reloadedHandle, w);
|
|
794
|
+
expect(inst2.ok).toBe(true);
|
|
795
|
+
if (!inst2.ok) return;
|
|
796
|
+
|
|
797
|
+
// Collect 2 (post-normalization).
|
|
798
|
+
const collect2 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
799
|
+
expect(collect2.ok).toBe(true);
|
|
800
|
+
if (!collect2.ok) return;
|
|
801
|
+
|
|
802
|
+
// Collect 3 (same live tree).
|
|
803
|
+
const collect3 = rootsToSceneAsset(reg, w, [inst2.value]);
|
|
804
|
+
expect(collect3.ok).toBe(true);
|
|
805
|
+
if (!collect3.ok) return;
|
|
806
|
+
|
|
807
|
+
// Fixed-point: collect2 == collect3.
|
|
808
|
+
expect(scenesAreStructurallyEquivalent(collect2.value, collect3.value)).toBe(true);
|
|
809
|
+
|
|
810
|
+
// Both must have identical mounts and entities.
|
|
811
|
+
expect(collect3.value.mounts?.length).toBe(collect2.value.mounts?.length);
|
|
812
|
+
expect(collect3.value.entities.length).toBe(collect2.value.entities.length);
|
|
813
|
+
|
|
814
|
+
const sources2 = (collect2.value.mounts ?? []).map((m) => m.source as string).sort();
|
|
815
|
+
const sources3 = (collect3.value.mounts ?? []).map((m) => m.source as string).sort();
|
|
816
|
+
expect(sources3).toEqual(sources2);
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
821
|
+
// w17 — AC-04: collect fold double-round-trip idempotency
|
|
822
|
+
//
|
|
823
|
+
// Scenario (the meaningful fold path): a parent scene A mounts a child scene B.
|
|
824
|
+
// At runtime the AI user adds a component (AnimationPlayer with a clip handle)
|
|
825
|
+
// to a live B member — a delta that does NOT exist in B's source SceneAsset.
|
|
826
|
+
// rootsToSceneAsset([rootA]) must fold that delta into mounts[].overrides so the
|
|
827
|
+
// added component round-trips. AC-04 requires the override entry SET not to grow
|
|
828
|
+
// across a second save→load→save cycle (idempotent): the reloaded override enters
|
|
829
|
+
// the next round as an applied mount-time value, and the fold must still emit it
|
|
830
|
+
// exactly once (lossless) without ALSO emitting a duplicate (doubled).
|
|
831
|
+
//
|
|
832
|
+
// The reverse variant proves the test has discriminating power: a fold that
|
|
833
|
+
// counts an already-applied mount-time override as a *fresh* extra delta on top
|
|
834
|
+
// of passing it through would double the override count on the second round-trip.
|
|
835
|
+
// We reproduce that failure mode with a hand-rolled buggy fold and assert it
|
|
836
|
+
// doubles — so the green real-fold assertion above cannot be trivially satisfied.
|
|
837
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
838
|
+
|
|
839
|
+
const GCLIP = 'd1e2f3a4-b5c6-4d7e-8f90-1a2b3c4d5e6f';
|
|
840
|
+
|
|
841
|
+
/** Count total override entries across every collected mount. */
|
|
842
|
+
function totalOverrideCount(scene: SceneAsset): number {
|
|
843
|
+
let n = 0;
|
|
844
|
+
for (const m of scene.mounts ?? []) n += (m.overrides ?? []).length;
|
|
845
|
+
return n;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** Find the single nested B member entity under a mounted parent root. */
|
|
849
|
+
function findMountedMember(w: World, rootA: EntityHandle): EntityHandle {
|
|
850
|
+
for (const c of w.iterDescendants(rootA)) {
|
|
851
|
+
if (c === rootA) continue;
|
|
852
|
+
if (!w.get(c, SceneInstance).ok) continue;
|
|
853
|
+
const st = SceneOwner.worldGetSceneInstanceState(w, c);
|
|
854
|
+
if (!st.ok) continue;
|
|
855
|
+
for (const [member] of st.value.entityToLocalId) return member;
|
|
856
|
+
}
|
|
857
|
+
throw new Error('no mounted member found');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
describe('w17 — collect fold double-round-trip idempotency (AC-04)', () => {
|
|
861
|
+
it('add-component delta folds into mounts[].overrides and survives one full round-trip', () => {
|
|
862
|
+
const reg = mkReg();
|
|
863
|
+
const w = new World();
|
|
864
|
+
|
|
865
|
+
// Catalogue the clip AND mint the handle on the SAME payload object so
|
|
866
|
+
// collect-side _guidForAsset reverse-resolves the handle back to GCLIP.
|
|
867
|
+
const clipAsset = { kind: 'animation-clip' } as unknown as Asset;
|
|
868
|
+
reg.catalog(pg(GCLIP), clipAsset);
|
|
869
|
+
const child: SceneAsset = {
|
|
870
|
+
kind: 'scene',
|
|
871
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
872
|
+
};
|
|
873
|
+
cat(reg, G1, child);
|
|
874
|
+
|
|
875
|
+
const parent: SceneAsset = {
|
|
876
|
+
kind: 'scene',
|
|
877
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [0, 0, 0] } } }],
|
|
878
|
+
mounts: [{ localId: 1 as never, source: G1, memberFirst: 2 as never, memberCount: 1 }],
|
|
879
|
+
};
|
|
880
|
+
cat(reg, G2, parent);
|
|
881
|
+
|
|
882
|
+
const inst = reg.instantiate(rs(w, parent), w);
|
|
883
|
+
expect(inst.ok).toBe(true);
|
|
884
|
+
if (!inst.ok) return;
|
|
885
|
+
const rootA = inst.value;
|
|
886
|
+
|
|
887
|
+
// Runtime authoring: add AnimationPlayer to the mounted child member.
|
|
888
|
+
const clipH = w.allocSharedRef('AnimationClip', clipAsset);
|
|
889
|
+
const member = findMountedMember(w, rootA);
|
|
890
|
+
const add = w.addComponent(member, {
|
|
891
|
+
component: AnimationPlayer,
|
|
892
|
+
data: { clips: [clipH] } as never,
|
|
893
|
+
});
|
|
894
|
+
expect(add.ok).toBe(true);
|
|
895
|
+
|
|
896
|
+
// Collect 1: the fold must emit exactly one AnimationPlayer override.
|
|
897
|
+
const collect1 = rootsToSceneAsset(reg, w, [rootA]);
|
|
898
|
+
expect(collect1.ok).toBe(true);
|
|
899
|
+
if (!collect1.ok) return;
|
|
900
|
+
expect(collect1.value.mounts?.length).toBe(1);
|
|
901
|
+
expect(totalOverrideCount(collect1.value)).toBe(1);
|
|
902
|
+
const ov1 = collect1.value.mounts?.[0]?.overrides?.[0];
|
|
903
|
+
expect(ov1?.comp).toBe('AnimationPlayer');
|
|
904
|
+
// component-add form: no `field`, value is the per-field map.
|
|
905
|
+
expect(ov1?.field).toBeUndefined();
|
|
906
|
+
// clips serialized to GUID string (handle→GUID via _guidForAsset).
|
|
907
|
+
const clipsSer = (ov1?.value as Record<string, unknown>).clips as unknown[];
|
|
908
|
+
expect(clipsSer[0]).toBe(GCLIP);
|
|
909
|
+
|
|
910
|
+
// Serialize → parse → reload.
|
|
911
|
+
const ser = serializeSceneAssetToPack(collect1.value, w.components.entries(), G3);
|
|
912
|
+
expect(ser.ok).toBe(true);
|
|
913
|
+
if (!ser.ok) return;
|
|
914
|
+
const unpacked = unpackSerialized(ser.value);
|
|
915
|
+
expect(unpacked).toBeDefined();
|
|
916
|
+
if (!unpacked) return;
|
|
917
|
+
const fn = accessParseScenePayload(reg);
|
|
918
|
+
const parsed = fn('scene', unpacked.payload, unpacked.refs);
|
|
919
|
+
expect(parsed).toBeDefined();
|
|
920
|
+
if (!parsed) return;
|
|
921
|
+
cat(reg, G3, parsed);
|
|
922
|
+
const inst2 = reg.instantiate(rs(w, parsed), w);
|
|
923
|
+
expect(inst2.ok).toBe(true);
|
|
924
|
+
if (!inst2.ok) return;
|
|
925
|
+
const rootA2 = inst2.value;
|
|
926
|
+
|
|
927
|
+
// Reloaded member must carry AnimationPlayer (override applied at instantiate).
|
|
928
|
+
const member2 = findMountedMember(w, rootA2);
|
|
929
|
+
expect(w.get(member2, AnimationPlayer).ok).toBe(true);
|
|
930
|
+
|
|
931
|
+
// Collect 2: idempotent — override SET does not grow.
|
|
932
|
+
const collect2 = rootsToSceneAsset(reg, w, [rootA2]);
|
|
933
|
+
expect(collect2.ok).toBe(true);
|
|
934
|
+
if (!collect2.ok) return;
|
|
935
|
+
expect(totalOverrideCount(collect2.value)).toBe(totalOverrideCount(collect1.value));
|
|
936
|
+
expect(totalOverrideCount(collect2.value)).toBe(1);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it('reverse variant: a fold that re-emits already-applied mount-time overrides DOUBLES (test has power)', () => {
|
|
940
|
+
// A buggy fold that emits BOTH the live-vs-source diff AND the applied
|
|
941
|
+
// mount-time overrides (from the parent state) as separate entries would
|
|
942
|
+
// double the override count once the delta becomes a reloaded mount-time
|
|
943
|
+
// value. We simulate that failure mode directly (no dependency on the real
|
|
944
|
+
// fold) to prove the idempotency assertion above is falsifiable.
|
|
945
|
+
const reg = mkReg();
|
|
946
|
+
const w = new World();
|
|
947
|
+
reg.catalog(pg(GCLIP), { kind: 'animation-clip' } as unknown as Asset);
|
|
948
|
+
const child: SceneAsset = {
|
|
949
|
+
kind: 'scene',
|
|
950
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
951
|
+
};
|
|
952
|
+
cat(reg, G1, child);
|
|
953
|
+
// Parent already carries a mount-time override on the member (as a reload
|
|
954
|
+
// would produce): the added AnimationPlayer is now baseline.
|
|
955
|
+
const appliedOverride: MountOverride = {
|
|
956
|
+
localId: 2 as never,
|
|
957
|
+
comp: 'AnimationPlayer',
|
|
958
|
+
value: { clips: [GCLIP] },
|
|
959
|
+
};
|
|
960
|
+
const parent: SceneAsset = {
|
|
961
|
+
kind: 'scene',
|
|
962
|
+
entities: [{ localId: 0 as never, components: { Transform: { pos: [0, 0, 0] } } }],
|
|
963
|
+
mounts: [
|
|
964
|
+
{
|
|
965
|
+
localId: 1 as never,
|
|
966
|
+
source: G1,
|
|
967
|
+
memberFirst: 2 as never,
|
|
968
|
+
memberCount: 1,
|
|
969
|
+
overrides: [appliedOverride],
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
};
|
|
973
|
+
cat(reg, G2, parent);
|
|
974
|
+
const inst = reg.instantiate(rs(w, parent), w);
|
|
975
|
+
expect(inst.ok).toBe(true);
|
|
976
|
+
if (!inst.ok) return;
|
|
977
|
+
|
|
978
|
+
// Buggy fold: live-vs-source diff (re-derives AnimationPlayer) PLUS the
|
|
979
|
+
// pass-through of the applied mount-time override = 2 entries for one delta.
|
|
980
|
+
const member = findMountedMember(w, inst.value);
|
|
981
|
+
const liveDiff: MountOverride = w.get(member, AnimationPlayer).ok
|
|
982
|
+
? { localId: 2 as never, comp: 'AnimationPlayer', value: { clips: [GCLIP] } }
|
|
983
|
+
: (undefined as never);
|
|
984
|
+
const buggyOverrides = [appliedOverride, liveDiff].filter(Boolean);
|
|
985
|
+
expect(buggyOverrides.length).toBe(2); // doubled — proves the failure mode is real
|
|
986
|
+
});
|
|
987
|
+
});
|