@forgeax/engine-runtime 0.1.21 → 0.1.24
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 +18 -9
- package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/asset.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bloom-gating.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/bloom-gating.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-pack.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/builtin-pack.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/camera-antialias.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-antialias.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/camera-bloom.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-bloom.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/camera-ortho.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-ortho.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cluster-binner.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/cluster-binner.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-metrics.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-metrics.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/font-asset-load.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/font-asset-load.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-residency.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/gpu-residency.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle-quad.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/handle-quad.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
- package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts +25 -0
- package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts.map +1 -0
- package/dist/__tests__/helpers/standard-material-manifest.d.ts +0 -1
- package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -1
- package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-registry.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/loader-registry.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pass-selector.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/pass-selector.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-register.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-register.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/record-all-topology.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/record-all-topology.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-data.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/render-data.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-query-regression.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/render-query-regression.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts +2 -0
- package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts +2 -0
- package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts +2 -0
- package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skin.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skybox-extract.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skybox-extract.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-component.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skylight-component.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/systems.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/systems.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tonemap.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/tonemap.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/verify-revisions.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/verify-revisions.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts.map +1 -0
- package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts +2 -0
- package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts.map +1 -0
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/dev-import-transport.d.ts.map +1 -1
- package/dist/index.mjs +4 -3
- package/dist/index.mjs.map +1 -1
- package/dist/sprite-param-values.d.ts +1 -1
- package/package.json +30 -30
- package/src/__tests__/advance-animation-player.cases.unit.test.ts +1666 -0
- package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +676 -0
- package/src/__tests__/asset-registry-bootstrap.cases.unit.test.ts +472 -0
- package/src/__tests__/asset-registry-builtin-nineslice.cases.unit.test.ts +481 -0
- package/src/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.ts +568 -0
- package/src/__tests__/asset-registry-guid.cases.unit.test.ts +500 -0
- package/src/__tests__/asset-registry-material-validate.cases.unit.test.ts +756 -0
- package/src/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.ts +591 -0
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +501 -0
- package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
- package/src/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.ts +536 -0
- package/src/__tests__/asset-renderer-assembly.cases.unit.test.ts +496 -0
- package/src/__tests__/asset.cases.unit.test.ts +439 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +6 -2
- package/src/__tests__/bloom-gating.cases.unit.test.ts +428 -0
- package/src/__tests__/builtin-guid-ssot.cases.unit.test.ts +472 -0
- package/src/__tests__/builtin-pack.cases.unit.test.ts +639 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
- package/src/__tests__/camera-antialias.cases.unit.test.ts +372 -0
- package/src/__tests__/camera-bloom.cases.unit.test.ts +396 -0
- package/src/__tests__/camera-clear-schema.cases.unit.test.ts +391 -0
- package/src/__tests__/camera-ortho.cases.unit.test.ts +484 -0
- package/src/__tests__/cluster-binner.cases.unit.test.ts +817 -0
- package/src/__tests__/collect-scene-mount-collapse.test.ts +5 -1
- package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
- package/src/__tests__/components.test-d.ts +9 -6
- package/src/__tests__/components.unit.test.ts +23 -19
- package/src/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.ts +345 -0
- package/src/__tests__/create-renderer-uniform-fallback.cases.unit.test.ts +445 -0
- package/src/__tests__/cubemap-upload.cases.unit.test.ts +593 -0
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +184 -17
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +17 -4
- package/src/__tests__/derive-bgl-integration.test.ts +8 -8
- package/src/__tests__/dev-import-transport.cases.unit.test.ts +503 -0
- package/src/__tests__/engine-metrics.cases.unit.test.ts +371 -0
- package/src/__tests__/equirect-projection-error.cases.unit.test.ts +416 -0
- package/src/__tests__/errors.unit.test.ts +3 -0
- package/src/__tests__/extract-frames-merge.test.ts +3 -0
- package/src/__tests__/font-asset-load.cases.unit.test.ts +510 -0
- package/src/__tests__/fxaa-intermediate-texture.cases.unit.test.ts +690 -0
- package/src/__tests__/fxaa-pipeline.cases.unit.test.ts +546 -0
- package/src/__tests__/gpu-residency.cases.unit.test.ts +479 -0
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +929 -0
- package/src/__tests__/handle-quad.cases.unit.test.ts +491 -0
- package/src/__tests__/hdrp-bgl-slots.cases.unit.test.ts +451 -0
- package/src/__tests__/hdrp-grid-invalid.cases.unit.test.ts +408 -0
- package/src/__tests__/hdrp-index-list-overflow-once.cases.unit.test.ts +405 -0
- package/src/__tests__/hdrp-light-budget.cases.unit.test.ts +483 -0
- package/src/__tests__/hdrp-pipeline-asset-config.cases.unit.test.ts +441 -0
- package/src/__tests__/helpers/register-runtime-components.ts +9 -1
- package/src/__tests__/helpers/register-scene-components.ts +9 -2
- package/src/__tests__/helpers/renderer-device-loss-dawn.ts +69 -0
- package/src/__tests__/helpers/standard-material-manifest.ts +11 -15
- package/src/__tests__/ibl-pipeline-cache.cases.unit.test.ts +415 -0
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +848 -0
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +14 -15
- package/src/__tests__/light-snapshot.test-d.ts +27 -1
- package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
- package/src/__tests__/lights.unit.test.ts +2076 -1
- package/src/__tests__/load-by-guid-hdr.cases.unit.test.ts +582 -0
- package/src/__tests__/load-by-guid-prod-material-parent.cases.unit.test.ts +1514 -0
- package/src/__tests__/load-by-guid-prod.cases.unit.test.ts +557 -0
- package/src/__tests__/loader-name-survival.test.ts +8 -9
- package/src/__tests__/loader-registry.cases.unit.test.ts +767 -0
- package/src/__tests__/m7-hdrp-demo-shape.cases.unit.test.ts +361 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +2 -2
- package/src/__tests__/materials.unit.test.ts +29 -10
- package/src/__tests__/mipmap-formula.cases.unit.test.ts +434 -0
- package/src/__tests__/mipmap-pipeline-cache.cases.unit.test.ts +490 -0
- package/src/__tests__/package-promotion.test.ts +7 -11
- package/src/__tests__/parse-asset-payload-material.cases.unit.test.ts +565 -0
- package/src/__tests__/parse-asset-payload-texture.cases.unit.test.ts +720 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +767 -0
- package/src/__tests__/pass-selector.cases.unit.test.ts +376 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +39 -21
- package/src/__tests__/pipeline-builder.cases.unit.test.ts +624 -0
- package/src/__tests__/pipeline-cache-key-topology.cases.unit.test.ts +807 -0
- package/src/__tests__/pipeline-vertex-stride-branch.cases.unit.test.ts +379 -0
- package/src/__tests__/pipeline.cases.unit.test.ts +370 -0
- package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
- package/src/__tests__/post-process-register.cases.unit.test.ts +405 -0
- package/src/__tests__/propagate-transforms.cases.unit.test.ts +502 -0
- package/src/__tests__/propagate-transforms.dawn.test.ts +34 -18
- package/src/__tests__/record-all-topology.cases.unit.test.ts +608 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +621 -0
- package/src/__tests__/record-strip-index-format.cases.unit.test.ts +419 -0
- package/src/__tests__/record-worldid-isolation.test.ts +16 -32
- package/src/__tests__/register-with-guid-rgba16float.cases.unit.test.ts +469 -0
- package/src/__tests__/render-data.cases.unit.test.ts +553 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +11 -0
- package/src/__tests__/render-query-regression.cases.unit.test.ts +531 -0
- package/src/__tests__/render-system-extract.test.ts +5 -5
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
- package/src/__tests__/render-system-record-submesh.cases.unit.test.ts +740 -0
- package/src/__tests__/render-system-record-warn-once.cases.unit.test.ts +418 -0
- package/src/__tests__/render-system-record.test.ts +15 -33
- package/src/__tests__/renderer-device-loss-multi-world.integration.test.ts +103 -0
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +124 -0
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +50 -0
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +514 -0
- package/src/__tests__/renderer-recovery-consumer.integration.test.ts +96 -0
- package/src/__tests__/renderer-surface.unit.test.ts +0 -2
- package/src/__tests__/renderstate-pipeline-cache.cases.unit.test.ts +631 -0
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +1123 -0
- package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
- package/src/__tests__/resolveName.test.ts +14 -2
- package/src/__tests__/shadow-caster-empty-schema.test.ts +1 -1
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
- package/src/__tests__/shadow-m1.dawn.test.ts +1 -1
- package/src/__tests__/shadow-skip-non-triangle.cases.unit.test.ts +681 -0
- package/src/__tests__/skin-errors-kebab-case.cases.unit.test.ts +475 -0
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +5 -4
- package/src/__tests__/skin-instances-coexist.cases.unit.test.ts +375 -0
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +592 -0
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +619 -0
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +569 -0
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +585 -0
- package/src/__tests__/skin-m3-record-cache.cases.unit.test.ts +457 -0
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +635 -0
- package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
- package/src/__tests__/skin-pipeline-routing.cases.unit.test.ts +411 -0
- package/src/__tests__/skin.cases.unit.test.ts +421 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +1 -6
- package/src/__tests__/skybox-extract.cases.unit.test.ts +511 -0
- package/src/__tests__/skybox-shader-compile.cases.unit.test.ts +437 -0
- package/src/__tests__/skylight-bind-group.cases.unit.test.ts +642 -0
- package/src/__tests__/skylight-component.cases.unit.test.ts +386 -0
- package/src/__tests__/skylight-fallback-path.cases.unit.test.ts +566 -0
- package/src/__tests__/skylight-lazy-projection.test.ts +35 -20
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +506 -0
- package/src/__tests__/skylight-solid-color-extract.cases.unit.test.ts +399 -0
- package/src/__tests__/sprite-animation-tick-boundary.cases.unit.test.ts +521 -0
- package/src/__tests__/sprite-animation-tick-clamp.cases.unit.test.ts +453 -0
- package/src/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.ts +447 -0
- package/src/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.ts +395 -0
- package/src/__tests__/sprite-animation-tick-loop.cases.unit.test.ts +494 -0
- package/src/__tests__/sprite-animation-tick-override-probe.cases.unit.test.ts +439 -0
- package/src/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.ts +466 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +16 -18
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
- package/src/__tests__/ssao-bgl.test.ts +2 -1
- package/src/__tests__/ssao-pipelines.test.ts +1 -1
- package/src/__tests__/storage-buffer-caps.cases.unit.test.ts +373 -0
- package/src/__tests__/systems.cases.unit.test.ts +425 -0
- package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +1 -1
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +670 -0
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +867 -0
- package/src/__tests__/tonemap.cases.unit.test.ts +446 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
- package/src/__tests__/transparent-sort-config-get.cases.unit.test.ts +395 -0
- package/src/__tests__/transparent-sort-config-set.cases.unit.test.ts +424 -0
- package/src/__tests__/transparent-sort.cases.unit.test.ts +684 -0
- package/src/__tests__/upload-texture-consistency.cases.unit.test.ts +478 -0
- package/src/__tests__/verify-revisions.cases.unit.test.ts +619 -0
- package/src/__tests__/wire-default-loaders.cases.unit.test.ts +461 -0
- package/src/__tests__/zero-camera-clear-fallback.cases.unit.test.ts +374 -0
- package/src/collect-scene-asset.ts +1 -1
- package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
- package/src/createRenderer.ts +1 -0
- package/src/dev-import-transport.ts +2 -1
- package/src/sprite-param-values.ts +1 -1
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/asset.unit.test.d.ts +0 -2
- package/dist/__tests__/asset.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline.unit.test.d.ts +0 -2
- package/dist/__tests__/pipeline.unit.test.d.ts.map +0 -1
- package/dist/__tests__/systems.unit.test.d.ts +0 -2
- package/dist/__tests__/systems.unit.test.d.ts.map +0 -1
- package/src/__tests__/asset.unit.test.ts +0 -7110
- package/src/__tests__/pipeline.unit.test.ts +0 -5283
- package/src/__tests__/systems.unit.test.ts +0 -9210
|
@@ -0,0 +1,1666 @@
|
|
|
1
|
+
// @ts-nocheck — merged file: indexed-access checks cascade across noUncheckedIndexedAccess for blocks originally outside src/ rootDir
|
|
2
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
3
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
|
|
4
|
+
//
|
|
5
|
+
// Source files (N=44):
|
|
6
|
+
// - packages/runtime/__tests__/antialias-from-f32.test.ts
|
|
7
|
+
// - packages/runtime/src/__tests__/bloom-gating.test.ts
|
|
8
|
+
// - packages/runtime/src/__tests__/camera-antialias.test.ts
|
|
9
|
+
// - packages/runtime/src/__tests__/camera-bloom.test.ts
|
|
10
|
+
// - packages/runtime/src/__tests__/camera-clear-schema.test.ts
|
|
11
|
+
// - packages/runtime/src/__tests__/camera-ortho.test.ts
|
|
12
|
+
// - packages/runtime/src/__tests__/fxaa-intermediate-texture.test.ts
|
|
13
|
+
// - packages/runtime/src/__tests__/fxaa-pipeline.test.ts
|
|
14
|
+
// - packages/runtime/src/__tests__/ibl-pipeline-cache.test.ts
|
|
15
|
+
// - packages/runtime/src/__tests__/ibl-runtime-probe.test.ts
|
|
16
|
+
// - packages/runtime/src/__tests__/render-system-record-warn-once.test.ts
|
|
17
|
+
// - packages/runtime/src/__tests__/shadow-skip-non-triangle.test.ts
|
|
18
|
+
// - packages/runtime/src/__tests__/skin-errors-kebab-case.test.ts
|
|
19
|
+
// - packages/runtime/src/__tests__/skybox-error.test.ts
|
|
20
|
+
// - packages/runtime/src/__tests__/skybox-shader-compile.test.ts
|
|
21
|
+
// - packages/runtime/src/__tests__/skylight-bind-group.test.ts
|
|
22
|
+
// - packages/runtime/src/__tests__/skylight-component.test.ts
|
|
23
|
+
// - packages/runtime/src/__tests__/skylight-fallback-path.test.ts
|
|
24
|
+
// - packages/runtime/src/__tests__/skylight-pipeline-layout.test.ts
|
|
25
|
+
// - packages/runtime/src/__tests__/tonemap-hdr-target.test.ts
|
|
26
|
+
// - packages/runtime/src/__tests__/tonemap-pipeline-split.test.ts
|
|
27
|
+
// - packages/runtime/src/__tests__/zero-camera-clear-fallback.test.ts
|
|
28
|
+
// - packages/runtime/src/components/__tests__/skin.test.ts
|
|
29
|
+
// - packages/runtime/src/systems/__tests__/advance-animation-player.test.ts
|
|
30
|
+
// - packages/runtime/src/systems/__tests__/graph-skybox.test.ts
|
|
31
|
+
// - packages/runtime/src/systems/__tests__/propagate-transforms.test.ts
|
|
32
|
+
// - packages/runtime/src/systems/__tests__/skin-cap-gate.test.ts
|
|
33
|
+
// - packages/runtime/src/systems/__tests__/skin-instances-coexist.test.ts
|
|
34
|
+
// - packages/runtime/src/systems/__tests__/skin-palette-extract.test.ts
|
|
35
|
+
// - packages/runtime/src/systems/__tests__/skin-pipeline-routing.test.ts
|
|
36
|
+
// - packages/runtime/src/systems/__tests__/skybox-extract.test.ts
|
|
37
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-boundary.test.ts
|
|
38
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-clamp.test.ts
|
|
39
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-frame-duration-negative.test.ts
|
|
40
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-frame-duration-zero.test.ts
|
|
41
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-loop.test.ts
|
|
42
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-override-probe.test.ts
|
|
43
|
+
// - packages/runtime/src/systems/__tests__/sprite-animation-tick-regions-mismatch.test.ts
|
|
44
|
+
// - packages/runtime/src/systems/__tests__/tonemap.test.ts
|
|
45
|
+
// - packages/runtime/src/systems/__tests__/transparent-sort-config-get.test.ts
|
|
46
|
+
// - packages/runtime/src/systems/__tests__/transparent-sort-config-set.test.ts
|
|
47
|
+
// - packages/runtime/src/systems/__tests__/transparent-sort.test.ts
|
|
48
|
+
// - packages/runtime/src/__tests__/render-system-multi-material.test.ts
|
|
49
|
+
// - packages/runtime/src/__tests__/render-system-record-submesh.test.ts
|
|
50
|
+
//
|
|
51
|
+
// Paradigm: each block-scope wraps a source file. ancestorTitles[0] is the
|
|
52
|
+
// source-preserved inner describe (NOT the source filename for these 3 files
|
|
53
|
+
// — recovery path: vitest report ancestorTitles -> grep this file -> upstream
|
|
54
|
+
// `// ─── from <name>.test.ts ───` block separator -> source filename).
|
|
55
|
+
// Top-level imports merged + deduped.
|
|
56
|
+
|
|
57
|
+
import { readFileSync } from 'node:fs';
|
|
58
|
+
import { resolve } from 'node:path';
|
|
59
|
+
import { fileURLToPath } from 'node:url';
|
|
60
|
+
import { AnimationPlayer } from '@forgeax/engine-animation';
|
|
61
|
+
import type { AssetRuntimeErrorCode } from '@forgeax/engine-assets-runtime';
|
|
62
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
63
|
+
import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
|
|
64
|
+
import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
|
|
65
|
+
import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
|
|
66
|
+
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
67
|
+
import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
|
|
68
|
+
import {
|
|
69
|
+
ANTIALIAS_FXAA,
|
|
70
|
+
ANTIALIAS_NONE,
|
|
71
|
+
BLOOM_DISABLED,
|
|
72
|
+
BLOOM_ENABLED,
|
|
73
|
+
CAMERA_PROJECTION_ORTHOGRAPHIC,
|
|
74
|
+
CAMERA_PROJECTION_PERSPECTIVE,
|
|
75
|
+
Camera,
|
|
76
|
+
MeshFilter,
|
|
77
|
+
MeshRenderer,
|
|
78
|
+
SkyboxBackground,
|
|
79
|
+
Skylight,
|
|
80
|
+
} from '@forgeax/engine-render';
|
|
81
|
+
import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
|
|
82
|
+
import { ok as rhiOk } from '@forgeax/engine-rhi';
|
|
83
|
+
import { ChildOf, Name, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
84
|
+
import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
|
|
85
|
+
import type { SkinErrorCode } from '@forgeax/engine-skinning';
|
|
86
|
+
import {
|
|
87
|
+
Skin,
|
|
88
|
+
SkinInstancesCoexistForbiddenError,
|
|
89
|
+
SkinJointCountExceededError,
|
|
90
|
+
SkinJointDespawnedError,
|
|
91
|
+
SkinJointPathUnresolvedError,
|
|
92
|
+
} from '@forgeax/engine-skinning';
|
|
93
|
+
import type {
|
|
94
|
+
Handle,
|
|
95
|
+
MaterialAsset,
|
|
96
|
+
MeshAsset,
|
|
97
|
+
SkeletonAsset,
|
|
98
|
+
TextureFormat,
|
|
99
|
+
} from '@forgeax/engine-types';
|
|
100
|
+
import { toShared } from '@forgeax/engine-types';
|
|
101
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
102
|
+
import { worldInternal } from '../../../ecs/src/world-internal';
|
|
103
|
+
import {
|
|
104
|
+
SPRITE_PLAYBACK_MODE_CLAMP,
|
|
105
|
+
SPRITE_PLAYBACK_MODE_LOOP,
|
|
106
|
+
SpriteAnimation,
|
|
107
|
+
SpriteRegionOverride,
|
|
108
|
+
} from '../../../render/src/components';
|
|
109
|
+
import {
|
|
110
|
+
type CameraProjection,
|
|
111
|
+
cameraProjectionFromF32,
|
|
112
|
+
} from '../../../render/src/components/camera';
|
|
113
|
+
import {
|
|
114
|
+
assembleMaterialWithSkylightEntries,
|
|
115
|
+
createSkylightFallback,
|
|
116
|
+
mergeSkylightIntoMaterialBgl,
|
|
117
|
+
} from '../../../render/src/ibl/skylight-bind-group';
|
|
118
|
+
import { buildPbrPipelineLayouts, buildUnlitMaterialBgl } from '../../../render/src/pbr-pipeline';
|
|
119
|
+
import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-ssbo';
|
|
120
|
+
import { selectSwapChainFormat } from '../../../render/src/render-system';
|
|
121
|
+
import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
|
|
122
|
+
import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
|
|
123
|
+
import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
|
|
124
|
+
import { drawWithOwners } from './renderer-test-utils';
|
|
125
|
+
|
|
126
|
+
function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
|
|
127
|
+
return component.fields[name]?.type;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function componentFieldDefault(
|
|
131
|
+
component: { fields: Record<string, { default?: unknown }> },
|
|
132
|
+
name: string,
|
|
133
|
+
) {
|
|
134
|
+
return component.fields[name]?.default;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function componentSchemaTypes(component: { fields: Record<string, { type: string }> }) {
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
Object.entries(component.fields).map(([name, field]) => [name, field.type]),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
afterEach(() => vi.restoreAllMocks());
|
|
144
|
+
|
|
145
|
+
type RendererErrorObservation = {
|
|
146
|
+
readonly code: string;
|
|
147
|
+
readonly detail?: unknown;
|
|
148
|
+
readonly hint?: string;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function unwrapRendererError(value: unknown): RendererErrorObservation {
|
|
152
|
+
let current = value as RendererErrorObservation;
|
|
153
|
+
while (
|
|
154
|
+
current.detail !== undefined &&
|
|
155
|
+
typeof current.detail === 'object' &&
|
|
156
|
+
current.detail !== null
|
|
157
|
+
) {
|
|
158
|
+
const cause = (current.detail as { cause?: unknown }).cause;
|
|
159
|
+
if (
|
|
160
|
+
cause === undefined ||
|
|
161
|
+
typeof cause !== 'object' ||
|
|
162
|
+
cause === null ||
|
|
163
|
+
typeof (cause as { code?: unknown }).code !== 'string'
|
|
164
|
+
) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
current = cause as RendererErrorObservation;
|
|
168
|
+
}
|
|
169
|
+
return current;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function subscribeRendererErrors(
|
|
173
|
+
renderer: RendererType,
|
|
174
|
+
listener: (error: RendererErrorObservation) => void,
|
|
175
|
+
): () => void {
|
|
176
|
+
return renderer.subscribe((event) => {
|
|
177
|
+
if (event.kind === 'error') listener(unwrapRendererError(event.error));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function drawPublished(renderer: RendererType, world: WorldType) {
|
|
182
|
+
const attached = renderer.attach(world);
|
|
183
|
+
if (!attached.ok) throw attached.error;
|
|
184
|
+
world.update().unwrap();
|
|
185
|
+
return drawWithOwners(renderer, world);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// feat-20260704-runtime-tier1-decomposition M2 / w12: reconstitute the
|
|
189
|
+
// eliminated top-level RuntimeErrorCode aggregate union (D-3) as a test-local
|
|
190
|
+
// alias so the exhaustive-switch bodies below stay byte-identical (AC-09).
|
|
191
|
+
type RuntimeLayerErrorCode = RenderErrorCode | AssetRuntimeErrorCode | SkinErrorCode;
|
|
192
|
+
|
|
193
|
+
import {
|
|
194
|
+
AnimationTargetId,
|
|
195
|
+
bindAnimationTargets,
|
|
196
|
+
advanceAnimationPlayer as canonicalAdvanceAnimationPlayer,
|
|
197
|
+
} from '@forgeax/engine-animation';
|
|
198
|
+
import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
|
|
199
|
+
import { DeviceScope } from '../../../render/src/device/device-scope';
|
|
200
|
+
import { GpuBuffer } from '../../../render/src/gpu-resource';
|
|
201
|
+
import { getOrCreateIblCache, hasIblCache } from '../../../render/src/ibl/IblPipelineCache';
|
|
202
|
+
import {
|
|
203
|
+
disposeInstanceBuffers,
|
|
204
|
+
type InstanceBufferCacheEntry,
|
|
205
|
+
} from '../../../render/src/instance-buffer-cache';
|
|
206
|
+
import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
|
|
207
|
+
import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
|
|
208
|
+
import {
|
|
209
|
+
warnMultiLightDirectional,
|
|
210
|
+
warnMultiLightPoint,
|
|
211
|
+
warnMultiLightSpot,
|
|
212
|
+
} from '../../../render/src/record/helpers';
|
|
213
|
+
import type { CameraSnapshot } from '../../../render/src/render-contract';
|
|
214
|
+
import {
|
|
215
|
+
type ExtractedLights,
|
|
216
|
+
extractFrame,
|
|
217
|
+
extractFrames,
|
|
218
|
+
prepareExtractContext,
|
|
219
|
+
} from '../../../render/src/render-system-extract';
|
|
220
|
+
import {
|
|
221
|
+
getTransparentSortConfig,
|
|
222
|
+
setTransparentSortConfig,
|
|
223
|
+
TRANSPARENT_SORT_CONFIG_KEY,
|
|
224
|
+
TRANSPARENT_SORT_MODE_LAYER_Y,
|
|
225
|
+
TRANSPARENT_SORT_MODE_LAYER_YZ,
|
|
226
|
+
TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
227
|
+
} from '../../../render/src/systems/transparent-sort-config';
|
|
228
|
+
import { spriteAnimationTickSystem } from '../systems/sprite-animation-tick';
|
|
229
|
+
import { REC709_LUMA_WEIGHTS, tonemapReinhardLuminance } from '../systems/tonemap';
|
|
230
|
+
import { transparentSortEntries } from '../systems/transparent-sort';
|
|
231
|
+
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
232
|
+
|
|
233
|
+
void [
|
|
234
|
+
ANTIALIAS_FXAA,
|
|
235
|
+
ANTIALIAS_NONE,
|
|
236
|
+
AnimationPlayer,
|
|
237
|
+
AnimationTargetId,
|
|
238
|
+
AssetRegistry,
|
|
239
|
+
BLOOM_DISABLED,
|
|
240
|
+
BLOOM_ENABLED,
|
|
241
|
+
CAMERA_PROJECTION_ORTHOGRAPHIC,
|
|
242
|
+
CAMERA_PROJECTION_PERSPECTIVE,
|
|
243
|
+
Camera,
|
|
244
|
+
ChildOf,
|
|
245
|
+
DeviceScope,
|
|
246
|
+
ENTITY_NULL_RAW,
|
|
247
|
+
GpuBuffer,
|
|
248
|
+
INSTANCE_STORAGE_STRIDE_FLOATS,
|
|
249
|
+
MeshFilter,
|
|
250
|
+
MeshRenderer,
|
|
251
|
+
Name,
|
|
252
|
+
REC709_LUMA_WEIGHTS,
|
|
253
|
+
SPRITE_PLAYBACK_MODE_CLAMP,
|
|
254
|
+
SPRITE_PLAYBACK_MODE_LOOP,
|
|
255
|
+
Skin,
|
|
256
|
+
SkinInstancesCoexistForbiddenError,
|
|
257
|
+
SkinJointCountExceededError,
|
|
258
|
+
SkinJointDespawnedError,
|
|
259
|
+
SkinJointPathUnresolvedError,
|
|
260
|
+
SkyboxBackground,
|
|
261
|
+
Skylight,
|
|
262
|
+
SpriteAnimation,
|
|
263
|
+
SpriteAnimationInvalidError,
|
|
264
|
+
SpriteRegionOverride,
|
|
265
|
+
TONEMAP_LUMINANCE_EPSILON,
|
|
266
|
+
TRANSPARENT_SORT_CONFIG_KEY,
|
|
267
|
+
TRANSPARENT_SORT_MODE_LAYER_Y,
|
|
268
|
+
TRANSPARENT_SORT_MODE_LAYER_YZ,
|
|
269
|
+
TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
270
|
+
Transform,
|
|
271
|
+
World,
|
|
272
|
+
ZERO_CAMERA_CLEAR_FALLBACK,
|
|
273
|
+
afterEach,
|
|
274
|
+
assembleMaterialWithSkylightEntries,
|
|
275
|
+
beforeAll,
|
|
276
|
+
beforeEach,
|
|
277
|
+
bindAnimationTargets,
|
|
278
|
+
buildPbrPipelineLayouts,
|
|
279
|
+
buildUnlitMaterialBgl,
|
|
280
|
+
cameraProjectionFromF32,
|
|
281
|
+
canonicalAdvanceAnimationPlayer,
|
|
282
|
+
componentFieldDefault,
|
|
283
|
+
componentFieldType,
|
|
284
|
+
componentSchemaTypes,
|
|
285
|
+
createSkinPaletteAllocator,
|
|
286
|
+
createSkylightFallback,
|
|
287
|
+
deriveAnimationTargetId,
|
|
288
|
+
describe,
|
|
289
|
+
disposeInstanceBuffers,
|
|
290
|
+
drawPublished,
|
|
291
|
+
drawWithOwners,
|
|
292
|
+
expect,
|
|
293
|
+
extractFrame,
|
|
294
|
+
extractFrames,
|
|
295
|
+
fileURLToPath,
|
|
296
|
+
getOrCreateIblCache,
|
|
297
|
+
getTransparentSortConfig,
|
|
298
|
+
hasIblCache,
|
|
299
|
+
it,
|
|
300
|
+
makeMockShaderRegistry,
|
|
301
|
+
mat4,
|
|
302
|
+
mergeSkylightIntoMaterialBgl,
|
|
303
|
+
prepareExtractContext,
|
|
304
|
+
propagateTransforms,
|
|
305
|
+
readFileSync,
|
|
306
|
+
resolve,
|
|
307
|
+
rhiOk,
|
|
308
|
+
selectSwapChainFormat,
|
|
309
|
+
setTransparentSortConfig,
|
|
310
|
+
spriteAnimationTickSystem,
|
|
311
|
+
standardMaterialShaderVariants,
|
|
312
|
+
subscribeRendererErrors,
|
|
313
|
+
toShared,
|
|
314
|
+
tonemapReinhardLuminance,
|
|
315
|
+
transparentSortEntries,
|
|
316
|
+
unwrapRendererError,
|
|
317
|
+
urpPipeline,
|
|
318
|
+
vec3,
|
|
319
|
+
vi,
|
|
320
|
+
warnMultiLightDirectional,
|
|
321
|
+
warnMultiLightPoint,
|
|
322
|
+
warnMultiLightSpot,
|
|
323
|
+
worldInternal,
|
|
324
|
+
];
|
|
325
|
+
type __MergedKeep =
|
|
326
|
+
| AssetRuntimeErrorCode
|
|
327
|
+
| BindGroupEntry
|
|
328
|
+
| Buffer
|
|
329
|
+
| CameraProjection
|
|
330
|
+
| CameraSnapshot
|
|
331
|
+
| EntityHandle
|
|
332
|
+
| ExtractedLights
|
|
333
|
+
| Handle
|
|
334
|
+
| InstanceBufferCacheEntry
|
|
335
|
+
| MaterialAsset
|
|
336
|
+
| MeshAsset
|
|
337
|
+
| RenderErrorCode
|
|
338
|
+
| RendererErrorObservation
|
|
339
|
+
| RendererType
|
|
340
|
+
| RuntimeLayerErrorCode
|
|
341
|
+
| Sampler
|
|
342
|
+
| SkeletonAsset
|
|
343
|
+
| SkinErrorCode
|
|
344
|
+
| Texture
|
|
345
|
+
| TextureFormat
|
|
346
|
+
| TextureView
|
|
347
|
+
| TransparentEntry
|
|
348
|
+
| WorldType;
|
|
349
|
+
|
|
350
|
+
{
|
|
351
|
+
// --- from advance-animation-player.test.ts ---
|
|
352
|
+
|
|
353
|
+
const defaultTransform = {
|
|
354
|
+
pos: [0, 0, 0],
|
|
355
|
+
quat: [0, 0, 0, 1],
|
|
356
|
+
scale: [1, 1, 1],
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
function makeSampler(
|
|
360
|
+
input: number[],
|
|
361
|
+
output: number[],
|
|
362
|
+
interpolation: 'LINEAR' | 'STEP' = 'LINEAR',
|
|
363
|
+
): AnimationSampler {
|
|
364
|
+
return {
|
|
365
|
+
input: new Float32Array(input),
|
|
366
|
+
output: new Float32Array(output),
|
|
367
|
+
interpolation,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function makeClip(duration: number, channels: AnimationClip['channels']): AnimationClip {
|
|
372
|
+
return {
|
|
373
|
+
kind: 'animation-clip',
|
|
374
|
+
duration,
|
|
375
|
+
channels,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
type TestResolver = { clips: Map<number, AnimationClip>; handles?: Map<number, number> };
|
|
380
|
+
|
|
381
|
+
function makeResolver(clips: Map<number, AnimationClip>): TestResolver {
|
|
382
|
+
return { clips };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function advanceAnimationPlayer(world: World, resolver: TestResolver, dt: number): void {
|
|
386
|
+
const handles = resolver.handles ?? new Map<number, number>();
|
|
387
|
+
resolver.handles = handles;
|
|
388
|
+
for (const [raw, clip] of resolver.clips) {
|
|
389
|
+
if (!handles.has(raw)) handles.set(raw, world.allocSharedRef('AnimationClip', clip));
|
|
390
|
+
}
|
|
391
|
+
const query = world.query({ with: [AnimationPlayer] }).unwrap();
|
|
392
|
+
const allocated = new Set(handles.values());
|
|
393
|
+
const players = Array.from(query, (row) => row.entity);
|
|
394
|
+
for (const entity of players) {
|
|
395
|
+
const player = world.get(entity, AnimationPlayer);
|
|
396
|
+
if (!player.ok) continue;
|
|
397
|
+
const clips = Array.from(player.value.clips, (handle) => {
|
|
398
|
+
const mapped = handles.get(handle);
|
|
399
|
+
return mapped ?? (allocated.has(handle) ? handle : 0);
|
|
400
|
+
});
|
|
401
|
+
world.set(entity, AnimationPlayer, { clips });
|
|
402
|
+
const skin = world.get(entity, Skin);
|
|
403
|
+
if (!skin.ok) continue;
|
|
404
|
+
const targets: EntityHandle[] = [];
|
|
405
|
+
for (const raw of skin.value.joints) {
|
|
406
|
+
if (raw === ENTITY_NULL_RAW) continue;
|
|
407
|
+
const target = raw as EntityHandle;
|
|
408
|
+
const name = world.get(target, Name);
|
|
409
|
+
if (!name.ok) continue;
|
|
410
|
+
if (!world.get(target, ChildOf).ok) {
|
|
411
|
+
world.addComponent(target, { component: ChildOf, data: { parent: entity } });
|
|
412
|
+
}
|
|
413
|
+
if (!world.get(target, AnimationTargetId).ok) {
|
|
414
|
+
world.addComponent(target, {
|
|
415
|
+
component: AnimationTargetId,
|
|
416
|
+
data: { value: deriveAnimationTargetId([name.value.value]) },
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
targets.push(target);
|
|
420
|
+
}
|
|
421
|
+
bindAnimationTargets(world, entity, targets);
|
|
422
|
+
}
|
|
423
|
+
canonicalAdvanceAnimationPlayer(world, dt);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// M2 / w3: spawn data migrated to SoA inline arrays. Single-clip legacy
|
|
427
|
+
// path: clips[0] = handle, weights[0] = 1, speeds[0] = speed, times[0] = 0;
|
|
428
|
+
// slots 1..3 stay zero. `world.set({ time: t })` becomes a partial column
|
|
429
|
+
// write `world.set({ times: new Float32Array([t,0,0,0]) })`. Reads of
|
|
430
|
+
// `.time` route through `.times[0]`. Old expectations preserved (time
|
|
431
|
+
// advance / looping modulo / paused skip) — schema cut only.
|
|
432
|
+
function spawnLegacySinglePlayer(
|
|
433
|
+
world: World,
|
|
434
|
+
clipId: number,
|
|
435
|
+
overrides: { speed?: number; paused?: boolean; looping?: boolean } = {},
|
|
436
|
+
): EntityHandle {
|
|
437
|
+
const speed = overrides.speed ?? 1;
|
|
438
|
+
const paused = overrides.paused ?? false;
|
|
439
|
+
const looping = overrides.looping ?? true;
|
|
440
|
+
return world
|
|
441
|
+
.spawn({
|
|
442
|
+
component: AnimationPlayer,
|
|
443
|
+
data: {
|
|
444
|
+
clips: [
|
|
445
|
+
toShared<'AnimationClip'>(clipId),
|
|
446
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
447
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
448
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
449
|
+
],
|
|
450
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
451
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
452
|
+
speeds: new Float32Array([speed, 1, 1, 1]),
|
|
453
|
+
paused,
|
|
454
|
+
looping,
|
|
455
|
+
},
|
|
456
|
+
})
|
|
457
|
+
.unwrap();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function readLegacyTime(world: World, e: EntityHandle): number {
|
|
461
|
+
const ap = world.get(e, AnimationPlayer).unwrap() as unknown as { times: Float32Array };
|
|
462
|
+
return ap.times[0] ?? 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function writeLegacyTime(world: World, e: EntityHandle, t: number): void {
|
|
466
|
+
world.set(e, AnimationPlayer, { times: new Float32Array([t, 0, 0, 0]) });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
describe('T-17 — advanceAnimationPlayer time advance (AC-17 / AC-18)', () => {
|
|
470
|
+
it('advances time by speed * dt each tick (paused=false, looping=true)', () => {
|
|
471
|
+
const world = new World();
|
|
472
|
+
const clip = makeClip(10, []);
|
|
473
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
474
|
+
|
|
475
|
+
const e = spawnLegacySinglePlayer(world, 1, { speed: 2 });
|
|
476
|
+
writeLegacyTime(world, e, 0);
|
|
477
|
+
|
|
478
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
479
|
+
expect(readLegacyTime(world, e)).toBe(1.0);
|
|
480
|
+
|
|
481
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
482
|
+
expect(readLegacyTime(world, e)).toBe(2.0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('looping=true: time wraps around with modulo', () => {
|
|
486
|
+
const world = new World();
|
|
487
|
+
const clip = makeClip(3, []);
|
|
488
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
489
|
+
|
|
490
|
+
const e = spawnLegacySinglePlayer(world, 1, { speed: 2, looping: true });
|
|
491
|
+
writeLegacyTime(world, e, 2);
|
|
492
|
+
|
|
493
|
+
advanceAnimationPlayer(world, resolver, 1.0); // 2 + 2*1 = 4, mod 3 = 1
|
|
494
|
+
expect(readLegacyTime(world, e)).toBeCloseTo(1.0, 5);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('looping=false: stops at duration', () => {
|
|
498
|
+
const world = new World();
|
|
499
|
+
const clip = makeClip(3, []);
|
|
500
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
501
|
+
|
|
502
|
+
const e = spawnLegacySinglePlayer(world, 1, { speed: 2, looping: false });
|
|
503
|
+
writeLegacyTime(world, e, 2);
|
|
504
|
+
|
|
505
|
+
advanceAnimationPlayer(world, resolver, 1.0); // 2 + 2*1 = 4, clamp to 3
|
|
506
|
+
expect(readLegacyTime(world, e)).toBe(3.0);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('paused=true: time does not change', () => {
|
|
510
|
+
const world = new World();
|
|
511
|
+
const clip = makeClip(10, []);
|
|
512
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
513
|
+
|
|
514
|
+
const e = spawnLegacySinglePlayer(world, 1, { speed: 2, paused: true });
|
|
515
|
+
writeLegacyTime(world, e, 5);
|
|
516
|
+
|
|
517
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
518
|
+
expect(readLegacyTime(world, e)).toBe(5.0);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe('T-17 — advanceAnimationPlayer joint sampling no-crash (AC-17)', () => {
|
|
523
|
+
it('LINEAR sampling with Skin+Transform: system runs without crash', () => {
|
|
524
|
+
const world = new World();
|
|
525
|
+
const sampler = makeSampler([0, 2], [0, 0, 0, 2, 4, 6], 'LINEAR');
|
|
526
|
+
const clip = makeClip(2, [
|
|
527
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler },
|
|
528
|
+
]);
|
|
529
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
530
|
+
|
|
531
|
+
const jointE = world.spawn({ component: Transform, data: defaultTransform }).unwrap();
|
|
532
|
+
|
|
533
|
+
world.spawn(
|
|
534
|
+
{
|
|
535
|
+
component: AnimationPlayer,
|
|
536
|
+
data: {
|
|
537
|
+
clips: [
|
|
538
|
+
toShared<'AnimationClip'>(1),
|
|
539
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
540
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
541
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
542
|
+
],
|
|
543
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
544
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
545
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
component: Skin,
|
|
550
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
551
|
+
},
|
|
552
|
+
{ component: Transform, data: defaultTransform },
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1.0)).not.toThrow();
|
|
556
|
+
// Time should have advanced.
|
|
557
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
558
|
+
expect(tf).toBeDefined();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('LINEAR rotation sampling with Skin+Transform: system runs without crash', () => {
|
|
562
|
+
const world = new World();
|
|
563
|
+
const halfSqrt2 = Math.sqrt(2) / 2;
|
|
564
|
+
const sampler = makeSampler([0, 2], [0, 0, 0, 1, 0, halfSqrt2, 0, halfSqrt2], 'LINEAR');
|
|
565
|
+
const clip = makeClip(2, [
|
|
566
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'rotation', sampler },
|
|
567
|
+
]);
|
|
568
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
569
|
+
|
|
570
|
+
const jointE = world.spawn({ component: Transform, data: defaultTransform }).unwrap();
|
|
571
|
+
|
|
572
|
+
world.spawn(
|
|
573
|
+
{
|
|
574
|
+
component: AnimationPlayer,
|
|
575
|
+
data: {
|
|
576
|
+
clips: [
|
|
577
|
+
toShared<'AnimationClip'>(1),
|
|
578
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
579
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
580
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
581
|
+
],
|
|
582
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
583
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
584
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
component: Skin,
|
|
589
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
590
|
+
},
|
|
591
|
+
{ component: Transform, data: defaultTransform },
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1.0)).not.toThrow();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('STEP interpolation with Skin+Transform: system runs without crash', () => {
|
|
598
|
+
const world = new World();
|
|
599
|
+
const sampler = makeSampler([0, 2], [0, 0, 0, 5, 5, 5], 'STEP');
|
|
600
|
+
const clip = makeClip(2, [
|
|
601
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler },
|
|
602
|
+
]);
|
|
603
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
604
|
+
|
|
605
|
+
const jointE = world.spawn({ component: Transform, data: defaultTransform }).unwrap();
|
|
606
|
+
|
|
607
|
+
world.spawn(
|
|
608
|
+
{
|
|
609
|
+
component: AnimationPlayer,
|
|
610
|
+
data: {
|
|
611
|
+
clips: [
|
|
612
|
+
toShared<'AnimationClip'>(1),
|
|
613
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
614
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
615
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
616
|
+
],
|
|
617
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
618
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
619
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
component: Skin,
|
|
624
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
625
|
+
},
|
|
626
|
+
{ component: Transform, data: defaultTransform },
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1.5)).not.toThrow();
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe('T-17 — edge cases', () => {
|
|
634
|
+
it('entity without Skin/Transform is skipped gracefully', () => {
|
|
635
|
+
const world = new World();
|
|
636
|
+
const clip = makeClip(10, []);
|
|
637
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
638
|
+
|
|
639
|
+
spawnLegacySinglePlayer(world, 1);
|
|
640
|
+
|
|
641
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1 / 60)).not.toThrow();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('entity with unresolved clip handle is skipped', () => {
|
|
645
|
+
const world = new World();
|
|
646
|
+
const resolver = makeResolver(new Map());
|
|
647
|
+
|
|
648
|
+
const e = spawnLegacySinglePlayer(world, 999);
|
|
649
|
+
writeLegacyTime(world, e, 0);
|
|
650
|
+
|
|
651
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1 / 60)).not.toThrow();
|
|
652
|
+
expect(readLegacyTime(world, e)).toBe(0);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('zero duration clip does not crash', () => {
|
|
656
|
+
const world = new World();
|
|
657
|
+
const clip = makeClip(0, []);
|
|
658
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
659
|
+
|
|
660
|
+
const e = spawnLegacySinglePlayer(world, 1, { speed: 2 });
|
|
661
|
+
writeLegacyTime(world, e, 0);
|
|
662
|
+
|
|
663
|
+
expect(() => advanceAnimationPlayer(world, resolver, 1.0)).not.toThrow();
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe('T-17 — registerAdvanceAnimationPlayer schedule order (D-9)', () => {
|
|
668
|
+
it('constant ADVANCE_ANIMATION_PLAYER_SYSTEM name matches', async () => {
|
|
669
|
+
const { ADVANCE_ANIMATION_PLAYER_SYSTEM } = await import('@forgeax/engine-animation');
|
|
670
|
+
expect(ADVANCE_ANIMATION_PLAYER_SYSTEM).toBe('advanceAnimationPlayer');
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
675
|
+
// M2 / w6 — dev-mode warn throttle (plan-strategy D-2 / AC-05(d)).
|
|
676
|
+
//
|
|
677
|
+
// Three it blocks per the M2 mission brief:
|
|
678
|
+
// (a) 60 frames + same triple => warn fires exactly once
|
|
679
|
+
// (b) two distinct channelKeys (different chIdx) => 2 warns
|
|
680
|
+
// (c) same channelKey, two distinct reasons => 2 warns (key shape includes reason)
|
|
681
|
+
//
|
|
682
|
+
// Each `it` resets via `_resetAnimationWarnsForTests(world)` so the WeakMap
|
|
683
|
+
// bag is fresh; vi.spyOn(console, 'warn') captures the fan-out and the
|
|
684
|
+
// mock is restored at the test's end.
|
|
685
|
+
//
|
|
686
|
+
// Test scaffolding mirrors the T-17 helpers above (spawnLegacySinglePlayer
|
|
687
|
+
// is single-clip; warn-pass tests need direct multi-slot setup or
|
|
688
|
+
// unmapped target clips, so they use the SoA literal form inline).
|
|
689
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
describe('M2 / w6 — advanceAnimationPlayer dev-mode warn throttle (D-2)', () => {
|
|
692
|
+
it('60 consecutive frames with same (entity, channel, reason) emit warn exactly once', async () => {
|
|
693
|
+
const { _resetAnimationWarnsForTests } = await import('@forgeax/engine-animation');
|
|
694
|
+
const world = new World();
|
|
695
|
+
// Channel target ID has no matching bound target ID
|
|
696
|
+
// — every frame triggers channel-target-missing on (entity, clip:1, ch:0).
|
|
697
|
+
const sampler = makeSampler([0, 1], [0, 0, 0, 1, 1, 1], 'LINEAR');
|
|
698
|
+
const clip = makeClip(1, [
|
|
699
|
+
{
|
|
700
|
+
targetId: deriveAnimationTargetId(['mismatched-leaf']),
|
|
701
|
+
property: 'translation',
|
|
702
|
+
sampler,
|
|
703
|
+
},
|
|
704
|
+
]);
|
|
705
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
706
|
+
|
|
707
|
+
const jointE = world
|
|
708
|
+
.spawn(
|
|
709
|
+
{ component: Transform, data: defaultTransform },
|
|
710
|
+
{ component: Name, data: { value: 'real-joint' } },
|
|
711
|
+
)
|
|
712
|
+
.unwrap();
|
|
713
|
+
world.spawn(
|
|
714
|
+
{
|
|
715
|
+
component: AnimationPlayer,
|
|
716
|
+
data: {
|
|
717
|
+
clips: [
|
|
718
|
+
toShared<'AnimationClip'>(1),
|
|
719
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
720
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
721
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
722
|
+
],
|
|
723
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
724
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
725
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
component: Skin,
|
|
730
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
731
|
+
},
|
|
732
|
+
{ component: Transform, data: defaultTransform },
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
_resetAnimationWarnsForTests(world);
|
|
736
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
737
|
+
try {
|
|
738
|
+
for (let i = 0; i < 60; i++) {
|
|
739
|
+
advanceAnimationPlayer(world, resolver, 1 / 60);
|
|
740
|
+
}
|
|
741
|
+
const targetMissingCalls = warnSpy.mock.calls.filter(
|
|
742
|
+
(c) => (c[0] as { code?: string }).code === 'animation-target-missing',
|
|
743
|
+
);
|
|
744
|
+
expect(targetMissingCalls.length).toBe(1);
|
|
745
|
+
} finally {
|
|
746
|
+
warnSpy.mockRestore();
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it('two distinct channel indices on same entity each emit one warn (3 channels => 3 warns)', async () => {
|
|
751
|
+
const { _resetAnimationWarnsForTests } = await import('@forgeax/engine-animation');
|
|
752
|
+
const world = new World();
|
|
753
|
+
// Three channels each targeting an unmapped ID — chIdx differs, so
|
|
754
|
+
// the (entity, clip, chIdx, reason) key splits into 3 distinct entries
|
|
755
|
+
// and the throttle should NOT collapse them.
|
|
756
|
+
const sampler = makeSampler([0, 1], [0, 0, 0, 1, 1, 1], 'LINEAR');
|
|
757
|
+
const clip = makeClip(1, [
|
|
758
|
+
{ targetId: deriveAnimationTargetId(['leaf-A']), property: 'translation', sampler },
|
|
759
|
+
{ targetId: deriveAnimationTargetId(['leaf-B']), property: 'translation', sampler },
|
|
760
|
+
{ targetId: deriveAnimationTargetId(['leaf-C']), property: 'translation', sampler },
|
|
761
|
+
]);
|
|
762
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
763
|
+
|
|
764
|
+
const jointE = world
|
|
765
|
+
.spawn(
|
|
766
|
+
{ component: Transform, data: defaultTransform },
|
|
767
|
+
{ component: Name, data: { value: 'real-joint' } },
|
|
768
|
+
)
|
|
769
|
+
.unwrap();
|
|
770
|
+
world.spawn(
|
|
771
|
+
{
|
|
772
|
+
component: AnimationPlayer,
|
|
773
|
+
data: {
|
|
774
|
+
clips: [
|
|
775
|
+
toShared<'AnimationClip'>(1),
|
|
776
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
777
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
778
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
779
|
+
],
|
|
780
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
781
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
782
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
component: Skin,
|
|
787
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
788
|
+
},
|
|
789
|
+
{ component: Transform, data: defaultTransform },
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
_resetAnimationWarnsForTests(world);
|
|
793
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
794
|
+
try {
|
|
795
|
+
for (let i = 0; i < 5; i++) advanceAnimationPlayer(world, resolver, 1 / 60);
|
|
796
|
+
const targetMissingCalls = warnSpy.mock.calls.filter(
|
|
797
|
+
(c) => (c[0] as { code?: string }).code === 'animation-target-missing',
|
|
798
|
+
);
|
|
799
|
+
expect(targetMissingCalls.length).toBe(3);
|
|
800
|
+
const chIdxValues = targetMissingCalls.map(
|
|
801
|
+
(c) => (c[0] as { detail: { channel: number } }).detail.channel,
|
|
802
|
+
);
|
|
803
|
+
expect(new Set(chIdxValues)).toEqual(new Set([0, 1, 2]));
|
|
804
|
+
} finally {
|
|
805
|
+
warnSpy.mockRestore();
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('two distinct reasons on same channel emit two warns (key includes reason)', async () => {
|
|
810
|
+
const { _resetAnimationWarnsForTests } = await import('@forgeax/engine-animation');
|
|
811
|
+
const world = new World();
|
|
812
|
+
// Slot 0: clip 1 has channel 0 = translation on 'real-joint' (resolves)
|
|
813
|
+
// and channel 1 = translation on 'unknown-joint' (target-missing).
|
|
814
|
+
// Slot 1: clip 2 has only channel 0 = rotation on 'real-joint'.
|
|
815
|
+
// After tick:
|
|
816
|
+
// - clip1.ch1 fires channel-target-missing (entity, 1, 1, target-missing).
|
|
817
|
+
// - The (real-joint, rotation) tuple is covered by slot1 but missing
|
|
818
|
+
// on slot0; slot1.ch0 fires channel-missing-on-some-slot
|
|
819
|
+
// (entity, 2, 0, missing-on-some-slot). The (real-joint, translation)
|
|
820
|
+
// tuple is covered by slot0 but missing on slot1; slot0.ch0 fires
|
|
821
|
+
// channel-missing-on-some-slot (entity, 1, 0, missing-on-some-slot).
|
|
822
|
+
// So we expect: target-missing=1, missing-on-some-slot=2.
|
|
823
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 1, 1, 1], 'LINEAR');
|
|
824
|
+
const sampR = makeSampler([0, 1], [0, 0, 0, 1, 0, 0, 0, 1], 'LINEAR');
|
|
825
|
+
const clip1 = makeClip(1, [
|
|
826
|
+
{
|
|
827
|
+
targetId: deriveAnimationTargetId(['real-joint']),
|
|
828
|
+
property: 'translation',
|
|
829
|
+
sampler: sampT,
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
targetId: deriveAnimationTargetId(['unknown-joint']),
|
|
833
|
+
property: 'translation',
|
|
834
|
+
sampler: sampT,
|
|
835
|
+
},
|
|
836
|
+
]);
|
|
837
|
+
const clip2 = makeClip(1, [
|
|
838
|
+
{ targetId: deriveAnimationTargetId(['real-joint']), property: 'rotation', sampler: sampR },
|
|
839
|
+
]);
|
|
840
|
+
const resolver = makeResolver(
|
|
841
|
+
new Map([
|
|
842
|
+
[1, clip1],
|
|
843
|
+
[2, clip2],
|
|
844
|
+
]),
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
const jointE = world
|
|
848
|
+
.spawn(
|
|
849
|
+
{ component: Transform, data: defaultTransform },
|
|
850
|
+
{ component: Name, data: { value: 'real-joint' } },
|
|
851
|
+
)
|
|
852
|
+
.unwrap();
|
|
853
|
+
world.spawn(
|
|
854
|
+
{
|
|
855
|
+
component: AnimationPlayer,
|
|
856
|
+
data: {
|
|
857
|
+
clips: [
|
|
858
|
+
toShared<'AnimationClip'>(1),
|
|
859
|
+
toShared<'AnimationClip'>(2),
|
|
860
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
861
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
862
|
+
],
|
|
863
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
864
|
+
weights: new Float32Array([0.5, 0.5, 0, 0]),
|
|
865
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
component: Skin,
|
|
870
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
871
|
+
},
|
|
872
|
+
{ component: Transform, data: defaultTransform },
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
_resetAnimationWarnsForTests(world);
|
|
876
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
877
|
+
try {
|
|
878
|
+
for (let i = 0; i < 5; i++) advanceAnimationPlayer(world, resolver, 1 / 60);
|
|
879
|
+
const targetCalls = warnSpy.mock.calls.filter(
|
|
880
|
+
(c) => (c[0] as { code?: string }).code === 'animation-target-missing',
|
|
881
|
+
);
|
|
882
|
+
const missingCalls = warnSpy.mock.calls.filter(
|
|
883
|
+
(c) => (c[0] as { code?: string }).code === 'animation-channel-missing',
|
|
884
|
+
);
|
|
885
|
+
expect(targetCalls.length).toBe(1);
|
|
886
|
+
// Both translation-on-slot1 and rotation-on-slot0 are missing — two
|
|
887
|
+
// distinct (clip, chIdx, missing-on-some-slot) keys.
|
|
888
|
+
expect(missingCalls.length).toBe(2);
|
|
889
|
+
} finally {
|
|
890
|
+
warnSpy.mockRestore();
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
896
|
+
// M3 / w8 — N-way weight blend matrix (plan-strategy §7 / AC-03/04/05).
|
|
897
|
+
//
|
|
898
|
+
// Ten it blocks covering normalized blend math, invalid-skip, per-channel
|
|
899
|
+
// normalize, duration-modulo, negative-weight clamp, paused time-stasis,
|
|
900
|
+
// resolver miss, and target-missing skips. All it blocks use the shared
|
|
901
|
+
// makeSampler / makeClip / makeResolver helpers from the enclosing block.
|
|
902
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
903
|
+
|
|
904
|
+
describe('M3 N-way weight blend matrix', () => {
|
|
905
|
+
it('single slot weights=[1,0,0,0] equals hard-cut (pos = clipA pos)', () => {
|
|
906
|
+
const world = new World();
|
|
907
|
+
const sampT = makeSampler([0, 1], [1, 2, 3, 10, 20, 30], 'LINEAR');
|
|
908
|
+
const clipA = makeClip(1, [
|
|
909
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
910
|
+
]);
|
|
911
|
+
const resolver = makeResolver(new Map([[1, clipA]]));
|
|
912
|
+
|
|
913
|
+
const jointE = world
|
|
914
|
+
.spawn(
|
|
915
|
+
{ component: Transform, data: defaultTransform },
|
|
916
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
917
|
+
)
|
|
918
|
+
.unwrap();
|
|
919
|
+
world.spawn(
|
|
920
|
+
{
|
|
921
|
+
component: AnimationPlayer,
|
|
922
|
+
data: {
|
|
923
|
+
clips: [
|
|
924
|
+
toShared<'AnimationClip'>(1),
|
|
925
|
+
toShared<'AnimationClip'>(2),
|
|
926
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
927
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
928
|
+
],
|
|
929
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
930
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
931
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
component: Skin,
|
|
936
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
937
|
+
},
|
|
938
|
+
{ component: Transform, data: defaultTransform },
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
advanceAnimationPlayer(world, resolver, 0.5); // time=0.5 => lerp midway
|
|
942
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
943
|
+
expect(tf.pos[0]).toBeCloseTo(5.5); // lerp(1,10,0.5) = 5.5
|
|
944
|
+
expect(tf.pos[1]).toBeCloseTo(11); // lerp(2,20,0.5) = 11
|
|
945
|
+
expect(tf.pos[2]).toBeCloseTo(16.5); // lerp(3,30,0.5) = 16.5
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('two-slot weights=[0.5,0.5,0,0] yields midpoint pose', () => {
|
|
949
|
+
const world = new World();
|
|
950
|
+
const sampT1 = makeSampler([0, 1], [0, 0, 0, 4, 0, 0], 'LINEAR'); // pos (2,0,0) at t=0.5
|
|
951
|
+
const sampT2 = makeSampler([0, 1], [0, 0, 0, 0, 6, 0], 'LINEAR'); // pos (0,3,0) at t=0.5
|
|
952
|
+
const clipA = makeClip(1, [
|
|
953
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT1 },
|
|
954
|
+
]);
|
|
955
|
+
const clipB = makeClip(1, [
|
|
956
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT2 },
|
|
957
|
+
]);
|
|
958
|
+
const resolver = makeResolver(
|
|
959
|
+
new Map([
|
|
960
|
+
[1, clipA],
|
|
961
|
+
[2, clipB],
|
|
962
|
+
]),
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
const jointE = world
|
|
966
|
+
.spawn(
|
|
967
|
+
{ component: Transform, data: defaultTransform },
|
|
968
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
969
|
+
)
|
|
970
|
+
.unwrap();
|
|
971
|
+
world.spawn(
|
|
972
|
+
{
|
|
973
|
+
component: AnimationPlayer,
|
|
974
|
+
data: {
|
|
975
|
+
clips: [
|
|
976
|
+
toShared<'AnimationClip'>(1),
|
|
977
|
+
toShared<'AnimationClip'>(2),
|
|
978
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
979
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
980
|
+
],
|
|
981
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
982
|
+
weights: new Float32Array([0.5, 0.5, 0, 0]),
|
|
983
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
component: Skin,
|
|
988
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
989
|
+
},
|
|
990
|
+
{ component: Transform, data: defaultTransform },
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
advanceAnimationPlayer(world, resolver, 0.5); // both at t=0.5
|
|
994
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
995
|
+
// clipA pos=(2,0,0), clipB pos=(0,3,0), blended=(1,1.5,0) with equal weights
|
|
996
|
+
expect(tf.pos[0]).toBeCloseTo(1);
|
|
997
|
+
expect(tf.pos[1]).toBeCloseTo(1.5);
|
|
998
|
+
expect(tf.pos[2]).toBeCloseTo(0);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('three-slot weights=[1/3,1/3,1/3,0] equal-weight blend', () => {
|
|
1002
|
+
const world = new World();
|
|
1003
|
+
const sampT1 = makeSampler([0, 1], [0, 0, 0, 3, 0, 0], 'LINEAR');
|
|
1004
|
+
const sampT2 = makeSampler([0, 1], [0, 0, 0, 0, 3, 0], 'LINEAR');
|
|
1005
|
+
const sampT3 = makeSampler([0, 1], [0, 0, 0, 0, 0, 3], 'LINEAR');
|
|
1006
|
+
const clipA = makeClip(1, [
|
|
1007
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT1 },
|
|
1008
|
+
]);
|
|
1009
|
+
const clipB = makeClip(1, [
|
|
1010
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT2 },
|
|
1011
|
+
]);
|
|
1012
|
+
const clipC = makeClip(1, [
|
|
1013
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT3 },
|
|
1014
|
+
]);
|
|
1015
|
+
const resolver = makeResolver(
|
|
1016
|
+
new Map([
|
|
1017
|
+
[1, clipA],
|
|
1018
|
+
[2, clipB],
|
|
1019
|
+
[3, clipC],
|
|
1020
|
+
]),
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
const jointE = world
|
|
1024
|
+
.spawn(
|
|
1025
|
+
{ component: Transform, data: defaultTransform },
|
|
1026
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1027
|
+
)
|
|
1028
|
+
.unwrap();
|
|
1029
|
+
world.spawn(
|
|
1030
|
+
{
|
|
1031
|
+
component: AnimationPlayer,
|
|
1032
|
+
data: {
|
|
1033
|
+
clips: [
|
|
1034
|
+
toShared<'AnimationClip'>(1),
|
|
1035
|
+
toShared<'AnimationClip'>(2),
|
|
1036
|
+
toShared<'AnimationClip'>(3),
|
|
1037
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1038
|
+
],
|
|
1039
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1040
|
+
weights: new Float32Array([1 / 3, 1 / 3, 1 / 3, 0]),
|
|
1041
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
component: Skin,
|
|
1046
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1047
|
+
},
|
|
1048
|
+
{ component: Transform, data: defaultTransform },
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
advanceAnimationPlayer(world, resolver, 0.5); // all at t=0.5
|
|
1052
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1053
|
+
// Each clip at t=0.5: (1.5,0,0), (0,1.5,0), (0,0,1.5) => avg = (0.5,0.5,0.5)
|
|
1054
|
+
expect(tf.pos[0]).toBeCloseTo(0.5);
|
|
1055
|
+
expect(tf.pos[1]).toBeCloseTo(0.5);
|
|
1056
|
+
expect(tf.pos[2]).toBeCloseTo(0.5);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('un-normalized weights [0.6,0.6,0,0] normalize to [0.5,0.5,0,0]', () => {
|
|
1060
|
+
const world = new World();
|
|
1061
|
+
const sampT1 = makeSampler([0, 1], [0, 0, 0, 10, 0, 0], 'LINEAR');
|
|
1062
|
+
const sampT2 = makeSampler([0, 1], [0, 0, 0, 0, 10, 0], 'LINEAR');
|
|
1063
|
+
const clipA = makeClip(1, [
|
|
1064
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT1 },
|
|
1065
|
+
]);
|
|
1066
|
+
const clipB = makeClip(1, [
|
|
1067
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT2 },
|
|
1068
|
+
]);
|
|
1069
|
+
const resolver = makeResolver(
|
|
1070
|
+
new Map([
|
|
1071
|
+
[1, clipA],
|
|
1072
|
+
[2, clipB],
|
|
1073
|
+
]),
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
const jointE = world
|
|
1077
|
+
.spawn(
|
|
1078
|
+
{ component: Transform, data: defaultTransform },
|
|
1079
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1080
|
+
)
|
|
1081
|
+
.unwrap();
|
|
1082
|
+
world.spawn(
|
|
1083
|
+
{
|
|
1084
|
+
component: AnimationPlayer,
|
|
1085
|
+
data: {
|
|
1086
|
+
clips: [
|
|
1087
|
+
toShared<'AnimationClip'>(1),
|
|
1088
|
+
toShared<'AnimationClip'>(2),
|
|
1089
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1090
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1091
|
+
],
|
|
1092
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1093
|
+
weights: new Float32Array([0.6, 0.6, 0, 0]),
|
|
1094
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
component: Skin,
|
|
1099
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1100
|
+
},
|
|
1101
|
+
{ component: Transform, data: defaultTransform },
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
1105
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1106
|
+
// Normalized blend: sumW=1.2, each w/Σw=0.5. pos = (5*0.5 + 0*0.5, 0*0.5 + 5*0.5) = (2.5, 2.5)
|
|
1107
|
+
expect(tf.pos[0]).toBeCloseTo(2.5);
|
|
1108
|
+
expect(tf.pos[1]).toBeCloseTo(2.5);
|
|
1109
|
+
// Weights column unchanged (weightsView reflects the original 0.6 values)
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('weights[i] < 0 clamped to 0 and NOT written back', () => {
|
|
1113
|
+
const world = new World();
|
|
1114
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 4, 4, 4], 'LINEAR');
|
|
1115
|
+
const clipA = makeClip(1, [
|
|
1116
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1117
|
+
]);
|
|
1118
|
+
const resolver = makeResolver(new Map([[1, clipA]]));
|
|
1119
|
+
|
|
1120
|
+
const jointE = world
|
|
1121
|
+
.spawn(
|
|
1122
|
+
{ component: Transform, data: defaultTransform },
|
|
1123
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1124
|
+
)
|
|
1125
|
+
.unwrap();
|
|
1126
|
+
const animE = world
|
|
1127
|
+
.spawn(
|
|
1128
|
+
{
|
|
1129
|
+
component: AnimationPlayer,
|
|
1130
|
+
data: {
|
|
1131
|
+
clips: [
|
|
1132
|
+
toShared<'AnimationClip'>(1),
|
|
1133
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1134
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1135
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1136
|
+
],
|
|
1137
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1138
|
+
weights: new Float32Array([-0.5, 0, 0, 0]),
|
|
1139
|
+
speeds: new Float32Array([1, 0, 0, 0]),
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
component: Skin,
|
|
1144
|
+
data: {
|
|
1145
|
+
skeleton: toShared<'SkeletonAsset'>(100),
|
|
1146
|
+
joints: new Uint32Array([jointE]),
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
{ component: Transform, data: defaultTransform },
|
|
1150
|
+
)
|
|
1151
|
+
.unwrap();
|
|
1152
|
+
|
|
1153
|
+
// Negative weight clamped -> w=0, slot skipped (no accumulator writes).
|
|
1154
|
+
// Transform should remain at its default (position = 0).
|
|
1155
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
1156
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1157
|
+
expect(tf.pos[0]).toBe(0);
|
|
1158
|
+
expect(tf.pos[1]).toBe(0);
|
|
1159
|
+
expect(tf.pos[2]).toBe(0);
|
|
1160
|
+
|
|
1161
|
+
// Read-back: weights[0] is still -0.5 (not written back per D-7).
|
|
1162
|
+
const apRes = world.get(animE, AnimationPlayer).unwrap() as unknown as {
|
|
1163
|
+
weights: Float32Array;
|
|
1164
|
+
};
|
|
1165
|
+
expect(apRes.weights[0]).toBe(-0.5);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('paused=true does not advance times but still blends by current times', () => {
|
|
1169
|
+
const world = new World();
|
|
1170
|
+
const sampT1 = makeSampler([0, 1], [0, 0, 0, 10, 0, 0], 'LINEAR');
|
|
1171
|
+
const sampT2 = makeSampler([0, 1], [0, 0, 0, 0, 10, 0], 'LINEAR');
|
|
1172
|
+
const clipA = makeClip(1, [
|
|
1173
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT1 },
|
|
1174
|
+
]);
|
|
1175
|
+
const clipB = makeClip(1, [
|
|
1176
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT2 },
|
|
1177
|
+
]);
|
|
1178
|
+
const resolver = makeResolver(
|
|
1179
|
+
new Map([
|
|
1180
|
+
[1, clipA],
|
|
1181
|
+
[2, clipB],
|
|
1182
|
+
]),
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
const jointE = world
|
|
1186
|
+
.spawn(
|
|
1187
|
+
{ component: Transform, data: defaultTransform },
|
|
1188
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1189
|
+
)
|
|
1190
|
+
.unwrap();
|
|
1191
|
+
const animE = world
|
|
1192
|
+
.spawn(
|
|
1193
|
+
{
|
|
1194
|
+
component: AnimationPlayer,
|
|
1195
|
+
data: {
|
|
1196
|
+
clips: [
|
|
1197
|
+
toShared<'AnimationClip'>(1),
|
|
1198
|
+
toShared<'AnimationClip'>(2),
|
|
1199
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1200
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1201
|
+
],
|
|
1202
|
+
times: new Float32Array([0.5, 0.2, 0, 0]),
|
|
1203
|
+
weights: new Float32Array([0.5, 0.5, 0, 0]),
|
|
1204
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1205
|
+
paused: true,
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
component: Skin,
|
|
1210
|
+
data: {
|
|
1211
|
+
skeleton: toShared<'SkeletonAsset'>(100),
|
|
1212
|
+
joints: new Uint32Array([jointE]),
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
{ component: Transform, data: defaultTransform },
|
|
1216
|
+
)
|
|
1217
|
+
.unwrap();
|
|
1218
|
+
|
|
1219
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
1220
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1221
|
+
// clipA at t=0.5: pos (5,0,0); clipB at t=0.2: pos (0,2,0)
|
|
1222
|
+
// blended: (2.5, 1.0, 0)
|
|
1223
|
+
expect(tf.pos[0]).toBeCloseTo(2.5);
|
|
1224
|
+
expect(tf.pos[1]).toBeCloseTo(1.0);
|
|
1225
|
+
|
|
1226
|
+
// Times unchanged by paused gate
|
|
1227
|
+
const apRes = world.get(animE, AnimationPlayer).unwrap() as unknown as {
|
|
1228
|
+
times: Float32Array;
|
|
1229
|
+
};
|
|
1230
|
+
expect(apRes.times[0]).toBeCloseTo(0.5);
|
|
1231
|
+
expect(apRes.times[1]).toBeCloseTo(0.2);
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('resolver cache miss skips slot without warning', () => {
|
|
1235
|
+
const world = new World();
|
|
1236
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 10, 10, 10], 'LINEAR');
|
|
1237
|
+
const clipA = makeClip(1, [
|
|
1238
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1239
|
+
]);
|
|
1240
|
+
const resolver = makeResolver(new Map([[1, clipA]]));
|
|
1241
|
+
// clip handle 2 is not in resolver — resolver returns undefined, slot skipped.
|
|
1242
|
+
|
|
1243
|
+
const jointE = world
|
|
1244
|
+
.spawn(
|
|
1245
|
+
{ component: Transform, data: defaultTransform },
|
|
1246
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1247
|
+
)
|
|
1248
|
+
.unwrap();
|
|
1249
|
+
world.spawn(
|
|
1250
|
+
{
|
|
1251
|
+
component: AnimationPlayer,
|
|
1252
|
+
data: {
|
|
1253
|
+
clips: [
|
|
1254
|
+
toShared<'AnimationClip'>(1),
|
|
1255
|
+
toShared<'AnimationClip'>(2),
|
|
1256
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1257
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1258
|
+
],
|
|
1259
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1260
|
+
weights: new Float32Array([1, 0.5, 0, 0]),
|
|
1261
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
component: Skin,
|
|
1266
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1267
|
+
},
|
|
1268
|
+
{ component: Transform, data: defaultTransform },
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1272
|
+
try {
|
|
1273
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
1274
|
+
// clip 1 at t=0.5 with weight=1: pos=(5,5,5). Slot 2 skipped silently.
|
|
1275
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1276
|
+
expect(tf.pos[0]).toBeCloseTo(5);
|
|
1277
|
+
expect(tf.pos[1]).toBeCloseTo(5);
|
|
1278
|
+
expect(tf.pos[2]).toBeCloseTo(5);
|
|
1279
|
+
// No warn emitted for resolver miss (AC-04: invalid handle / miss = silent skip).
|
|
1280
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
1281
|
+
} finally {
|
|
1282
|
+
warnSpy.mockRestore();
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
it('clips[i]=0 invalid handle skips slot silently', () => {
|
|
1287
|
+
const world = new World();
|
|
1288
|
+
const sampT = makeSampler([0, 1], [3, 0, 0, 3, 0, 0], 'LINEAR');
|
|
1289
|
+
const clipA = makeClip(1, [
|
|
1290
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1291
|
+
]);
|
|
1292
|
+
const resolver = makeResolver(new Map([[1, clipA]]));
|
|
1293
|
+
|
|
1294
|
+
const jointE = world
|
|
1295
|
+
.spawn(
|
|
1296
|
+
{ component: Transform, data: defaultTransform },
|
|
1297
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1298
|
+
)
|
|
1299
|
+
.unwrap();
|
|
1300
|
+
world.spawn(
|
|
1301
|
+
{
|
|
1302
|
+
component: AnimationPlayer,
|
|
1303
|
+
data: {
|
|
1304
|
+
clips: [
|
|
1305
|
+
toShared<'AnimationClip'>(1),
|
|
1306
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1307
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1308
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1309
|
+
],
|
|
1310
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1311
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
1312
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
component: Skin,
|
|
1317
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1318
|
+
},
|
|
1319
|
+
{ component: Transform, data: defaultTransform },
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
advanceAnimationPlayer(world, resolver, 0);
|
|
1323
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1324
|
+
// clipA at t=0 always gives pos=(3,0,0), regardless of other slots being 0.
|
|
1325
|
+
expect(tf.pos[0]).toBeCloseTo(3);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it('different durations each modulo independently without warning', () => {
|
|
1329
|
+
const world = new World();
|
|
1330
|
+
const sampTShort = makeSampler([0, 0.5], [0, 0, 0, 0.5, 0, 0], 'LINEAR');
|
|
1331
|
+
const sampTLong = makeSampler([0, 2], [0, 0, 0, 0, 2, 0], 'LINEAR');
|
|
1332
|
+
const clipShort = makeClip(0.5, [
|
|
1333
|
+
{
|
|
1334
|
+
targetId: deriveAnimationTargetId(['joint0']),
|
|
1335
|
+
property: 'translation',
|
|
1336
|
+
sampler: sampTShort,
|
|
1337
|
+
},
|
|
1338
|
+
]);
|
|
1339
|
+
const clipLong = makeClip(2, [
|
|
1340
|
+
{
|
|
1341
|
+
targetId: deriveAnimationTargetId(['joint0']),
|
|
1342
|
+
property: 'translation',
|
|
1343
|
+
sampler: sampTLong,
|
|
1344
|
+
},
|
|
1345
|
+
]);
|
|
1346
|
+
const resolver = makeResolver(
|
|
1347
|
+
new Map([
|
|
1348
|
+
[1, clipShort],
|
|
1349
|
+
[2, clipLong],
|
|
1350
|
+
]),
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
const jointE = world
|
|
1354
|
+
.spawn(
|
|
1355
|
+
{ component: Transform, data: defaultTransform },
|
|
1356
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1357
|
+
)
|
|
1358
|
+
.unwrap();
|
|
1359
|
+
world.spawn(
|
|
1360
|
+
{
|
|
1361
|
+
component: AnimationPlayer,
|
|
1362
|
+
data: {
|
|
1363
|
+
clips: [
|
|
1364
|
+
toShared<'AnimationClip'>(1),
|
|
1365
|
+
toShared<'AnimationClip'>(2),
|
|
1366
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1367
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1368
|
+
],
|
|
1369
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1370
|
+
weights: new Float32Array([0.5, 0.5, 0, 0]),
|
|
1371
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
component: Skin,
|
|
1376
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1377
|
+
},
|
|
1378
|
+
{ component: Transform, data: defaultTransform },
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
// Advance by 1.25 seconds.
|
|
1382
|
+
// clipShort (duration=0.5, looping): t = 0 + 1.25 = 1.25 -> 1.25 % 0.5 = 0.25 => pos=(0.25,0,0)
|
|
1383
|
+
// clipLong (duration=2, looping): t = 0 + 1.25 = 1.25 -> 1.25 % 2 = 1.25 => pos=(0,1.25,0)
|
|
1384
|
+
advanceAnimationPlayer(world, resolver, 1.25);
|
|
1385
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1386
|
+
// 0.5*(0.25,0,0) + 0.5*(0,1.25,0) => (0.125, 0.625, 0)
|
|
1387
|
+
expect(tf.pos[0]).toBeCloseTo(0.125);
|
|
1388
|
+
expect(tf.pos[1]).toBeCloseTo(0.625);
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
it('targetId mismatch skips channel (no crash)', () => {
|
|
1392
|
+
const world = new World();
|
|
1393
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 4, 4, 4], 'LINEAR');
|
|
1394
|
+
const clip = makeClip(1, [
|
|
1395
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1396
|
+
{
|
|
1397
|
+
targetId: deriveAnimationTargetId(['nonexistent-joint']),
|
|
1398
|
+
property: 'translation',
|
|
1399
|
+
sampler: sampT,
|
|
1400
|
+
},
|
|
1401
|
+
]);
|
|
1402
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
1403
|
+
|
|
1404
|
+
const jointE = world
|
|
1405
|
+
.spawn(
|
|
1406
|
+
{ component: Transform, data: defaultTransform },
|
|
1407
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1408
|
+
)
|
|
1409
|
+
.unwrap();
|
|
1410
|
+
world.spawn(
|
|
1411
|
+
{
|
|
1412
|
+
component: AnimationPlayer,
|
|
1413
|
+
data: {
|
|
1414
|
+
clips: [
|
|
1415
|
+
toShared<'AnimationClip'>(1),
|
|
1416
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1417
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1418
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1419
|
+
],
|
|
1420
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1421
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
1422
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
component: Skin,
|
|
1427
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1428
|
+
},
|
|
1429
|
+
{ component: Transform, data: defaultTransform },
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
// Should not crash — the nonexistent-joint channel is skipped.
|
|
1433
|
+
expect(() => advanceAnimationPlayer(world, resolver, 0.5)).not.toThrow();
|
|
1434
|
+
// The joint0 channel still applies.
|
|
1435
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1436
|
+
expect(tf.pos[0]).toBeCloseTo(2); // lerp(0,4,0.5) = 2
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
it('two-slot quat nlerp with sign fix (opposite hemispheres)', () => {
|
|
1440
|
+
const world = new World();
|
|
1441
|
+
// Clip 1: quat rotates 90 degrees around y (0, 0.707, 0, 0.707)
|
|
1442
|
+
// Clip 2: quat rotates -90 degrees around y (0, -0.707, 0, 0.707)
|
|
1443
|
+
// Dot = 0*0 + 0.707*(-0.707) + 0*0 + 0.707*0.707 = -0.5 + 0.5 = 0.
|
|
1444
|
+
// Sign fix will negate one for short-arc nlerp.
|
|
1445
|
+
const sampR1 = makeSampler(
|
|
1446
|
+
[0, 1],
|
|
1447
|
+
[0, Math.SQRT1_2, 0, Math.SQRT1_2, 0, Math.SQRT1_2, 0, Math.SQRT1_2],
|
|
1448
|
+
'LINEAR',
|
|
1449
|
+
);
|
|
1450
|
+
const sampR2 = makeSampler(
|
|
1451
|
+
[0, 1],
|
|
1452
|
+
[0, -Math.SQRT1_2, 0, Math.SQRT1_2, 0, -Math.SQRT1_2, 0, Math.SQRT1_2],
|
|
1453
|
+
'LINEAR',
|
|
1454
|
+
);
|
|
1455
|
+
const clipA = makeClip(1, [
|
|
1456
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'rotation', sampler: sampR1 },
|
|
1457
|
+
]);
|
|
1458
|
+
const clipB = makeClip(1, [
|
|
1459
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'rotation', sampler: sampR2 },
|
|
1460
|
+
]);
|
|
1461
|
+
const resolver = makeResolver(
|
|
1462
|
+
new Map([
|
|
1463
|
+
[1, clipA],
|
|
1464
|
+
[2, clipB],
|
|
1465
|
+
]),
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
const jointE = world
|
|
1469
|
+
.spawn(
|
|
1470
|
+
{ component: Transform, data: defaultTransform },
|
|
1471
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1472
|
+
)
|
|
1473
|
+
.unwrap();
|
|
1474
|
+
world.spawn(
|
|
1475
|
+
{
|
|
1476
|
+
component: AnimationPlayer,
|
|
1477
|
+
data: {
|
|
1478
|
+
clips: [
|
|
1479
|
+
toShared<'AnimationClip'>(1),
|
|
1480
|
+
toShared<'AnimationClip'>(2),
|
|
1481
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1482
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1483
|
+
],
|
|
1484
|
+
times: new Float32Array([0.5, 0.5, 0, 0]),
|
|
1485
|
+
weights: new Float32Array([0.5, 0.5, 0, 0]),
|
|
1486
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1487
|
+
},
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
component: Skin,
|
|
1491
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1492
|
+
},
|
|
1493
|
+
{ component: Transform, data: defaultTransform },
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
expect(() => advanceAnimationPlayer(world, resolver, 0.5)).not.toThrow();
|
|
1497
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1498
|
+
// nlerp of equal-weight opposite y-rotations should give identity-ish result.
|
|
1499
|
+
// quatX/quatZ should stay near 0, quatW stays near 1.
|
|
1500
|
+
expect(Math.abs(tf.quat[1])).toBeLessThan(0.01);
|
|
1501
|
+
expect(tf.quat[3]).toBeGreaterThan(0.99);
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
it('speed<0 reverses time (looping wraparound)', () => {
|
|
1505
|
+
const world = new World();
|
|
1506
|
+
const sampT = makeSampler([0, 2], [0, 0, 0, 2, 0, 0], 'LINEAR');
|
|
1507
|
+
const clip = makeClip(2, [
|
|
1508
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1509
|
+
]);
|
|
1510
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
1511
|
+
|
|
1512
|
+
const jointE = world
|
|
1513
|
+
.spawn(
|
|
1514
|
+
{ component: Transform, data: defaultTransform },
|
|
1515
|
+
{ component: Name, data: { value: 'joint0' } },
|
|
1516
|
+
)
|
|
1517
|
+
.unwrap();
|
|
1518
|
+
world.spawn(
|
|
1519
|
+
{
|
|
1520
|
+
component: AnimationPlayer,
|
|
1521
|
+
data: {
|
|
1522
|
+
clips: [
|
|
1523
|
+
toShared<'AnimationClip'>(1),
|
|
1524
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1525
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1526
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1527
|
+
],
|
|
1528
|
+
times: new Float32Array([0.5, 0, 0, 0]),
|
|
1529
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
1530
|
+
speeds: new Float32Array([-2, 1, 1, 1]),
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
component: Skin,
|
|
1535
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array([jointE]) },
|
|
1536
|
+
},
|
|
1537
|
+
{ component: Transform, data: defaultTransform },
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
advanceAnimationPlayer(world, resolver, 0.5); // t = 0.5 + (-2)*0.5 = -0.5 => -0.5 % 2 = -0.5 => -0.5+2=1.5
|
|
1541
|
+
const tf = world.get(jointE, Transform).unwrap();
|
|
1542
|
+
expect(tf.pos[0]).toBeCloseTo(1.5); // lerp(0,2,0.75) = 1.5
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it('entity with Skin but empty joints is skipped', () => {
|
|
1546
|
+
const world = new World();
|
|
1547
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 4, 4, 4], 'LINEAR');
|
|
1548
|
+
const clip = makeClip(1, [
|
|
1549
|
+
{ targetId: deriveAnimationTargetId(['joint0']), property: 'translation', sampler: sampT },
|
|
1550
|
+
]);
|
|
1551
|
+
const resolver = makeResolver(new Map([[1, clip]]));
|
|
1552
|
+
|
|
1553
|
+
world.spawn(
|
|
1554
|
+
{
|
|
1555
|
+
component: AnimationPlayer,
|
|
1556
|
+
data: {
|
|
1557
|
+
clips: [
|
|
1558
|
+
toShared<'AnimationClip'>(1),
|
|
1559
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1560
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1561
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1562
|
+
],
|
|
1563
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1564
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
1565
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1566
|
+
},
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
component: Skin,
|
|
1570
|
+
data: { skeleton: toShared<'SkeletonAsset'>(100), joints: new Uint32Array(0) },
|
|
1571
|
+
},
|
|
1572
|
+
{ component: Transform, data: defaultTransform },
|
|
1573
|
+
);
|
|
1574
|
+
|
|
1575
|
+
expect(() => advanceAnimationPlayer(world, resolver, 0.5)).not.toThrow();
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1580
|
+
// M3 / w9 — sample writes counting (per-joint single world.set, not
|
|
1581
|
+
// per-channel triple set). Spies on World.prototype.set to count calls
|
|
1582
|
+
// matching Transform writes on joint entities.
|
|
1583
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
1584
|
+
|
|
1585
|
+
describe('M3 per-joint sample writes count assertion', () => {
|
|
1586
|
+
it('world.set callCount = joint count (not joints x channels)', () => {
|
|
1587
|
+
const world = new World();
|
|
1588
|
+
// 2 joints + 3 channels (translation/rotation/scale) per clip
|
|
1589
|
+
// = 2 clips x 2 joints x 3 channels = 12 channels total.
|
|
1590
|
+
// If per-channel set were kept, callCount would be 6 (2 joints x 3 channels).
|
|
1591
|
+
// With per-joint accumulator: callCount = 2 (one per joint).
|
|
1592
|
+
const sampT = makeSampler([0, 1], [0, 0, 0, 3, 0, 0], 'LINEAR');
|
|
1593
|
+
const sampR = makeSampler([0, 1], [0, 0, 0, 1, 0, 0, 0, 1], 'LINEAR');
|
|
1594
|
+
const sampS = makeSampler([0, 1], [1, 1, 1, 2, 2, 2], 'LINEAR');
|
|
1595
|
+
const clipA = makeClip(1, [
|
|
1596
|
+
{ targetId: deriveAnimationTargetId(['jointA']), property: 'translation', sampler: sampT },
|
|
1597
|
+
{ targetId: deriveAnimationTargetId(['jointA']), property: 'rotation', sampler: sampR },
|
|
1598
|
+
{ targetId: deriveAnimationTargetId(['jointA']), property: 'scale', sampler: sampS },
|
|
1599
|
+
{ targetId: deriveAnimationTargetId(['jointB']), property: 'translation', sampler: sampT },
|
|
1600
|
+
{ targetId: deriveAnimationTargetId(['jointB']), property: 'rotation', sampler: sampR },
|
|
1601
|
+
{ targetId: deriveAnimationTargetId(['jointB']), property: 'scale', sampler: sampS },
|
|
1602
|
+
]);
|
|
1603
|
+
const resolver = makeResolver(new Map([[1, clipA]]));
|
|
1604
|
+
|
|
1605
|
+
const jointA = world
|
|
1606
|
+
.spawn(
|
|
1607
|
+
{ component: Transform, data: defaultTransform },
|
|
1608
|
+
{ component: Name, data: { value: 'jointA' } },
|
|
1609
|
+
)
|
|
1610
|
+
.unwrap();
|
|
1611
|
+
const jointB = world
|
|
1612
|
+
.spawn(
|
|
1613
|
+
{ component: Transform, data: defaultTransform },
|
|
1614
|
+
{ component: Name, data: { value: 'jointB' } },
|
|
1615
|
+
)
|
|
1616
|
+
.unwrap();
|
|
1617
|
+
|
|
1618
|
+
world.spawn(
|
|
1619
|
+
{
|
|
1620
|
+
component: AnimationPlayer,
|
|
1621
|
+
data: {
|
|
1622
|
+
clips: [
|
|
1623
|
+
toShared<'AnimationClip'>(1),
|
|
1624
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1625
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1626
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1627
|
+
],
|
|
1628
|
+
times: new Float32Array([0, 0, 0, 0]),
|
|
1629
|
+
weights: new Float32Array([1, 0, 0, 0]),
|
|
1630
|
+
speeds: new Float32Array([1, 1, 1, 1]),
|
|
1631
|
+
},
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
component: Skin,
|
|
1635
|
+
data: {
|
|
1636
|
+
skeleton: toShared<'SkeletonAsset'>(100),
|
|
1637
|
+
joints: new Uint32Array([jointA, jointB]),
|
|
1638
|
+
},
|
|
1639
|
+
},
|
|
1640
|
+
{ component: Transform, data: defaultTransform },
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
// Spy on world.set — count calls whose first arg is jointA or jointB.
|
|
1644
|
+
const jointHandles = new Set<number>([
|
|
1645
|
+
jointA as unknown as number,
|
|
1646
|
+
jointB as unknown as number,
|
|
1647
|
+
]);
|
|
1648
|
+
let setCount = 0;
|
|
1649
|
+
const origSet = world.set.bind(world);
|
|
1650
|
+
world.set = (...args: unknown[]) => {
|
|
1651
|
+
const entity = args[0] as unknown as number;
|
|
1652
|
+
if (jointHandles.has(entity)) setCount++;
|
|
1653
|
+
return origSet(...args) as ReturnType<typeof world.set>;
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
try {
|
|
1657
|
+
advanceAnimationPlayer(world, resolver, 0.5);
|
|
1658
|
+
// 2 joints, each receives exactly 1 set(Transform, partial).
|
|
1659
|
+
// NOT 6 (2 joints x 3 channels).
|
|
1660
|
+
expect(setCount).toBe(2);
|
|
1661
|
+
} finally {
|
|
1662
|
+
world.set = origSet;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
});
|
|
1666
|
+
}
|