@forgeax/engine-runtime 0.1.2
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/LICENSE +202 -0
- package/README.md +183 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/active-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/active-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts +2 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts.map +1 -0
- package/dist/__tests__/append-injection.test.d.ts +2 -0
- package/dist/__tests__/append-injection.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts.map +1 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts +2 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts +2 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts +2 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts.map +1 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts +2 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts +3 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts +2 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts +3 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -0
- package/dist/__tests__/components.test-d.d.ts +2 -0
- package/dist/__tests__/components.test-d.d.ts.map +1 -0
- package/dist/__tests__/components.unit.test.d.ts +2 -0
- package/dist/__tests__/components.unit.test.d.ts.map +1 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts +2 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mipmap-srgb-linear.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mipmap-srgb-linear.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts +2 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts.map +1 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts +2 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-published.d.ts +5 -0
- package/dist/__tests__/draw-published.d.ts.map +1 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts +8 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts +2 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts.map +1 -0
- package/dist/__tests__/error-exhaustive.test.d.ts +2 -0
- package/dist/__tests__/error-exhaustive.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts.map +1 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts +2 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-manifest.d.ts +131 -0
- package/dist/__tests__/fog-producer-manifest.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts +42 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts +5 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts +3 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +1 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +1 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/geometry.unit.test.d.ts +2 -0
- package/dist/__tests__/geometry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts +2 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +2 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +1 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts +2 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts +11 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts.map +1 -0
- package/dist/__tests__/helpers/primitive-world.d.ts +8 -0
- package/dist/__tests__/helpers/primitive-world.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts +15 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts +9 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts +6 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -0
- package/dist/__tests__/instances.test-d.d.ts +2 -0
- package/dist/__tests__/instances.test-d.d.ts.map +1 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts +2 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts.map +1 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts +2 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts.map +1 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts +2 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts.map +1 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts +2 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts.map +1 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights.unit.test.d.ts +2 -0
- package/dist/__tests__/lights.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts +2 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts.map +1 -0
- package/dist/__tests__/materials.unit.test.d.ts +2 -0
- package/dist/__tests__/materials.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts +2 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/name.unit.test.d.ts +2 -0
- package/dist/__tests__/name.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts +2 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-spec.test.d.ts +2 -0
- package/dist/__tests__/pipeline-spec.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +2 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts +2 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts +2 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts +2 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts.map +1 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts +2 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-mega.test.d.ts +2 -0
- package/dist/__tests__/render-system-mega.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts +2 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-result.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-result.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts +2 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-component.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-component.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts +2 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts +2 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts +83 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts +2 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts +14 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts +2 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts +2 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts +2 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts.map +1 -0
- package/dist/__tests__/ssao-bgl.test.d.ts +2 -0
- package/dist/__tests__/ssao-bgl.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-buffers.test.d.ts +2 -0
- package/dist/__tests__/ssao-buffers.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-data.test.d.ts +2 -0
- package/dist/__tests__/ssao-data.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-errors.test.d.ts +2 -0
- package/dist/__tests__/ssao-errors.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-manifest.test.d.ts +2 -0
- package/dist/__tests__/ssao-manifest.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-passes.test.d.ts +2 -0
- package/dist/__tests__/ssao-passes.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts +2 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts +2 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts +2 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts.map +1 -0
- package/dist/__tests__/systems.unit.test.d.ts +2 -0
- package/dist/__tests__/systems.unit.test.d.ts.map +1 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts +2 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +1 -0
- package/dist/__tests__/text.unit.test.d.ts +2 -0
- package/dist/__tests__/text.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts +2 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts +2 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts +2 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts +2 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts +2 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts +2 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts +2 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-validate.test.d.ts +2 -0
- package/dist/__tests__/tilemap-validate.test.d.ts.map +1 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts +2 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts.map +1 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts +2 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts.map +1 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts +2 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts +2 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts.map +1 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts +2 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts +2 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts.map +1 -0
- package/dist/__tests__/world-entity-key.test.d.ts +2 -0
- package/dist/__tests__/world-entity-key.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts +2 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts +2 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-nested.test.d.ts +2 -0
- package/dist/__tests__/world-scene-nested.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-override.test.d.ts +2 -0
- package/dist/__tests__/world-scene-override.test.d.ts.map +1 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts +2 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts.map +1 -0
- package/dist/asset-contributions.d.ts +3 -0
- package/dist/asset-contributions.d.ts.map +1 -0
- package/dist/asset-kind.d.ts +4 -0
- package/dist/asset-kind.d.ts.map +1 -0
- package/dist/collect-scene-asset.d.ts +16 -0
- package/dist/collect-scene-asset.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test-d.d.ts +2 -0
- package/dist/components/__tests__/camera.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test.d.ts +2 -0
- package/dist/components/__tests__/camera.test.d.ts.map +1 -0
- package/dist/components/__tests__/child-of.test.d.ts +2 -0
- package/dist/components/__tests__/child-of.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts +2 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts +2 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts +2 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts.map +1 -0
- package/dist/create-renderer-env-classify.d.ts +21 -0
- package/dist/create-renderer-env-classify.d.ts.map +1 -0
- package/dist/createRenderer.d.ts +3 -0
- package/dist/createRenderer.d.ts.map +1 -0
- package/dist/dev-import-transport.d.ts +24 -0
- package/dist/dev-import-transport.d.ts.map +1 -0
- package/dist/errors/environment.d.ts +2 -0
- package/dist/errors/environment.d.ts.map +1 -0
- package/dist/errors/sprite-animation.d.ts +3 -0
- package/dist/errors/sprite-animation.d.ts.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +814 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scene-asset-port.d.ts +17 -0
- package/dist/scene-asset-port.d.ts.map +1 -0
- package/dist/scene-collect-errors.d.ts +22 -0
- package/dist/scene-collect-errors.d.ts.map +1 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts +2 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts.map +1 -0
- package/dist/scene-utils/mount-override-fold.d.ts +16 -0
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -0
- package/dist/shadow-m3-calibrate.d.ts +40 -0
- package/dist/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/sprite-param-values.d.ts +62 -0
- package/dist/sprite-param-values.d.ts.map +1 -0
- package/dist/systems/__tests__/_xorshift.d.ts +28 -0
- package/dist/systems/__tests__/_xorshift.d.ts.map +1 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts +2 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts.map +1 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts +2 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts.map +1 -0
- package/dist/systems/sprite-animation-tick.d.ts +50 -0
- package/dist/systems/sprite-animation-tick.d.ts.map +1 -0
- package/dist/systems/tonemap.d.ts +27 -0
- package/dist/systems/tonemap.d.ts.map +1 -0
- package/dist/systems/transparent-sort.d.ts +51 -0
- package/dist/systems/transparent-sort.d.ts.map +1 -0
- package/dist/tilemap-register-validate.d.ts +21 -0
- package/dist/tilemap-register-validate.d.ts.map +1 -0
- package/package.json +100 -0
- package/src/__tests__/active-camera.unit.test.ts +193 -0
- package/src/__tests__/animation-graph-add.unit.test.ts +67 -0
- package/src/__tests__/animation-graph-asset-union.test-d.ts +83 -0
- package/src/__tests__/animation-graph-blend.unit.test.ts +66 -0
- package/src/__tests__/animation-graph-build.unit.test.ts +100 -0
- package/src/__tests__/animation-graph-clip-missing.unit.test.ts +62 -0
- package/src/__tests__/animation-graph-clip.unit.test.ts +77 -0
- package/src/__tests__/animation-graph-default-path.integration.test.ts +65 -0
- package/src/__tests__/animation-graph-describe.unit.test.ts +68 -0
- package/src/__tests__/animation-graph-errors.unit.test.ts +75 -0
- package/src/__tests__/animation-graph-nesting.unit.test.ts +84 -0
- package/src/__tests__/animation-graph-orthogonal-weight.unit.test.ts +98 -0
- package/src/__tests__/animation-graph-slot-fill.unit.test.ts +95 -0
- package/src/__tests__/animation-nslot.unit.test.ts +63 -0
- package/src/__tests__/animation-slot-length-guard.unit.test.ts +81 -0
- package/src/__tests__/append-injection.test.ts +105 -0
- package/src/__tests__/asi-world-baseline-export.unit.test.ts +173 -0
- package/src/__tests__/asi-world-ci-config.unit.test.ts +186 -0
- package/src/__tests__/asi-world-smoke.unit.test.ts +192 -0
- package/src/__tests__/asset-registry-scene.test-d.ts +49 -0
- package/src/__tests__/bind-group-cache-binding.test.ts +2513 -0
- package/src/__tests__/bind-group-cache-frame.test.ts +1511 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +295 -0
- package/src/__tests__/block-upload-math.unit.test.ts +192 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +151 -0
- package/src/__tests__/cache-false-eviction.test.ts +126 -0
- package/src/__tests__/cache-isolation-matrix.test.ts +182 -0
- package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +76 -0
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +183 -0
- package/src/__tests__/clamp-to-last.e2e.dawn.test.ts +325 -0
- package/src/__tests__/collect-scene-asset.test.ts +876 -0
- package/src/__tests__/collect-subtree.unit.test.ts +76 -0
- package/src/__tests__/collect-transient-children-absent.test.ts +154 -0
- package/src/__tests__/collect-transient-roundtrip.test.ts +245 -0
- package/src/__tests__/collect-transient-skip.test.ts +165 -0
- package/src/__tests__/components.test-d.ts +236 -0
- package/src/__tests__/components.unit.test.ts +2665 -0
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +378 -0
- package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -0
- package/src/__tests__/createRenderer-dispose.test.ts +416 -0
- package/src/__tests__/createRenderer-requiredFeatures.unit.test.ts +134 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +59 -0
- package/src/__tests__/createRenderer.unit.test.ts +231 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +393 -0
- package/src/__tests__/dawn/ibl-compose.dawn.test.ts +151 -0
- package/src/__tests__/dawn/ibl-modules-compose.dawn.test.ts +223 -0
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +241 -0
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +611 -0
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +443 -0
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +313 -0
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +304 -0
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +64 -0
- package/src/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.ts +106 -0
- package/src/__tests__/dawn/mesh-update.dawn.test.ts +342 -0
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +117 -0
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +567 -0
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +251 -0
- package/src/__tests__/dawn/tonemap.dawn.test.ts +107 -0
- package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +291 -0
- package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +204 -0
- package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +122 -0
- package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +99 -0
- package/src/__tests__/demo-sortscope-migration.unit.test.ts +115 -0
- package/src/__tests__/derive-bgl-integration.test.ts +398 -0
- package/src/__tests__/derive-render-data-skin.unit.test.ts +90 -0
- package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +204 -0
- package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +297 -0
- package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +271 -0
- package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +321 -0
- package/src/__tests__/draw-published.ts +10 -0
- package/src/__tests__/environment-error-exhaustive.test-d.ts +28 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +35 -0
- package/src/__tests__/error-exhaustive.test.ts +220 -0
- package/src/__tests__/extract-frames-graceful-degradation.test.ts +298 -0
- package/src/__tests__/extract-frames-merge.test.ts +643 -0
- package/src/__tests__/extract-frames-reset.test.ts +158 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +92 -0
- package/src/__tests__/fog-producer-manifest.ts +162 -0
- package/src/__tests__/fog-producer-pixel-probe.ts +263 -0
- package/src/__tests__/fog-producer-pixel.browser.test.ts +26 -0
- package/src/__tests__/fog-producer-pixel.dawn.test.ts +55 -0
- package/src/__tests__/fog-producer-real-fixture.ts +805 -0
- package/src/__tests__/fog-producer-vfx-effect.ts +272 -0
- package/src/__tests__/fog-scene-asset.integration.test.ts +123 -0
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +28 -0
- package/src/__tests__/fullscreen-post-bgl-depth.test.ts +131 -0
- package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +84 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +320 -0
- package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +107 -0
- package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +292 -0
- package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +358 -0
- package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +254 -0
- package/src/__tests__/geometry.unit.test.ts +2265 -0
- package/src/__tests__/glyph-tilemap-worldid.test.ts +146 -0
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +388 -0
- package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +207 -0
- package/src/__tests__/gpu-resource.test.ts +1206 -0
- package/src/__tests__/hdrp-bgl-slots.browser.test.ts +50 -0
- package/src/__tests__/hdrp-buffers.unit.test.ts +70 -0
- package/src/__tests__/hdrp-execute-filter.unit.test.ts +200 -0
- package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +64 -0
- package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +139 -0
- package/src/__tests__/hdrp-ssao-boundary.test.ts +237 -0
- package/src/__tests__/hdrp-ssao-falsification.test.ts +289 -0
- package/src/__tests__/hello-tilemap-smoke-upgrade.unit.test.ts +147 -0
- package/src/__tests__/helpers/mock-shader-registry.ts +139 -0
- package/src/__tests__/helpers/primitive-world.ts +91 -0
- package/src/__tests__/helpers/register-runtime-components.ts +69 -0
- package/src/__tests__/helpers/register-scene-components.ts +10 -0
- package/src/__tests__/helpers/render-asset-port.ts +65 -0
- package/src/__tests__/helpers/scene-asset-port.ts +23 -0
- package/src/__tests__/helpers/tilemap-assets.ts +29 -0
- package/src/__tests__/instances.test-d.ts +61 -0
- package/src/__tests__/light-attenuation-formula.test.ts +171 -0
- package/src/__tests__/light-casters-9-light.browser.test.ts +880 -0
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +163 -0
- package/src/__tests__/light-snapshot.test-d.ts +84 -0
- package/src/__tests__/lights-vec-negative.unit.test.ts +52 -0
- package/src/__tests__/lights.unit.test.ts +2936 -0
- package/src/__tests__/m3-vec-negative.unit.test.ts +45 -0
- package/src/__tests__/m5-browser-dawn-sweep.integration.test.ts +8 -0
- package/src/__tests__/material-derived-coordinates.integration.test.ts +100 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +49 -0
- package/src/__tests__/materials.unit.test.ts +271 -0
- package/src/__tests__/mesh-ssbo-grow.unit.test.ts +244 -0
- package/src/__tests__/mip-gate.unit.test.ts +79 -0
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +406 -0
- package/src/__tests__/multi-uv-8set.e2e.dawn.test.ts +296 -0
- package/src/__tests__/name.unit.test.ts +123 -0
- package/src/__tests__/node-shims.d.ts +33 -0
- package/src/__tests__/pass-selector.unit.test.ts +102 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +204 -0
- package/src/__tests__/pipeline-builder-msaa.unit.test.ts +224 -0
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +316 -0
- package/src/__tests__/pipeline-layout-selection.test.ts +298 -0
- package/src/__tests__/pipeline-spec.test.ts +1061 -0
- package/src/__tests__/pipeline.unit.test.ts +6974 -0
- package/src/__tests__/point-light-shadow.browser.test.ts +278 -0
- package/src/__tests__/point-light-shadow.dawn.test.ts +822 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +716 -0
- package/src/__tests__/post-process-error-codes.unit.test.ts +158 -0
- package/src/__tests__/post-process-params-component.unit.test.ts +153 -0
- package/src/__tests__/post-process-params-extract.unit.test.ts +100 -0
- package/src/__tests__/post-process-reads-narrowing.test-d.ts +52 -0
- package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +230 -0
- package/src/__tests__/postprocess-camera-consistency.test.ts +378 -0
- package/src/__tests__/profiler-assembly.integration.test.ts +74 -0
- package/src/__tests__/propagate-transforms.dawn.test.ts +253 -0
- package/src/__tests__/record-worldid-isolation.test.ts +247 -0
- package/src/__tests__/recover-error-exhaustive.test-d.ts +35 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +245 -0
- package/src/__tests__/render-feature-assembly.test-d.ts +28 -0
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +124 -0
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +582 -0
- package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +602 -0
- package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +264 -0
- package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +361 -0
- package/src/__tests__/render-feature-recover.integration.test.ts +142 -0
- package/src/__tests__/render-feature-runtime-surface.test-d.ts +45 -0
- package/src/__tests__/render-feature-zero-feature.browser.test.ts +152 -0
- package/src/__tests__/render-pipeline-trivial.dawn.test.ts +525 -0
- package/src/__tests__/render-schema-consumer.integration.test.ts +25 -0
- package/src/__tests__/render-schema-owner-boundary.unit.test.ts +26 -0
- package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +172 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +322 -0
- package/src/__tests__/render-system-extract.test.ts +1399 -0
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +288 -0
- package/src/__tests__/render-system-fold-fallback.unit.test.ts +148 -0
- package/src/__tests__/render-system-fold-material.unit.test.ts +145 -0
- package/src/__tests__/render-system-fold-metrics.unit.test.ts +170 -0
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +166 -0
- package/src/__tests__/render-system-fold-scene-instances.unit.test.ts +51 -0
- package/src/__tests__/render-system-fold.dawn.test.ts +306 -0
- package/src/__tests__/render-system-mega.test.ts +1578 -0
- package/src/__tests__/render-system-owner-split-contract.test.ts +200 -0
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +684 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +464 -0
- package/src/__tests__/render-system-record.test.ts +1505 -0
- package/src/__tests__/render-system-record.unit.test.ts +270 -0
- package/src/__tests__/render-system-skin-bg.unit.test.ts +147 -0
- package/src/__tests__/renderer-consumer.test-d.ts +5 -0
- package/src/__tests__/renderer-health.unit.test.ts +935 -0
- package/src/__tests__/renderer-lifecycle.integration.test.ts +29 -0
- package/src/__tests__/renderer-recover.unit.test.ts +1075 -0
- package/src/__tests__/renderer-result.test-d.ts +26 -0
- package/src/__tests__/renderer-surface.unit.test.ts +416 -0
- package/src/__tests__/resolve-depth-only-view.test.ts +173 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +273 -0
- package/src/__tests__/rhi-null-noop-behavior.unit.test.ts +196 -0
- package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +182 -0
- package/src/__tests__/roots-to-scene-asset.test.ts +605 -0
- package/src/__tests__/runtime-authority-scan.unit.test.ts +19 -0
- package/src/__tests__/runtime-domain-residue.unit.test.ts +13 -0
- package/src/__tests__/scene-equirect-roundtrip.test.ts +33 -0
- package/src/__tests__/scene-instance-bridge.integration.test.ts +8 -0
- package/src/__tests__/scene-instance-component.test.ts +45 -0
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +66 -0
- package/src/__tests__/serialize-scene-asset.test.ts +336 -0
- package/src/__tests__/shadow-caster-empty-schema.test.ts +83 -0
- package/src/__tests__/shadow-csm-atlas.test.ts +178 -0
- package/src/__tests__/shadow-csm-blend.dawn.test.ts +230 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +334 -0
- package/src/__tests__/shadow-csm-caster-membership.test.ts +232 -0
- package/src/__tests__/shadow-csm-component.test.ts +286 -0
- package/src/__tests__/shadow-csm-extract.test.ts +334 -0
- package/src/__tests__/shadow-csm-orthoz.test.ts +151 -0
- package/src/__tests__/shadow-csm-pssm.test.ts +142 -0
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +198 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +358 -0
- package/src/__tests__/shadow-csm-temporal.browser.test.ts +145 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +419 -0
- package/src/__tests__/shadow-csm-ubo.dawn.test.ts +293 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +300 -0
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +85 -0
- package/src/__tests__/shadow-csm-viewport.test.ts +74 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +615 -0
- package/src/__tests__/shadow-m1.dawn.test.ts +105 -0
- package/src/__tests__/shadow-m2.dawn.test.ts +394 -0
- package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +248 -0
- package/src/__tests__/shadow-m3-calibrate.ts +203 -0
- package/src/__tests__/shadow-m3.dawn.test.ts +288 -0
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +64 -0
- package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +264 -0
- package/src/__tests__/shadow-quality-metrics.dawn.test.ts +324 -0
- package/src/__tests__/skin-error-exhaustive.test-d.ts +74 -0
- package/src/__tests__/skin-errors.unit.test.ts +10 -0
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +23 -0
- package/src/__tests__/skin-parented-double-transform.test.ts +268 -0
- package/src/__tests__/skylight-lazy-projection.test.ts +359 -0
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +43 -0
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +470 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +141 -0
- package/src/__tests__/sprite-lit-extract-branch.test.ts +117 -0
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +123 -0
- package/src/__tests__/sprite-param-values.test-d.ts +104 -0
- package/src/__tests__/ssao-bgl.test.ts +347 -0
- package/src/__tests__/ssao-buffers.test.ts +211 -0
- package/src/__tests__/ssao-data.test.ts +121 -0
- package/src/__tests__/ssao-errors.test.ts +162 -0
- package/src/__tests__/ssao-manifest.test.ts +109 -0
- package/src/__tests__/ssao-passes.test.ts +1229 -0
- package/src/__tests__/ssao-pipelines.test.ts +275 -0
- package/src/__tests__/ssao-uniform-layout.test.ts +178 -0
- package/src/__tests__/static-rig.integration.test.ts +9 -0
- package/src/__tests__/stencil-outline-pixel.dawn.test.ts +295 -0
- package/src/__tests__/swap-chain-format.unit.test.ts +117 -0
- package/src/__tests__/systems.unit.test.ts +9957 -0
- package/src/__tests__/temporal-off-path.unit.test.ts +69 -0
- package/src/__tests__/text.unit.test.ts +466 -0
- package/src/__tests__/tilemap-childed-despawn.test.ts +468 -0
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +314 -0
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +80 -0
- package/src/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.ts +424 -0
- package/src/__tests__/tilemap-chunk-extract.test.ts +236 -0
- package/src/__tests__/tilemap-chunk-extract.unit.test.ts +383 -0
- package/src/__tests__/tilemap-chunk-y-sort-bench.unit.test.ts +109 -0
- package/src/__tests__/tilemap-dirty-rebuild.test.ts +300 -0
- package/src/__tests__/tilemap-material-cache.test.ts +137 -0
- package/src/__tests__/tilemap-multi-atlas.test.ts +237 -0
- package/src/__tests__/tilemap-owner-boundary.unit.test.ts +63 -0
- package/src/__tests__/tilemap-region-pack.test.ts +153 -0
- package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +274 -0
- package/src/__tests__/tilemap-uv-inset.test.ts +204 -0
- package/src/__tests__/tilemap-validate.test.ts +201 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +274 -0
- package/src/__tests__/transient-fields-collect.test.ts +203 -0
- package/src/__tests__/transitional-authoring.integration.test.ts +7 -0
- package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +227 -0
- package/src/__tests__/variant-selection.unit.test.ts +21 -0
- package/src/__tests__/vertex-attribute-layout.test.ts +324 -0
- package/src/__tests__/video-cache-regression.unit.test.ts +186 -0
- package/src/__tests__/video-extract-bindgroup.dawn.test.ts +450 -0
- package/src/__tests__/view-ubo-layout.browser.test.ts +183 -0
- package/src/__tests__/world-entity-key.test.ts +92 -0
- package/src/__tests__/world-scene-despawn.test.ts +132 -0
- package/src/__tests__/world-scene-instantiate.test.ts +228 -0
- package/src/__tests__/world-scene-nested.test.ts +230 -0
- package/src/__tests__/world-scene-override.test.ts +199 -0
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +107 -0
- package/src/asset-contributions.ts +48 -0
- package/src/asset-kind.ts +7 -0
- package/src/collect-scene-asset.ts +689 -0
- package/src/components/__tests__/camera.test-d.ts +57 -0
- package/src/components/__tests__/camera.test.ts +95 -0
- package/src/components/__tests__/child-of.test.ts +35 -0
- package/src/components/__tests__/sprite-animation.test-d.ts +160 -0
- package/src/components/__tests__/sprite-instances.unit.test.ts +108 -0
- package/src/components/__tests__/sprite-region-override.test-d.ts +101 -0
- package/src/components/__tests__/tile-layer.test-d.ts +33 -0
- package/src/components/__tests__/tile-layer.unit.test.ts +125 -0
- package/src/components/__tests__/tilemap.test-d.ts +35 -0
- package/src/components/__tests__/transform-vec-schema.test.ts +121 -0
- package/src/create-renderer-env-classify.ts +110 -0
- package/src/createRenderer.ts +61 -0
- package/src/dev-import-transport.ts +97 -0
- package/src/errors/environment.ts +6 -0
- package/src/errors/sprite-animation.ts +5 -0
- package/src/index.ts +442 -0
- package/src/scene-asset-port.ts +32 -0
- package/src/scene-collect-errors.ts +30 -0
- package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +183 -0
- package/src/scene-utils/mount-override-fold.ts +217 -0
- package/src/shadow-m3-calibrate.ts +171 -0
- package/src/sprite-param-values.ts +61 -0
- package/src/systems/__tests__/_xorshift.ts +83 -0
- package/src/systems/__tests__/argsort-in-place.test.ts +155 -0
- package/src/systems/__tests__/transparent-sort.bench.ts +218 -0
- package/src/systems/sprite-animation-tick.ts +302 -0
- package/src/systems/tonemap.ts +68 -0
- package/src/systems/transparent-sort.ts +434 -0
- package/src/tilemap-register-validate.ts +100 -0
|
@@ -0,0 +1,2665 @@
|
|
|
1
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
|
|
2
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
3
|
+
//
|
|
4
|
+
// Source files (N=21):
|
|
5
|
+
// - packages/runtime/src/__tests__/children.test.ts
|
|
6
|
+
// - packages/runtime/src/__tests__/components.test.ts
|
|
7
|
+
// - packages/runtime/src/__tests__/hierarchy-components.test.ts
|
|
8
|
+
// - packages/runtime/src/__tests__/inspector-frustum-stats.test.ts
|
|
9
|
+
// - packages/runtime/src/__tests__/layer-component.test.ts
|
|
10
|
+
// - packages/runtime/src/__tests__/mesh-renderer-pickable.test.ts
|
|
11
|
+
// - packages/runtime/src/__tests__/pick.test.ts
|
|
12
|
+
// - packages/runtime/src/__tests__/register-inspector.test.ts
|
|
13
|
+
// - packages/runtime/src/__tests__/relationship-migration-regression.test.ts
|
|
14
|
+
// - packages/runtime/src/__tests__/scene-defaults.test.ts
|
|
15
|
+
// - packages/runtime/src/__tests__/sort-key-component.test.ts
|
|
16
|
+
// - packages/runtime/src/components/__tests__/animation-player.test.ts
|
|
17
|
+
// - packages/runtime/src/components/__tests__/camera.test.ts
|
|
18
|
+
// - packages/runtime/src/components/__tests__/layer.test.ts
|
|
19
|
+
// - packages/runtime/src/components/__tests__/mesh-renderer.test.ts
|
|
20
|
+
// - packages/runtime/src/components/__tests__/skybox-background.test.ts
|
|
21
|
+
// - packages/runtime/src/components/__tests__/sort-key.test.ts
|
|
22
|
+
// - packages/runtime/src/components/__tests__/sprite-components-schema.test.ts
|
|
23
|
+
// - packages/runtime/src/components/__tests__/sprite-playback-mode.test.ts
|
|
24
|
+
// - packages/runtime/src/components/__tests__/transform.test.ts
|
|
25
|
+
// - packages/runtime/src/__tests__/mesh-renderer-multi-material.test.ts
|
|
26
|
+
//
|
|
27
|
+
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
28
|
+
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
29
|
+
|
|
30
|
+
import { AnimationPlayer } from '@forgeax/engine-animation';
|
|
31
|
+
import {
|
|
32
|
+
type Component,
|
|
33
|
+
defineComponent,
|
|
34
|
+
ENTITY_NULL_RAW,
|
|
35
|
+
type EntityHandle,
|
|
36
|
+
World,
|
|
37
|
+
} from '@forgeax/engine-ecs';
|
|
38
|
+
import { componentDefinition, componentId, componentSchema } from '@forgeax/engine-ecs/internal';
|
|
39
|
+
import {
|
|
40
|
+
GlyphText,
|
|
41
|
+
SPRITE_PLAYBACK_MODE_CLAMP,
|
|
42
|
+
SPRITE_PLAYBACK_MODE_LOOP,
|
|
43
|
+
SpriteAnimation,
|
|
44
|
+
type SpritePlaybackMode,
|
|
45
|
+
SpriteRegionOverride,
|
|
46
|
+
spritePlaybackModeFromU32,
|
|
47
|
+
Tilemap,
|
|
48
|
+
} from '@forgeax/engine-render/authoring';
|
|
49
|
+
import {
|
|
50
|
+
ANTIALIAS_NONE,
|
|
51
|
+
BLOOM_DISABLED,
|
|
52
|
+
CAMERA_PROJECTION_ORTHOGRAPHIC,
|
|
53
|
+
CAMERA_PROJECTION_PERSPECTIVE,
|
|
54
|
+
Camera,
|
|
55
|
+
DirectionalLight,
|
|
56
|
+
extractFrame,
|
|
57
|
+
Layer,
|
|
58
|
+
MeshFilter,
|
|
59
|
+
MeshRenderer,
|
|
60
|
+
orthographic,
|
|
61
|
+
perspective,
|
|
62
|
+
prepareExtractContext,
|
|
63
|
+
SceneInstance,
|
|
64
|
+
SKYBOX_MODE_CUBEMAP,
|
|
65
|
+
SkyboxBackground,
|
|
66
|
+
type SkyboxMode,
|
|
67
|
+
SortKey,
|
|
68
|
+
skyboxModeFromF32,
|
|
69
|
+
TONEMAP_NONE,
|
|
70
|
+
TONEMAP_REINHARD,
|
|
71
|
+
TONEMAP_REINHARD_EXTENDED,
|
|
72
|
+
tonemapFromF32,
|
|
73
|
+
} from '@forgeax/engine-render/internal';
|
|
74
|
+
import * as SceneOwner from '@forgeax/engine-scene';
|
|
75
|
+
import { ChildOf, Children, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
76
|
+
import type { Handle, LocalEntityId, SceneAsset, SceneEntity } from '@forgeax/engine-types';
|
|
77
|
+
import { toShared } from '@forgeax/engine-types';
|
|
78
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
79
|
+
import { createTestRenderAssetOwner } from './helpers/render-asset-port';
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
// --- from children.test.ts ---
|
|
83
|
+
// w13 - Children component schema migration to `array<entity>` (M3).
|
|
84
|
+
//
|
|
85
|
+
// Locks AC-05 (requirements.md): the `entities: 'array<entity>'` field
|
|
86
|
+
// resolves to a fresh `Uint32Array` snapshot at the get-site, and the
|
|
87
|
+
// snapshot length reflects the live element count after
|
|
88
|
+
// `world.push(parent, Children, 'entities', child)` /
|
|
89
|
+
// `world.pop(parent, Children, 'entities')` mutation routes through the
|
|
90
|
+
// M0/M1/M2 BufferPool slot + sidecar count column infrastructure.
|
|
91
|
+
//
|
|
92
|
+
// feat-20260515-buffer-array-vocab-collapse M3 / w17: rewritten for the
|
|
93
|
+
// collapsed-vocab API surface -- the `VarArrayView<Entity>` wrapper was
|
|
94
|
+
// retired (M2 / w10). AI users mutate via the three `world` commands and
|
|
95
|
+
// read through the read-only `Uint32Array` snapshot; `snap.length` is the
|
|
96
|
+
// live count, `snap[i]` is the packed Entity u32.
|
|
97
|
+
//
|
|
98
|
+
// Test path note: project convention places runtime tests under
|
|
99
|
+
// `src/__tests__/` (TS rootDir = `./src`). The plan-tasks.json target path
|
|
100
|
+
// `packages/runtime/src/components/__tests__/children.test.ts` lives outside
|
|
101
|
+
// rootDir and would not be picked up by `tsc -b`. The test file therefore
|
|
102
|
+
// lands under `src/__tests__/` to match the existing
|
|
103
|
+
// `hierarchy-components.test.ts` convention.
|
|
104
|
+
|
|
105
|
+
describe('w13 - Children { entities: array<entity> } schema (AC-05)', () => {
|
|
106
|
+
it('componentSchema(Children).entities is the array<entity> keyword (no legacy `count` field)', () => {
|
|
107
|
+
expect(Children.name).toBe('Children');
|
|
108
|
+
expect(Object.keys(componentSchema(Children)).length).toBe(1);
|
|
109
|
+
expect((componentSchema(Children) as Record<string, unknown>).entities).toBe('array<entity>');
|
|
110
|
+
expect((componentSchema(Children) as Record<string, unknown>).count).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('world.get(e, Children).entities is a Uint32Array snapshot with length reflecting initial payload', () => {
|
|
114
|
+
const world = new World();
|
|
115
|
+
world.components.register(ChildOf).unwrap();
|
|
116
|
+
world.components.register(Children).unwrap();
|
|
117
|
+
const parent = world.spawn().unwrap();
|
|
118
|
+
const a = world.spawn({ component: ChildOf, data: { parent } }).unwrap();
|
|
119
|
+
const b = world.spawn({ component: ChildOf, data: { parent } }).unwrap();
|
|
120
|
+
const got = world.get(parent, Children).unwrap();
|
|
121
|
+
// `Children` is an engine-maintained materialized target. Its snapshot
|
|
122
|
+
// is read-only; source-side ChildOf writes are the only mutation route.
|
|
123
|
+
expect(got.entities.length).toBe(2);
|
|
124
|
+
expect(got.entities[0]).toBe(a);
|
|
125
|
+
expect(got.entities[1]).toBe(b);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
// --- from components.test.ts ---
|
|
132
|
+
// w7 - 5 component schema runtime registration + multi-component spawn (TDD red).
|
|
133
|
+
//
|
|
134
|
+
// Locks plan-strategy 7.2 naming + requirements IN-1 schema field set:
|
|
135
|
+
// Transform: pos:[f32x3] + quat:[f32x4] + scale:[f32x3] = 10 f32
|
|
136
|
+
// MeshFilter: assetHandle:'shared<MeshAsset>' (u32)
|
|
137
|
+
// MeshRenderer: materials:'array<shared<MaterialAsset>>' (u32)
|
|
138
|
+
// Camera: fov:f32 + aspect:f32 + near:f32 + far:f32 = 4 f32
|
|
139
|
+
// DirectionalLight: direction:[f32x3] + color:[f32x3] + intensity:f32 = 7 f32
|
|
140
|
+
//
|
|
141
|
+
// Storage shape: forgeax ECS columns store scalar fields as flat scalars
|
|
142
|
+
// (`Float32Array` / `Uint32Array`) and vector / quaternion fields as inline
|
|
143
|
+
// fixed-length array columns (`array<f32, N>`, e.g. `pos: [x, y, z]`).
|
|
144
|
+
// The 5-component schema picks names that AI users see in `world.get(e, T)`
|
|
145
|
+
// and `data: {...}` shapes.
|
|
146
|
+
//
|
|
147
|
+
// charter mapping: proposition 1 (single import + LSP hover discoverability) +
|
|
148
|
+
// proposition 3 (machine-readable schema > prose) + proposition 5 (consistent
|
|
149
|
+
// abstraction: components are flat-scalar SoA columns, not nested objects).
|
|
150
|
+
|
|
151
|
+
describe('w7 - 5 component schemas register through defineComponent', () => {
|
|
152
|
+
it('Transform has pos/quat/scale inline array fields + world array<f32,16>', () => {
|
|
153
|
+
expect(Transform.name).toBe('Transform');
|
|
154
|
+
expect(Object.keys(componentSchema(Transform)).length).toBe(4);
|
|
155
|
+
// pos: inline [x, y, z]
|
|
156
|
+
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
157
|
+
// quat: inline [x, y, z, w] quaternion
|
|
158
|
+
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
159
|
+
// scale: inline [x, y, z]
|
|
160
|
+
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
161
|
+
// world: resolved mat4 (column-major 16 floats)
|
|
162
|
+
expect(componentSchema(Transform).world).toBe('array<f32, 16>');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('MeshFilter has 1 shared<MeshAsset> field (assetHandle; M5 / w14)', () => {
|
|
166
|
+
expect(MeshFilter.name).toBe('MeshFilter');
|
|
167
|
+
expect(Object.keys(componentSchema(MeshFilter)).length).toBe(1);
|
|
168
|
+
expect(componentSchema(MeshFilter).assetHandle).toBe('shared<MeshAsset>');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('MeshRenderer has 1 field (materials; feat-20260608 M2 / w7 multi-material array)', () => {
|
|
172
|
+
expect(MeshRenderer.name).toBe('MeshRenderer');
|
|
173
|
+
expect(Object.keys(componentSchema(MeshRenderer)).length).toBe(1);
|
|
174
|
+
const schemaRecord = componentSchema(MeshRenderer) as Record<string, string>;
|
|
175
|
+
expect(schemaRecord.materials).toBe('array<shared<MaterialAsset>>');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('Camera has 20 fields (17 f32 + historyVersion u32 + clearColor array<f32,4> + autoAspect bool)', () => {
|
|
179
|
+
expect(Camera.name).toBe('Camera');
|
|
180
|
+
expect(Object.keys(componentSchema(Camera)).length).toBe(20);
|
|
181
|
+
expect(componentSchema(Camera).fov).toBe('f32');
|
|
182
|
+
expect(componentSchema(Camera).aspect).toBe('f32');
|
|
183
|
+
expect(componentSchema(Camera).near).toBe('f32');
|
|
184
|
+
expect(componentSchema(Camera).far).toBe('f32');
|
|
185
|
+
expect(componentSchema(Camera).projection).toBe('f32');
|
|
186
|
+
expect(componentSchema(Camera).left).toBe('f32');
|
|
187
|
+
expect(componentSchema(Camera).right).toBe('f32');
|
|
188
|
+
expect(componentSchema(Camera).bottom).toBe('f32');
|
|
189
|
+
expect(componentSchema(Camera).top).toBe('f32');
|
|
190
|
+
expect(componentSchema(Camera).historyVersion).toBe('u32');
|
|
191
|
+
// feat-20260519-tonemap-reinhard-mvp / M1 / T-M1.2: AC-01 + D-1.
|
|
192
|
+
expect(componentSchema(Camera).tonemap).toBe('f32');
|
|
193
|
+
expect(componentSchema(Camera).exposure).toBe('f32');
|
|
194
|
+
expect(componentSchema(Camera).whitePoint).toBe('f32');
|
|
195
|
+
expect(componentSchema(Camera).antialias).toBe('f32');
|
|
196
|
+
// feat-20260531-bloom-first-declarative-render-graph-pass / w2.
|
|
197
|
+
expect(componentSchema(Camera).bloom).toBe('f32');
|
|
198
|
+
expect(componentSchema(Camera).bloomThreshold).toBe('f32');
|
|
199
|
+
expect(componentSchema(Camera).bloomIntensity).toBe('f32');
|
|
200
|
+
expect(componentSchema(Camera).bloomBlurRadius).toBe('f32');
|
|
201
|
+
// feat-20260709 M3: clear-color quartet collapsed into one inline
|
|
202
|
+
// array<f32,4> column (clearColor); per-axis scalars are gone.
|
|
203
|
+
expect(componentSchema(Camera).clearColor).toBe('array<f32, 4>');
|
|
204
|
+
expect('clearR' in componentSchema(Camera)).toBe(false);
|
|
205
|
+
expect('clearG' in componentSchema(Camera)).toBe(false);
|
|
206
|
+
expect('clearB' in componentSchema(Camera)).toBe(false);
|
|
207
|
+
expect('clearA' in componentSchema(Camera)).toBe(false);
|
|
208
|
+
// feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
|
|
209
|
+
// opt-out flag (bool column tier, not f32).
|
|
210
|
+
expect(componentSchema(Camera).autoAspect).toBe('bool');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('DirectionalLight has 12 fields: 3 light + castShadow bool + 8 merged shadow f32', () => {
|
|
214
|
+
// feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
|
|
215
|
+
// shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
|
|
216
|
+
// feat-20260709 M2: direction/color collapsed from 6 per-axis scalars to
|
|
217
|
+
// two array<f32,3> columns (3 light fields: direction + color + intensity).
|
|
218
|
+
expect(DirectionalLight.name).toBe('DirectionalLight');
|
|
219
|
+
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(12);
|
|
220
|
+
// 3 light fields
|
|
221
|
+
expect(componentSchema(DirectionalLight).direction).toBe('array<f32, 3>');
|
|
222
|
+
expect(componentSchema(DirectionalLight).color).toBe('array<f32, 3>');
|
|
223
|
+
expect(componentSchema(DirectionalLight).intensity).toBe('f32');
|
|
224
|
+
// shadow gate + 8 merged shadow fields
|
|
225
|
+
expect(componentSchema(DirectionalLight).castShadow).toBe('bool');
|
|
226
|
+
expect(componentSchema(DirectionalLight).mapSize).toBe('f32');
|
|
227
|
+
expect(componentSchema(DirectionalLight).cascadeCount).toBe('f32');
|
|
228
|
+
expect(componentSchema(DirectionalLight).splitLambda).toBe('f32');
|
|
229
|
+
expect(componentSchema(DirectionalLight).cascadeBlend).toBe('f32');
|
|
230
|
+
expect(componentSchema(DirectionalLight).depthBias).toBe('f32');
|
|
231
|
+
expect(componentSchema(DirectionalLight).normalBias).toBe('f32');
|
|
232
|
+
expect(componentSchema(DirectionalLight).shadowDistance).toBe('f32');
|
|
233
|
+
expect(componentSchema(DirectionalLight).pcfKernelSize).toBe('f32');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('all 5 components are frozen tokens with owner-assigned identities', () => {
|
|
237
|
+
expect(Object.isFrozen(Transform)).toBe(true);
|
|
238
|
+
expect(Object.isFrozen(MeshFilter)).toBe(true);
|
|
239
|
+
expect(Object.isFrozen(MeshRenderer)).toBe(true);
|
|
240
|
+
expect(Object.isFrozen(Camera)).toBe(true);
|
|
241
|
+
expect(Object.isFrozen(DirectionalLight)).toBe(true);
|
|
242
|
+
for (const t of [Transform, MeshFilter, MeshRenderer, Camera, DirectionalLight]) {
|
|
243
|
+
expect(typeof componentId(t)).toBe('number');
|
|
244
|
+
expect(componentId(t)).toBeGreaterThanOrEqual(0);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('w7 - world.spawn({ component, data }) accepts each of the 5 components', () => {
|
|
250
|
+
it('spawn Transform succeeds and stores the pos/quat/scale values', () => {
|
|
251
|
+
const world = new World();
|
|
252
|
+
const e = world
|
|
253
|
+
.spawn({
|
|
254
|
+
component: Transform,
|
|
255
|
+
data: { pos: [1, 2, 3], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
256
|
+
})
|
|
257
|
+
.unwrap();
|
|
258
|
+
const r = world.get(e, Transform).unwrap();
|
|
259
|
+
expect(Array.from(r.pos)).toEqual([1, 2, 3]);
|
|
260
|
+
expect(Array.from(r.quat)).toEqual([0, 0, 0, 1]);
|
|
261
|
+
expect(Array.from(r.scale)).toEqual([1, 1, 1]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('spawn MeshFilter with HANDLE_CUBE-style u32 succeeds', () => {
|
|
265
|
+
const world = new World();
|
|
266
|
+
const e = world
|
|
267
|
+
.spawn({
|
|
268
|
+
component: MeshFilter,
|
|
269
|
+
data: { assetHandle: 1 as Handle<'MeshAsset', 'shared'> },
|
|
270
|
+
})
|
|
271
|
+
.unwrap();
|
|
272
|
+
const r = world.get(e, MeshFilter).unwrap();
|
|
273
|
+
expect(r.assetHandle).toBe(1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('spawn MeshRenderer with a branded Handle<MaterialAsset> succeeds', () => {
|
|
277
|
+
const world = new World();
|
|
278
|
+
const e = world
|
|
279
|
+
.spawn({
|
|
280
|
+
component: MeshRenderer,
|
|
281
|
+
data: {
|
|
282
|
+
materials: [7 as Handle<'MaterialAsset', 'shared'>],
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
.unwrap();
|
|
286
|
+
const r = world.get(e, MeshRenderer).unwrap();
|
|
287
|
+
expect(r.materials[0]).toBe(7);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('spawn Camera with perspective parameters succeeds', () => {
|
|
291
|
+
const world = new World();
|
|
292
|
+
const e = world
|
|
293
|
+
.spawn({
|
|
294
|
+
component: Camera,
|
|
295
|
+
data: {
|
|
296
|
+
fov: Math.PI / 4,
|
|
297
|
+
aspect: 16 / 9,
|
|
298
|
+
near: 0.1,
|
|
299
|
+
far: 100,
|
|
300
|
+
projection: 0,
|
|
301
|
+
left: -1,
|
|
302
|
+
right: 1,
|
|
303
|
+
bottom: -1,
|
|
304
|
+
top: 1,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
.unwrap();
|
|
308
|
+
const r = world.get(e, Camera).unwrap();
|
|
309
|
+
expect(r.fov).toBeCloseTo(Math.PI / 4, 5);
|
|
310
|
+
expect(r.far).toBe(100);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('spawn DirectionalLight succeeds', () => {
|
|
314
|
+
const world = new World();
|
|
315
|
+
const e = world
|
|
316
|
+
.spawn({
|
|
317
|
+
component: DirectionalLight,
|
|
318
|
+
data: {
|
|
319
|
+
direction: [-0.5, -1, -0.3],
|
|
320
|
+
color: [1, 1, 1],
|
|
321
|
+
intensity: 1,
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
.unwrap();
|
|
325
|
+
const r = world.get(e, DirectionalLight).unwrap();
|
|
326
|
+
expect(r.intensity).toBe(1);
|
|
327
|
+
expect(r.color[0]).toBe(1);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('multi-component spawn (Transform + MeshFilter + MeshRenderer + Camera) targets a single archetype', () => {
|
|
331
|
+
const world = new World();
|
|
332
|
+
const e = world
|
|
333
|
+
.spawn(
|
|
334
|
+
{
|
|
335
|
+
component: Transform,
|
|
336
|
+
data: {
|
|
337
|
+
pos: [0, 0, 0],
|
|
338
|
+
quat: [0, 0, 0, 1],
|
|
339
|
+
scale: [1, 1, 1],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{ component: MeshFilter, data: { assetHandle: 1 as Handle<'MeshAsset', 'shared'> } },
|
|
343
|
+
{
|
|
344
|
+
component: MeshRenderer,
|
|
345
|
+
data: {
|
|
346
|
+
materials: [5 as Handle<'MaterialAsset', 'shared'>],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
component: Camera,
|
|
351
|
+
data: {
|
|
352
|
+
fov: Math.PI / 4,
|
|
353
|
+
aspect: 1,
|
|
354
|
+
near: 0.1,
|
|
355
|
+
far: 100,
|
|
356
|
+
projection: 0,
|
|
357
|
+
left: -1,
|
|
358
|
+
right: 1,
|
|
359
|
+
bottom: -1,
|
|
360
|
+
top: 1,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
.unwrap();
|
|
365
|
+
expect(world.get(e, Transform).unwrap().pos[0]).toBe(0);
|
|
366
|
+
expect(world.get(e, MeshFilter).unwrap().assetHandle).toBe(1);
|
|
367
|
+
expect(world.get(e, MeshRenderer).unwrap().materials[0]).toBe(5);
|
|
368
|
+
expect(world.get(e, Camera).unwrap().far).toBe(100);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('w7 - 5 component schemas remain unique tokens (no double registration / no name collision)', () => {
|
|
373
|
+
it('5 components have 5 distinct componentId values', () => {
|
|
374
|
+
const ids = new Set([
|
|
375
|
+
componentId(Transform),
|
|
376
|
+
componentId(MeshFilter),
|
|
377
|
+
componentId(MeshRenderer),
|
|
378
|
+
componentId(Camera),
|
|
379
|
+
componentId(DirectionalLight),
|
|
380
|
+
]);
|
|
381
|
+
expect(ids.size).toBe(5);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('user-defined component with the same name shape compiles independently', () => {
|
|
385
|
+
// Using the same user-style schema to confirm the 5 engine tokens do not
|
|
386
|
+
// collide with downstream user code at the registry level. The user token
|
|
387
|
+
// is a fresh allocation; it does not interfere with the 5 engine tokens.
|
|
388
|
+
const UserPos = defineComponent('UserPos', { x: { type: 'f32' } });
|
|
389
|
+
expect(componentId(UserPos)).not.toBe(componentId(Transform));
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
{
|
|
395
|
+
// --- from hierarchy-components.test.ts ---
|
|
396
|
+
// w5 - ChildOf / Children hierarchy component schema tests.
|
|
397
|
+
//
|
|
398
|
+
// Locks plan-strategy §D-P2 + requirements AC-12 schema field set:
|
|
399
|
+
// ChildOf: { parent: 'entity' }
|
|
400
|
+
// (M5 / w18 - migrated from raw 'ref' to schema-vocab
|
|
401
|
+
// 'entity' keyword; the parent column carries the encoded
|
|
402
|
+
// Entity and the ECS does not bottom out dangling refs on
|
|
403
|
+
// read - consumers check liveness themselves)
|
|
404
|
+
// Children: { entities: 'array<entity>' } = variable-length
|
|
405
|
+
// forward-list of child entity u32s (M3 / w13 of
|
|
406
|
+
// feat-20260514-ecs-children-instances-managed-buffer-array
|
|
407
|
+
// migrated from the legacy `count: 'u32'` advisory marker
|
|
408
|
+
// to the real ECS-managed array storage path; AC-05).
|
|
409
|
+
// The dedicated assertion lives in children.test.ts; this
|
|
410
|
+
// file keeps an integration-shape spawn smoke for the
|
|
411
|
+
// archetype-with-Children path.
|
|
412
|
+
//
|
|
413
|
+
// feat-20260601 M4: GlobalTransform is retired. The resolved world transform
|
|
414
|
+
// is the `Transform.world` mat4 column; propagate + hierarchy compose coverage
|
|
415
|
+
// lives in render-system-extract.test.ts (Transform.world parent x child).
|
|
416
|
+
//
|
|
417
|
+
// charter mapping: proposition 2 (Bevy ChildOf/Children industry analog) +
|
|
418
|
+
// proposition 3 (machine-readable schema > prose: grep componentSchema(ChildOf) /
|
|
419
|
+
// componentSchema(Children) recovers shape).
|
|
420
|
+
|
|
421
|
+
describe('w5 - ChildOf / Children register through defineComponent', () => {
|
|
422
|
+
it('ChildOf has 1 entity field (parent) (M5 / w18)', () => {
|
|
423
|
+
expect(ChildOf.name).toBe('ChildOf');
|
|
424
|
+
expect(Object.keys(componentSchema(ChildOf)).length).toBe(1);
|
|
425
|
+
expect(componentSchema(ChildOf).parent).toBe('entity');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('Children has 1 array<entity> field (entities; M3 / w13 migration from legacy `count: u32`)', () => {
|
|
429
|
+
expect(Children.name).toBe('Children');
|
|
430
|
+
expect(Object.keys(componentSchema(Children)).length).toBe(1);
|
|
431
|
+
expect((componentSchema(Children) as Record<string, unknown>).entities).toBe('array<entity>');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('Transform carries the resolved world mat4 column (array<f32, 16>)', () => {
|
|
435
|
+
// feat-20260601 M4: the world transform lives on Transform.world, not a
|
|
436
|
+
// separate GlobalTransform component. The 3 local-TRS inline array
|
|
437
|
+
// columns (pos/quat/scale) plus the `world: array<f32, 16>` resolved
|
|
438
|
+
// mat4 are the full shape.
|
|
439
|
+
const tKeys = Object.keys(componentSchema(Transform));
|
|
440
|
+
expect(tKeys).toContain('world');
|
|
441
|
+
expect(componentSchema(Transform).world).toBe('array<f32, 16>');
|
|
442
|
+
expect(componentSchema(Transform).pos).toBe('array<f32, 3>');
|
|
443
|
+
expect(componentSchema(Transform).quat).toBe('array<f32, 4>');
|
|
444
|
+
expect(componentSchema(Transform).scale).toBe('array<f32, 3>');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('ChildOf / Children frozen tokens (schema immutable)', () => {
|
|
448
|
+
expect(Object.isFrozen(componentSchema(ChildOf))).toBe(true);
|
|
449
|
+
expect(Object.isFrozen(componentSchema(Children))).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('spawn with ChildOf carries encoded Entity u32 through ref field round-trip', () => {
|
|
453
|
+
const world = new World();
|
|
454
|
+
const root = world
|
|
455
|
+
.spawn({
|
|
456
|
+
component: Transform,
|
|
457
|
+
data: {
|
|
458
|
+
pos: [0, 0, 0],
|
|
459
|
+
quat: [0, 0, 0, 1],
|
|
460
|
+
scale: [1, 1, 1],
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
.unwrap();
|
|
464
|
+
const child = world
|
|
465
|
+
.spawn(
|
|
466
|
+
{
|
|
467
|
+
component: Transform,
|
|
468
|
+
data: {
|
|
469
|
+
pos: [1, 0, 0],
|
|
470
|
+
quat: [0, 0, 0, 1],
|
|
471
|
+
scale: [1, 1, 1],
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{ component: ChildOf, data: { parent: root } },
|
|
475
|
+
)
|
|
476
|
+
.unwrap();
|
|
477
|
+
const r = world.get(child, ChildOf);
|
|
478
|
+
expect(r.ok).toBe(true);
|
|
479
|
+
if (!r.ok) return;
|
|
480
|
+
expect(r.value.parent).toBe(root);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('spawn with Transform + Children creates archetype with both components', () => {
|
|
484
|
+
const world = new World();
|
|
485
|
+
const root = world
|
|
486
|
+
.spawn(
|
|
487
|
+
{
|
|
488
|
+
component: Transform,
|
|
489
|
+
data: {
|
|
490
|
+
pos: [0, 0, 0],
|
|
491
|
+
quat: [0, 0, 0, 1],
|
|
492
|
+
scale: [1, 1, 1],
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{ component: Children, data: { entities: new Uint32Array([]) as never } },
|
|
496
|
+
)
|
|
497
|
+
.unwrap();
|
|
498
|
+
const rt = world.get(root, Transform);
|
|
499
|
+
expect(rt.ok).toBe(true);
|
|
500
|
+
if (!rt.ok) return;
|
|
501
|
+
expect(rt.value.quat[3]).toBe(1);
|
|
502
|
+
expect(rt.value.scale[0]).toBe(1);
|
|
503
|
+
const rc = world.get(root, Children);
|
|
504
|
+
expect(rc.ok).toBe(true);
|
|
505
|
+
if (!rc.ok) return;
|
|
506
|
+
// The variable-length entities snapshot starts empty for a fresh spawn
|
|
507
|
+
// with a zero-length payload; the snapshot length equals the live count.
|
|
508
|
+
// A dedicated push/pop suite lives in children.test.ts.
|
|
509
|
+
expect(rc.value.entities.length).toBe(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Silence unused-import lint if defineComponent elsewhere; keep the symbol
|
|
513
|
+
// reference so the test file double-checks barrel wiring side-effects.
|
|
514
|
+
it('defineComponent type is re-exported through @forgeax/engine-ecs', () => {
|
|
515
|
+
expect(typeof defineComponent).toBe('function');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
{
|
|
521
|
+
// --- from layer-component.test.ts ---
|
|
522
|
+
// w07 - Layer component schema spawn (TDD red).
|
|
523
|
+
//
|
|
524
|
+
// feat-20260520-2d-sprite-layer-mvp M-2 w07 / requirements AC-06 + AC-18 path (1).
|
|
525
|
+
//
|
|
526
|
+
// Coverage:
|
|
527
|
+
// - 4 explicit Layer values: {-100, 0, 100, 1000} (background / default /
|
|
528
|
+
// foreground / UI). i32 schema must preserve negative values via two's
|
|
529
|
+
// complement (no schema-layer mutate). plan-strategy §7 M-2 acceptance
|
|
530
|
+
// anchor AC-06.
|
|
531
|
+
// - 1 fallback case: entity spawned without Layer must round-trip via the
|
|
532
|
+
// existing 4-layer spawn fallback chain (feat-20260517-spawn-default-
|
|
533
|
+
// fallback) — value defaults to 0 (i32 scalar default), no fifth fallback
|
|
534
|
+
// layer introduced (AC-18 path (1)).
|
|
535
|
+
//
|
|
536
|
+
// charter mapping: F1 (single-import barrel discovery) + P3 (explicit defaults
|
|
537
|
+
// — i32 zero default surfaces as a read-back value, not undefined) +
|
|
538
|
+
// P4 (consistent abstraction — Layer is a generic ECS render component, not a
|
|
539
|
+
// 2D-only special; 3D entities may also carry Layer).
|
|
540
|
+
//
|
|
541
|
+
// TDD red: imports Layer from the runtime barrel; w11 implements the
|
|
542
|
+
// component file + barrel re-export to turn this green.
|
|
543
|
+
|
|
544
|
+
describe('w07 - Layer = defineComponent("Layer", { value: "i32" })', () => {
|
|
545
|
+
it('has schema { value: "i32" } (1 i32 field)', () => {
|
|
546
|
+
expect(Layer.name).toBe('Layer');
|
|
547
|
+
expect(Object.keys(componentSchema(Layer)).length).toBe(1);
|
|
548
|
+
expect(componentSchema(Layer).value).toBe('i32');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("spawn Layer { value: -100 } (background) round-trips with two's complement", () => {
|
|
552
|
+
const world = new World();
|
|
553
|
+
const e = world.spawn({ component: Layer, data: { value: -100 } }).unwrap();
|
|
554
|
+
const r = world.get(e, Layer).unwrap();
|
|
555
|
+
expect(r.value).toBe(-100);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('spawn Layer { value: 0 } (default / game layer) round-trips', () => {
|
|
559
|
+
const world = new World();
|
|
560
|
+
const e = world.spawn({ component: Layer, data: { value: 0 } }).unwrap();
|
|
561
|
+
const r = world.get(e, Layer).unwrap();
|
|
562
|
+
expect(r.value).toBe(0);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('spawn Layer { value: 100 } (foreground) round-trips', () => {
|
|
566
|
+
const world = new World();
|
|
567
|
+
const e = world.spawn({ component: Layer, data: { value: 100 } }).unwrap();
|
|
568
|
+
const r = world.get(e, Layer).unwrap();
|
|
569
|
+
expect(r.value).toBe(100);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it('spawn Layer { value: 1000 } (UI) round-trips', () => {
|
|
573
|
+
const world = new World();
|
|
574
|
+
const e = world.spawn({ component: Layer, data: { value: 1000 } }).unwrap();
|
|
575
|
+
const r = world.get(e, Layer).unwrap();
|
|
576
|
+
expect(r.value).toBe(1000);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('spawn payload omitting value falls through 4-layer chain to schema default 0 (AC-18 path 1)', () => {
|
|
580
|
+
// Entity spawned without Layer payload — the existing 4-layer spawn
|
|
581
|
+
// fallback chain (feat-20260517-spawn-default-fallback) fills i32
|
|
582
|
+
// scalar default 0. No fifth fallback layer is introduced for this
|
|
583
|
+
// feat (AC-18 path 1). The call below uses an empty `data: {}` payload
|
|
584
|
+
// (no value field). The spawn must succeed (Result.ok) and the
|
|
585
|
+
// round-tripped value must be exactly 0.
|
|
586
|
+
const world = new World();
|
|
587
|
+
const spawnResult = world.spawn({ component: Layer, data: {} });
|
|
588
|
+
expect(spawnResult.ok).toBe(true);
|
|
589
|
+
const e = spawnResult.unwrap();
|
|
590
|
+
const r = world.get(e, Layer).unwrap();
|
|
591
|
+
expect(r.value).toBe(0);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
{
|
|
597
|
+
// --- from relationship-migration-regression.test.ts ---
|
|
598
|
+
// relationship-migration-regression.test.ts -- unit project (t21, M4).
|
|
599
|
+
// Zero-regression + auto-sync verification for the ChildOf/Children
|
|
600
|
+
// relationship migration (t20).
|
|
601
|
+
//
|
|
602
|
+
// Two orthogonal guarantees after ChildOf gained its `relationship` block:
|
|
603
|
+
// AC-25 reader path unchanged -- propagateTransforms still walks ChildOf.parent
|
|
604
|
+
// upward and composes child.Transform.world = parent.world x
|
|
605
|
+
// compose(child local). The migration must NOT alter this output.
|
|
606
|
+
// AC-25 mirror auto-maintained -- spawning / removing / reparenting ChildOf
|
|
607
|
+
// now auto-updates the parent's Children.entities list (the OOS-10 manual-
|
|
608
|
+
// sync contract is retired). This is the new behaviour the migration adds.
|
|
609
|
+
//
|
|
610
|
+
// CPU-only (no GPU access): propagateTransforms is pure matrix math and the
|
|
611
|
+
// bidirectional sync is pure ECS bookkeeping, so this runs in the unit layer
|
|
612
|
+
// alongside the pixel-readback propagate coverage in the dawn layer (AC-04).
|
|
613
|
+
|
|
614
|
+
function transformData(x: number, y: number, z: number) {
|
|
615
|
+
return { pos: [x, y, z], quat: [0, 0, 0, 1], scale: [1, 1, 1] };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
describe('relationship-migration-regression (t21 / AC-25 / AC-26)', () => {
|
|
619
|
+
it('ChildOf remains the writable relationship source token', () => {
|
|
620
|
+
expect(ChildOf.name).toBe('ChildOf');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('propagateTransforms reader path unchanged: child composes parent x local translation', () => {
|
|
624
|
+
const world = new World();
|
|
625
|
+
const parent = world.spawn({ component: Transform, data: transformData(10, 0, 0) }).unwrap();
|
|
626
|
+
const child = world
|
|
627
|
+
.spawn(
|
|
628
|
+
{ component: Transform, data: transformData(1, 2, 3) },
|
|
629
|
+
{ component: ChildOf, data: { parent } },
|
|
630
|
+
)
|
|
631
|
+
.unwrap();
|
|
632
|
+
|
|
633
|
+
const r = propagateTransforms(world);
|
|
634
|
+
expect(r.ok).toBe(true);
|
|
635
|
+
|
|
636
|
+
// feat-20260601: the resolved world transform lives on Transform.world
|
|
637
|
+
// (a 16-float column-major mat4); the translation column is m[12,13,14].
|
|
638
|
+
const t = world.get(child, Transform);
|
|
639
|
+
expect(t.ok).toBe(true);
|
|
640
|
+
if (!t.ok) return;
|
|
641
|
+
const w = t.value.world;
|
|
642
|
+
// parent translates +10 X, child local +1/+2/+3 -> world 11/2/3.
|
|
643
|
+
expect(w[12]).toBeCloseTo(11, 5);
|
|
644
|
+
expect(w[13]).toBeCloseTo(2, 5);
|
|
645
|
+
expect(w[14]).toBeCloseTo(3, 5);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('spawning ChildOf auto-appends the child to parent.Children.entities (OOS-10 retired)', () => {
|
|
649
|
+
const world = new World();
|
|
650
|
+
// Mirror must be registered before the holder is used (M2 contract:
|
|
651
|
+
// the relationship hook resolves Children by name from the registry).
|
|
652
|
+
const parent = world.spawn({ component: Transform, data: transformData(0, 0, 0) }).unwrap();
|
|
653
|
+
const a = world
|
|
654
|
+
.spawn(
|
|
655
|
+
{ component: Transform, data: transformData(0, 0, 0) },
|
|
656
|
+
{ component: ChildOf, data: { parent } },
|
|
657
|
+
)
|
|
658
|
+
.unwrap();
|
|
659
|
+
const b = world
|
|
660
|
+
.spawn(
|
|
661
|
+
{ component: Transform, data: transformData(0, 0, 0) },
|
|
662
|
+
{ component: ChildOf, data: { parent } },
|
|
663
|
+
)
|
|
664
|
+
.unwrap();
|
|
665
|
+
|
|
666
|
+
const snap = world.get(parent, Children);
|
|
667
|
+
expect(snap.ok).toBe(true);
|
|
668
|
+
if (!snap.ok) return;
|
|
669
|
+
expect(Array.from(snap.value.entities)).toEqual([a, b]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('removing ChildOf prunes the child from parent.Children.entities', () => {
|
|
673
|
+
const world = new World();
|
|
674
|
+
const parent = world.spawn({ component: Transform, data: transformData(0, 0, 0) }).unwrap();
|
|
675
|
+
const a = world
|
|
676
|
+
.spawn(
|
|
677
|
+
{ component: Transform, data: transformData(0, 0, 0) },
|
|
678
|
+
{ component: ChildOf, data: { parent } },
|
|
679
|
+
)
|
|
680
|
+
.unwrap();
|
|
681
|
+
const b = world
|
|
682
|
+
.spawn(
|
|
683
|
+
{ component: Transform, data: transformData(0, 0, 0) },
|
|
684
|
+
{ component: ChildOf, data: { parent } },
|
|
685
|
+
)
|
|
686
|
+
.unwrap();
|
|
687
|
+
|
|
688
|
+
world.removeComponent(a, ChildOf).unwrap();
|
|
689
|
+
|
|
690
|
+
const snap = world.get(parent, Children);
|
|
691
|
+
expect(snap.ok).toBe(true);
|
|
692
|
+
if (!snap.ok) return;
|
|
693
|
+
expect(Array.from(snap.value.entities)).toEqual([b]);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('reparent (exclusive re-add) moves the child between parent Children lists', () => {
|
|
697
|
+
const world = new World();
|
|
698
|
+
const oldParent = world
|
|
699
|
+
.spawn({ component: Transform, data: transformData(0, 0, 0) })
|
|
700
|
+
.unwrap();
|
|
701
|
+
const newParent = world
|
|
702
|
+
.spawn({ component: Transform, data: transformData(0, 0, 0) })
|
|
703
|
+
.unwrap();
|
|
704
|
+
const child = world
|
|
705
|
+
.spawn(
|
|
706
|
+
{ component: Transform, data: transformData(0, 0, 0) },
|
|
707
|
+
{ component: ChildOf, data: { parent: oldParent } },
|
|
708
|
+
)
|
|
709
|
+
.unwrap();
|
|
710
|
+
|
|
711
|
+
// Atomic detach-then-attach reparent via the M3 Commands surface.
|
|
712
|
+
world.reparent(child, newParent, ChildOf, { parent: newParent }).unwrap();
|
|
713
|
+
|
|
714
|
+
const oldSnap = world.get(oldParent, Children);
|
|
715
|
+
const newSnap = world.get(newParent, Children);
|
|
716
|
+
expect(oldSnap.ok && newSnap.ok).toBe(true);
|
|
717
|
+
if (!oldSnap.ok || !newSnap.ok) return;
|
|
718
|
+
expect(Array.from(oldSnap.value.entities)).toEqual([]);
|
|
719
|
+
expect(Array.from(newSnap.value.entities)).toEqual([child]);
|
|
720
|
+
|
|
721
|
+
// Reader path: ChildOf.parent now points at newParent.
|
|
722
|
+
const co = world.get(child, ChildOf);
|
|
723
|
+
expect(co.ok).toBe(true);
|
|
724
|
+
if (!co.ok) return;
|
|
725
|
+
expect(co.value.parent).toBe(newParent);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// feat-20260602 M2 / w7: relationship-mirror validation moved into
|
|
730
|
+
// `defineComponent` (define-time fail-fast). The runtime ChildOf holder
|
|
731
|
+
// declares `mirror: 'Children'`, so the component barrel must export Children
|
|
732
|
+
// (the mirror) before ChildOf (the holder) -- otherwise `defineComponent`
|
|
733
|
+
// throws RelationshipMirrorComponentNotRegisteredError while the package is
|
|
734
|
+
// being evaluated. This block is the load-bearing guard for that barrel
|
|
735
|
+
// ordering: a clean dynamic import proves the module-evaluation order
|
|
736
|
+
// satisfies the mirror-before-holder define-time contract.
|
|
737
|
+
describe('relationship-migration-regression (feat-20260602 M2 / w7 barrel order)', () => {
|
|
738
|
+
it('imports @forgeax/engine-runtime without throwing on module evaluation', async () => {
|
|
739
|
+
const mod = await import('@forgeax/engine-scene');
|
|
740
|
+
expect(mod.Children).toBeDefined();
|
|
741
|
+
expect(mod.ChildOf).toBeDefined();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('runtime scene barrel exposes both relationship tokens', async () => {
|
|
745
|
+
const { ChildOf, Children } = await import('@forgeax/engine-scene');
|
|
746
|
+
expect(ChildOf.name).toBe('ChildOf');
|
|
747
|
+
expect(Children.name).toBe('Children');
|
|
748
|
+
expect((componentSchema(Children) as Record<string, string>).entities).toBe('array<entity>');
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
{
|
|
754
|
+
// --- from scene-defaults.test.ts ---
|
|
755
|
+
// scene-defaults.test - default-value 4-layer fallback (w20 TDD red).
|
|
756
|
+
//
|
|
757
|
+
// Coverage map (anchored to requirements §AC-07 / §AC-11 / §AC-12 +
|
|
758
|
+
// §default-value mechanism + plan-strategy §D-P3 / §D-P4):
|
|
759
|
+
//
|
|
760
|
+
// (a) AC-07 layer 1 explicit Scene value:
|
|
761
|
+
// SceneEntity.components.Transform.pos = [1.5, 0, 0] -> instantiate ->
|
|
762
|
+
// pos === [1.5, 0, 0]. Explicit Scene value beats every layer 2/3
|
|
763
|
+
// default (no setOverride written, so overrides() stays empty).
|
|
764
|
+
//
|
|
765
|
+
// (b) AC-11 layer 2 component-level defaults (D-P3 add-only minor):
|
|
766
|
+
// defineComponent('Transform',
|
|
767
|
+
// { pos: { type: 'array<f32, 3>', default: new Float32Array([0, 7, 0]) } });
|
|
768
|
+
// SceneEntity does NOT write pos -> instantiate -> pos === [0, 7, 0].
|
|
769
|
+
//
|
|
770
|
+
// (c) AC-12 layer 3 TS type defaults + NULL_ENTITY sentinel:
|
|
771
|
+
// Three layers all silent on pos -> pos === [0, 0, 0] (TS array<f32, N>
|
|
772
|
+
// zero-fill default).
|
|
773
|
+
// 'entity' typed field with no source value -> NULL_ENTITY sentinel
|
|
774
|
+
// (raw u32 = 0xffffffff per ENTITY_NULL_RAW SSOT). Layer 3 silent --
|
|
775
|
+
// MUST NOT throw 'scene-default-missing'; charter tension explicitly
|
|
776
|
+
// captured in requirements + plan-strategy.
|
|
777
|
+
//
|
|
778
|
+
// (d) layer-boundary discipline:
|
|
779
|
+
// Known schema field name + missing value -> walks layer 1 -> 2 -> 3
|
|
780
|
+
// chain (this test file).
|
|
781
|
+
// Unknown field name (typo 'pozX') -> ajv fail-fast rejects upstream
|
|
782
|
+
// at pack-schema validate, never reaching instantiate -- that path is
|
|
783
|
+
// owned by AC-08 in packages/pack scene-schema tests (M1 / w4 covers
|
|
784
|
+
// it). The boundary is "known name + missing value = silent fallback;
|
|
785
|
+
// unknown name = fail-fast" -- this file asserts the silent half so
|
|
786
|
+
// the contract is bidirectionally pinned.
|
|
787
|
+
//
|
|
788
|
+
// w22 (ImplementerAgent M4) lands the impl in
|
|
789
|
+
// packages/ecs/src/scene-instance-container.ts; this file is the red phase.
|
|
790
|
+
|
|
791
|
+
// M3 ECS-fication: scene-instance container API replaced; tests use
|
|
792
|
+
// world.instantiateScene + registerSceneAsset (allocUniqueRef + toShared)
|
|
793
|
+
// and read mapping via the SceneInstance component on the synthetic root.
|
|
794
|
+
// SceneInstance schema must be locally registered so resolveComponent finds
|
|
795
|
+
// it during instantiateScene (matches the runtime schema definition).
|
|
796
|
+
defineComponent('SceneInstance', {
|
|
797
|
+
source: { type: 'shared<SceneAsset>' },
|
|
798
|
+
mapping: { type: 'array<entity>' },
|
|
799
|
+
state: { type: 'unique<SceneInstanceState>' },
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
function localId(n: number): LocalEntityId {
|
|
803
|
+
return n as LocalEntityId;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function buildScene(nodes: readonly SceneEntity[]): SceneAsset {
|
|
807
|
+
return { kind: 'scene', entities: nodes };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function registerSceneAsset(
|
|
811
|
+
world: World,
|
|
812
|
+
asset: SceneAsset,
|
|
813
|
+
components: readonly Component[] = [Transform],
|
|
814
|
+
): Handle<'SceneAsset', 'shared'> {
|
|
815
|
+
for (const component of [SceneInstance, ChildOf, Children, ...components]) {
|
|
816
|
+
world.components.register(component).unwrap();
|
|
817
|
+
}
|
|
818
|
+
return world.allocSharedRef('SceneAsset', asset);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function firstNodeEntity(world: World, root: EntityHandle): EntityHandle {
|
|
822
|
+
// entityToLocalId.keys() iterates in topo-sort spawn order; first key is
|
|
823
|
+
// localId 0's live Entity. Robust against the mapping[0]===0 encoding
|
|
824
|
+
// (gen=0+idx=0 produces raw u32 0, which is a valid Entity not "empty").
|
|
825
|
+
const stateRes = SceneOwner.worldGetSceneInstanceState(world, root);
|
|
826
|
+
if (!stateRes.ok) throw new Error('SceneInstance state lookup failed');
|
|
827
|
+
const it = stateRes.value.entityToLocalId.keys();
|
|
828
|
+
const first = it.next();
|
|
829
|
+
if (first.done) throw new Error('entityToLocalId empty');
|
|
830
|
+
return first.value;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
describe('default-value 4-layer fallback (w20 / AC-07 + AC-11 + AC-12)', () => {
|
|
834
|
+
it('AC-07 layer 1: explicit Scene value beats every default', () => {
|
|
835
|
+
const Transform = defineComponent('Transform', {
|
|
836
|
+
pos: { type: 'array<f32, 3>', default: new Float32Array([99, 99, 99]) },
|
|
837
|
+
});
|
|
838
|
+
const world = new World();
|
|
839
|
+
world.components.register(Transform).unwrap();
|
|
840
|
+
|
|
841
|
+
const nodes: SceneEntity[] = [
|
|
842
|
+
{
|
|
843
|
+
localId: localId(0),
|
|
844
|
+
components: { Transform: { pos: [1.5, 0, 0] } },
|
|
845
|
+
},
|
|
846
|
+
];
|
|
847
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
848
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
849
|
+
expect(r.ok).toBe(true);
|
|
850
|
+
if (!r.ok) return;
|
|
851
|
+
const e = firstNodeEntity(world, r.value.root);
|
|
852
|
+
const t = world.get(e, Transform).unwrap();
|
|
853
|
+
expect(Array.from(t.pos)).toEqual([1.5, 0, 0]);
|
|
854
|
+
// overrides stay empty - no setSceneOverride was called. M3 reads them
|
|
855
|
+
// from the SceneInstanceState payload via getSceneInstanceState.
|
|
856
|
+
const stateRes = SceneOwner.worldGetSceneInstanceState(world, r.value.root);
|
|
857
|
+
expect(stateRes.ok).toBe(true);
|
|
858
|
+
if (!stateRes.ok) return;
|
|
859
|
+
expect(stateRes.value.overrides.size).toBe(0);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('AC-11 layer 2: component-level defaults fill missing fields (D-P3)', () => {
|
|
863
|
+
const Transform = defineComponent('Transform', {
|
|
864
|
+
pos: { type: 'array<f32, 3>', default: new Float32Array([0, 7, 0]) },
|
|
865
|
+
});
|
|
866
|
+
const world = new World();
|
|
867
|
+
world.components.register(Transform).unwrap();
|
|
868
|
+
|
|
869
|
+
// Node omits pos entirely -> layer 2 fills the whole array column with
|
|
870
|
+
// the field-descriptor default [0, 7, 0] (beats the layer-3 zero-fill,
|
|
871
|
+
// asserted in the next test).
|
|
872
|
+
const nodes: SceneEntity[] = [
|
|
873
|
+
{
|
|
874
|
+
localId: localId(0),
|
|
875
|
+
components: { Transform: {} },
|
|
876
|
+
},
|
|
877
|
+
];
|
|
878
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
879
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
880
|
+
expect(r.ok).toBe(true);
|
|
881
|
+
if (!r.ok) return;
|
|
882
|
+
const e = firstNodeEntity(world, r.value.root);
|
|
883
|
+
const t = world.get(e, Transform).unwrap();
|
|
884
|
+
expect(Array.from(t.pos)).toEqual([0, 7, 0]);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('AC-12 layer 3: TS type defaults silently fill three-layer-empty fields', () => {
|
|
888
|
+
const Transform = defineComponent('Transform', {
|
|
889
|
+
pos: 'array<f32, 3>',
|
|
890
|
+
flag: 'bool',
|
|
891
|
+
kind: 'u32',
|
|
892
|
+
});
|
|
893
|
+
const world = new World();
|
|
894
|
+
world.components.register(Transform).unwrap();
|
|
895
|
+
|
|
896
|
+
// No defaults at the component level; node writes nothing. Three-layer
|
|
897
|
+
// chain leaves pos / flag / kind empty -> layer 3 fills with TS type
|
|
898
|
+
// defaults. AC-12 mandates this path is SILENT (no error code).
|
|
899
|
+
const nodes: SceneEntity[] = [
|
|
900
|
+
{
|
|
901
|
+
localId: localId(0),
|
|
902
|
+
components: { Transform: {} },
|
|
903
|
+
},
|
|
904
|
+
];
|
|
905
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
906
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
907
|
+
expect(r.ok).toBe(true);
|
|
908
|
+
if (!r.ok) return;
|
|
909
|
+
const e = firstNodeEntity(world, r.value.root);
|
|
910
|
+
const t = world.get(e, Transform).unwrap();
|
|
911
|
+
expect(Array.from(t.pos)).toEqual([0, 0, 0]); // array<f32, 3> -> zero-fill
|
|
912
|
+
expect(t.flag).toBe(false); // bool -> false (read back as 0/1)
|
|
913
|
+
expect(t.kind).toBe(0); // u32 -> 0
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('AC-12 layer 3: entity-typed field falls back to NULL_ENTITY sentinel', () => {
|
|
917
|
+
const TargetSlot = defineComponent('TargetSlot', { target: 'entity' });
|
|
918
|
+
const world = new World();
|
|
919
|
+
world.components.register(TargetSlot).unwrap();
|
|
920
|
+
|
|
921
|
+
// Node declares TargetSlot with NO target value -> three layers silent
|
|
922
|
+
// -> layer 3 must store ENTITY_NULL_RAW (0xffffffff) sentinel in the u32
|
|
923
|
+
// column. On read, readRow decodes ENTITY_NULL_RAW back to JS null (the
|
|
924
|
+
// null-sentinel mapping is a storage-level convention, not a liveness check).
|
|
925
|
+
// Both shapes are pinned here so a regression in either direction trips
|
|
926
|
+
// this test:
|
|
927
|
+
// - storage layer: ENTITY_NULL_RAW is the sentinel SSOT
|
|
928
|
+
// - read layer: null is what the AI user observes after world.get
|
|
929
|
+
const nodes: SceneEntity[] = [
|
|
930
|
+
{
|
|
931
|
+
localId: localId(0),
|
|
932
|
+
components: { TargetSlot: {} },
|
|
933
|
+
},
|
|
934
|
+
];
|
|
935
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [TargetSlot]);
|
|
936
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
937
|
+
expect(r.ok).toBe(true);
|
|
938
|
+
if (!r.ok) return;
|
|
939
|
+
const e = firstNodeEntity(world, r.value.root);
|
|
940
|
+
const slot = world.get(e, TargetSlot).unwrap();
|
|
941
|
+
// Read-side: ENTITY_NULL_RAW storage maps back to null; layer 3 silent
|
|
942
|
+
// path must NOT emit any error code.
|
|
943
|
+
expect(slot.target).toBe(null);
|
|
944
|
+
// SSOT pin: verify ENTITY_NULL_RAW is the agreed sentinel (so the
|
|
945
|
+
// contract between layer 3 and the storage column stays bidirectional).
|
|
946
|
+
expect(ENTITY_NULL_RAW).toBe(0xffffffff);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('layer chain order: layer 1 > layer 2 > layer 3 priority discipline', () => {
|
|
950
|
+
// Compose all three layers in one node so the priority chain is
|
|
951
|
+
// observable in a single instantiate roundtrip (one field per layer;
|
|
952
|
+
// array columns fill whole-field, not per-lane):
|
|
953
|
+
// pos: layer 1 wins (explicit Scene value)
|
|
954
|
+
// quat: layer 2 wins (component default)
|
|
955
|
+
// scale: layer 3 wins (silent TS default)
|
|
956
|
+
const Transform = defineComponent('Transform', {
|
|
957
|
+
pos: { type: 'array<f32, 3>', default: new Float32Array([99, 99, 99]) },
|
|
958
|
+
quat: { type: 'array<f32, 4>', default: new Float32Array([0, 7, 0, 1]) },
|
|
959
|
+
scale: 'array<f32, 3>',
|
|
960
|
+
});
|
|
961
|
+
const world = new World();
|
|
962
|
+
world.components.register(Transform).unwrap();
|
|
963
|
+
|
|
964
|
+
const nodes: SceneEntity[] = [
|
|
965
|
+
{
|
|
966
|
+
localId: localId(0),
|
|
967
|
+
components: { Transform: { pos: [1.5, 0, 0] } },
|
|
968
|
+
},
|
|
969
|
+
];
|
|
970
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
971
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
972
|
+
expect(r.ok).toBe(true);
|
|
973
|
+
if (!r.ok) return;
|
|
974
|
+
const e = firstNodeEntity(world, r.value.root);
|
|
975
|
+
const t = world.get(e, Transform).unwrap();
|
|
976
|
+
expect(Array.from(t.pos)).toEqual([1.5, 0, 0]); // layer 1 wins (explicit beats default 99)
|
|
977
|
+
expect(Array.from(t.quat)).toEqual([0, 7, 0, 1]); // layer 2 wins (component default beats type 0)
|
|
978
|
+
expect(Array.from(t.scale)).toEqual([0, 0, 0]); // layer 3 wins (TS array<f32, N> zero-fill)
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('layer 3 silent path: no error code surface on three-layer-empty schema field', () => {
|
|
982
|
+
// Charter tension declared in requirements §default-value mechanism +
|
|
983
|
+
// plan-strategy §3.3 §error-model: layer 3 MUST be silent. This test
|
|
984
|
+
// pins the negative -- the Result must be ok and no error-shaped value
|
|
985
|
+
// surfaces to the caller.
|
|
986
|
+
const Transform = defineComponent('Transform', {
|
|
987
|
+
pos: 'array<f32, 3>',
|
|
988
|
+
});
|
|
989
|
+
const world = new World();
|
|
990
|
+
world.components.register(Transform).unwrap();
|
|
991
|
+
|
|
992
|
+
const nodes: SceneEntity[] = [{ localId: localId(0), components: { Transform: {} } }];
|
|
993
|
+
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
994
|
+
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
995
|
+
expect(r.ok).toBe(true);
|
|
996
|
+
// No 'scene-default-missing' / similar code surfaces. Asserted at the
|
|
997
|
+
// Result envelope -- if w22 ever introduces such a code in the silent
|
|
998
|
+
// path this assertion flips red.
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
{
|
|
1004
|
+
// --- from sort-key-component.test.ts ---
|
|
1005
|
+
// w08 - SortKey component schema spawn (TDD red).
|
|
1006
|
+
//
|
|
1007
|
+
// feat-20260520-2d-sprite-layer-mvp M-2 w08 / requirements AC-07 + AC-19 (4).
|
|
1008
|
+
//
|
|
1009
|
+
// Coverage:
|
|
1010
|
+
// - SortKey { value: 1.5 } round-trips with f32 precision.
|
|
1011
|
+
// - spawn without SortKey reads back default 0.0 (f32 scalar default
|
|
1012
|
+
// via the 4-layer fallback chain — feat-20260517-spawn-default-fallback).
|
|
1013
|
+
// - Layer { value: 100 } + SortKey { value: -2.0 } co-exist on one entity
|
|
1014
|
+
// (multi-component spawn targets a single archetype).
|
|
1015
|
+
//
|
|
1016
|
+
// The downstream "SortKey overrides TransparentSortConfig mode formula"
|
|
1017
|
+
// behaviour is verified by w16 inside `transparent-sort.test.ts` — this
|
|
1018
|
+
// task only validates that SortKey is a registerable scalar f32 component
|
|
1019
|
+
// with the expected read-back path. charter mapping: F1 + P3 + P4 (Layer +
|
|
1020
|
+
// SortKey are generic ECS renderer components, not 2D-only specials).
|
|
1021
|
+
//
|
|
1022
|
+
// TDD red: w12 implements the component file + barrel re-export to turn
|
|
1023
|
+
// this green.
|
|
1024
|
+
|
|
1025
|
+
describe('w08 - SortKey = defineComponent("SortKey", { value: "f32" })', () => {
|
|
1026
|
+
it('has schema { value: "f32" } (1 f32 field)', () => {
|
|
1027
|
+
expect(SortKey.name).toBe('SortKey');
|
|
1028
|
+
expect(Object.keys(componentSchema(SortKey)).length).toBe(1);
|
|
1029
|
+
expect(componentSchema(SortKey).value).toBe('f32');
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('spawn SortKey { value: 1.5 } round-trips within f32 precision', () => {
|
|
1033
|
+
const world = new World();
|
|
1034
|
+
const e = world.spawn({ component: SortKey, data: { value: 1.5 } }).unwrap();
|
|
1035
|
+
const r = world.get(e, SortKey).unwrap();
|
|
1036
|
+
expect(r.value).toBeCloseTo(1.5, 5);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('spawn payload omitting value falls through 4-layer chain to f32 default 0', () => {
|
|
1040
|
+
const world = new World();
|
|
1041
|
+
const spawnResult = world.spawn({ component: SortKey, data: {} });
|
|
1042
|
+
expect(spawnResult.ok).toBe(true);
|
|
1043
|
+
const e = spawnResult.unwrap();
|
|
1044
|
+
const r = world.get(e, SortKey).unwrap();
|
|
1045
|
+
expect(r.value).toBe(0);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('Layer + SortKey co-exist on a single entity (multi-component spawn)', () => {
|
|
1049
|
+
const world = new World();
|
|
1050
|
+
const e = world
|
|
1051
|
+
.spawn(
|
|
1052
|
+
{ component: Layer, data: { value: 100 } },
|
|
1053
|
+
{ component: SortKey, data: { value: -2.0 } },
|
|
1054
|
+
)
|
|
1055
|
+
.unwrap();
|
|
1056
|
+
const layerRead = world.get(e, Layer).unwrap();
|
|
1057
|
+
const sortKeyRead = world.get(e, SortKey).unwrap();
|
|
1058
|
+
expect(layerRead.value).toBe(100);
|
|
1059
|
+
expect(sortKeyRead.value).toBeCloseTo(-2.0, 5);
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
{
|
|
1065
|
+
// --- from animation-player.test.ts ---
|
|
1066
|
+
// feat-20260615-animation-player-crossfade-simple-transition M1 / w1 —
|
|
1067
|
+
// AnimationPlayer SoA schema lock test.
|
|
1068
|
+
//
|
|
1069
|
+
// Old 5-field schema { clip, time, speed, paused, looping } replaced
|
|
1070
|
+
// by 6-field SoA variable arrays (feat-20260713 M1 / w4 dropped the fixed
|
|
1071
|
+
// 4-slot cap; the columns are now variable `array<T>`), then extended to
|
|
1072
|
+
// 10 fields by feat-20260713 M3 / w24 (plan D-3: a single component surface
|
|
1073
|
+
// hosts both direct-write and graph-driven playback — no second peer
|
|
1074
|
+
// component):
|
|
1075
|
+
// clips: 'array<shared<AnimationClip>>' (default empty — no slot active)
|
|
1076
|
+
// times: 'array<f32>' (default empty Float32Array)
|
|
1077
|
+
// weights: 'array<f32>' (default empty Float32Array)
|
|
1078
|
+
// speeds: 'array<f32>' (default empty — consumers write
|
|
1079
|
+
// all four columns length-synced)
|
|
1080
|
+
// graph: 'shared<AnimationGraph>' (default 0 — no graph attached)
|
|
1081
|
+
// nodeWeights: 'array<f32>' (default empty — per-node runtime knob)
|
|
1082
|
+
// nodeTimes: 'array<f32>' (default empty — per-node seek time)
|
|
1083
|
+
// nodeSpeeds: 'array<f32>' (default empty — per-node speed)
|
|
1084
|
+
// paused: 'bool' (default false)
|
|
1085
|
+
// looping: 'bool' (default true)
|
|
1086
|
+
//
|
|
1087
|
+
// Anchors: requirements AC-01 (variable N-slot direct-write field set) + AC-02
|
|
1088
|
+
// (type-level error on old shape) + IS-1 (SoA arrays); feat-20260713 M1
|
|
1089
|
+
// plan D-1/D-6 (variable columns, speeds default []); M3 plan D-3 (graph
|
|
1090
|
+
// handle + per-node runtime knobs extend the same component);
|
|
1091
|
+
// plan-tasks.json w3/w4/w24.
|
|
1092
|
+
|
|
1093
|
+
describe('AnimationPlayer — SoA 10-field schema lock (M1 / w3 + M3 / w24)', () => {
|
|
1094
|
+
it('AnimationPlayer is a registered component with name "AnimationPlayer" and 10 SoA schema fields', () => {
|
|
1095
|
+
expect(AnimationPlayer.name).toBe('AnimationPlayer');
|
|
1096
|
+
const schema = componentSchema(AnimationPlayer) as Record<string, unknown>;
|
|
1097
|
+
expect(Object.keys(schema).length).toBe(10);
|
|
1098
|
+
expect(schema).toEqual({
|
|
1099
|
+
clips: 'array<shared<AnimationClip>>',
|
|
1100
|
+
times: 'array<f32>',
|
|
1101
|
+
weights: 'array<f32>',
|
|
1102
|
+
speeds: 'array<f32>',
|
|
1103
|
+
graph: 'shared<AnimationGraph>',
|
|
1104
|
+
nodeWeights: 'array<f32>',
|
|
1105
|
+
nodeTimes: 'array<f32>',
|
|
1106
|
+
nodeSpeeds: 'array<f32>',
|
|
1107
|
+
paused: 'bool',
|
|
1108
|
+
looping: 'bool',
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('componentSchema(AnimationPlayer).clips is variable array<shared<AnimationClip>> (SoA keyword)', () => {
|
|
1113
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).clips).toBe(
|
|
1114
|
+
'array<shared<AnimationClip>>',
|
|
1115
|
+
);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('componentSchema(AnimationPlayer).times is variable array<f32>', () => {
|
|
1119
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).times).toBe(
|
|
1120
|
+
'array<f32>',
|
|
1121
|
+
);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it('componentSchema(AnimationPlayer).weights is variable array<f32>', () => {
|
|
1125
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).weights).toBe(
|
|
1126
|
+
'array<f32>',
|
|
1127
|
+
);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('componentSchema(AnimationPlayer).speeds is variable array<f32>', () => {
|
|
1131
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).speeds).toBe(
|
|
1132
|
+
'array<f32>',
|
|
1133
|
+
);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it('componentSchema(AnimationPlayer).paused is bool', () => {
|
|
1137
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).paused).toBe('bool');
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('componentSchema(AnimationPlayer).looping is bool', () => {
|
|
1141
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).looping).toBe('bool');
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it('componentSchema(AnimationPlayer).graph is shared<AnimationGraph> (M3 / w24 graph handle)', () => {
|
|
1145
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).graph).toBe(
|
|
1146
|
+
'shared<AnimationGraph>',
|
|
1147
|
+
);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
it('componentSchema(AnimationPlayer).nodeWeights is variable array<f32> (M3 / w24 per-node knob)', () => {
|
|
1151
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).nodeWeights).toBe(
|
|
1152
|
+
'array<f32>',
|
|
1153
|
+
);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('componentSchema(AnimationPlayer).nodeTimes is variable array<f32> (M3 / w24 per-node seek time)', () => {
|
|
1157
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).nodeTimes).toBe(
|
|
1158
|
+
'array<f32>',
|
|
1159
|
+
);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it('componentSchema(AnimationPlayer).nodeSpeeds is variable array<f32> (M3 / w24 per-node speed)', () => {
|
|
1163
|
+
expect((componentSchema(AnimationPlayer) as Record<string, unknown>).nodeSpeeds).toBe(
|
|
1164
|
+
'array<f32>',
|
|
1165
|
+
);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('old field clip is absent from schema (AC-02 type-level error)', () => {
|
|
1169
|
+
expect(componentSchema(AnimationPlayer)).not.toHaveProperty('clip');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('old field time is absent from schema (AC-02 type-level error)', () => {
|
|
1173
|
+
expect(componentSchema(AnimationPlayer)).not.toHaveProperty('time');
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it('old field speed is absent from schema (AC-02 type-level error)', () => {
|
|
1177
|
+
expect(componentSchema(AnimationPlayer)).not.toHaveProperty('speed');
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
it('AnimationPlayer spawn yields variable SoA defaults: clips/times/weights/speeds all empty, paused=false, looping=true (M1 / w4 variable columns, speeds default [])', () => {
|
|
1181
|
+
const world = new World();
|
|
1182
|
+
const e = world
|
|
1183
|
+
.spawn({
|
|
1184
|
+
component: AnimationPlayer,
|
|
1185
|
+
data: {},
|
|
1186
|
+
})
|
|
1187
|
+
.unwrap();
|
|
1188
|
+
const ap = world.get(e, AnimationPlayer).unwrap() as unknown as {
|
|
1189
|
+
clips: Uint32Array;
|
|
1190
|
+
times: Float32Array;
|
|
1191
|
+
weights: Float32Array;
|
|
1192
|
+
speeds: Float32Array;
|
|
1193
|
+
paused: boolean;
|
|
1194
|
+
looping: boolean;
|
|
1195
|
+
};
|
|
1196
|
+
// Variable columns default to empty (no slot active). Consumers write all
|
|
1197
|
+
// four columns length-synced when spawning an active player (M1 / w7).
|
|
1198
|
+
expect(ap.clips).toBeInstanceOf(Uint32Array);
|
|
1199
|
+
expect(ap.clips.length).toBe(0);
|
|
1200
|
+
expect(ap.times).toBeInstanceOf(Float32Array);
|
|
1201
|
+
expect(ap.times.length).toBe(0);
|
|
1202
|
+
expect(ap.weights).toBeInstanceOf(Float32Array);
|
|
1203
|
+
expect(ap.weights.length).toBe(0);
|
|
1204
|
+
// speeds layer-2 default retired to [] (was [1,1,1,1]); a variable column
|
|
1205
|
+
// cannot carry a fixed-width "play at 1x" default without desyncing from
|
|
1206
|
+
// an empty clips column.
|
|
1207
|
+
expect(ap.speeds).toBeInstanceOf(Float32Array);
|
|
1208
|
+
expect(ap.speeds.length).toBe(0);
|
|
1209
|
+
expect(ap.paused).toBe(false);
|
|
1210
|
+
expect(ap.looping).toBe(true);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
it('AnimationPlayer spawn with explicit SoA data overrides defaults', () => {
|
|
1214
|
+
const world = new World();
|
|
1215
|
+
const e = world
|
|
1216
|
+
.spawn({
|
|
1217
|
+
component: AnimationPlayer,
|
|
1218
|
+
data: {
|
|
1219
|
+
clips: [
|
|
1220
|
+
toShared<'AnimationClip'>(7),
|
|
1221
|
+
toShared<'AnimationClip'>(3),
|
|
1222
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1223
|
+
0 as Handle<'AnimationClip', 'shared'>,
|
|
1224
|
+
],
|
|
1225
|
+
times: new Float32Array([0, 1.5, 0, 0]),
|
|
1226
|
+
weights: new Float32Array([0.7, 0.3, 0, 0]),
|
|
1227
|
+
speeds: new Float32Array([1, 0.5, 0, 0]),
|
|
1228
|
+
paused: true,
|
|
1229
|
+
looping: false,
|
|
1230
|
+
},
|
|
1231
|
+
})
|
|
1232
|
+
.unwrap();
|
|
1233
|
+
const ap = world.get(e, AnimationPlayer).unwrap() as unknown as {
|
|
1234
|
+
clips: Uint32Array;
|
|
1235
|
+
times: Float32Array;
|
|
1236
|
+
weights: Float32Array;
|
|
1237
|
+
speeds: Float32Array;
|
|
1238
|
+
paused: boolean;
|
|
1239
|
+
looping: boolean;
|
|
1240
|
+
};
|
|
1241
|
+
expect(ap.clips[0]).toBe(toShared<'AnimationClip'>(7));
|
|
1242
|
+
expect(ap.clips[1]).toBe(toShared<'AnimationClip'>(3));
|
|
1243
|
+
expect(ap.clips[2]).toBe(0);
|
|
1244
|
+
expect(ap.clips[3]).toBe(0);
|
|
1245
|
+
expect(ap.times[0]).toBe(0);
|
|
1246
|
+
expect(ap.times[1]).toBeCloseTo(1.5, 5);
|
|
1247
|
+
expect(ap.weights[0]).toBeCloseTo(0.7, 5);
|
|
1248
|
+
expect(ap.weights[1]).toBeCloseTo(0.3, 5);
|
|
1249
|
+
expect(ap.speeds[0]).toBeCloseTo(1, 5);
|
|
1250
|
+
expect(ap.speeds[1]).toBeCloseTo(0.5, 5);
|
|
1251
|
+
expect(ap.paused).toBe(true);
|
|
1252
|
+
expect(ap.looping).toBe(false);
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it('AnimationPlayer set(times) updates the times array column', () => {
|
|
1256
|
+
const world = new World();
|
|
1257
|
+
const e = world
|
|
1258
|
+
.spawn({
|
|
1259
|
+
component: AnimationPlayer,
|
|
1260
|
+
data: {},
|
|
1261
|
+
})
|
|
1262
|
+
.unwrap();
|
|
1263
|
+
world.set(e, AnimationPlayer, { times: new Float32Array([2.0, 0, 0, 0]) });
|
|
1264
|
+
const ap = world.get(e, AnimationPlayer).unwrap() as unknown as { times: Float32Array };
|
|
1265
|
+
expect(ap.times[0]).toBeCloseTo(2.0, 5);
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
{
|
|
1271
|
+
// --- from camera.test.ts ---
|
|
1272
|
+
// feat-20260602-ecs-component-type-field-reflection-metadata-base / M5 / w14.
|
|
1273
|
+
//
|
|
1274
|
+
// Camera SSOT invariant guards (AC-07). Two concerns:
|
|
1275
|
+
//
|
|
1276
|
+
// (a) Full 21-field snapshot tests for perspective() / orthographic() factory
|
|
1277
|
+
// functions — every field value is asserted to match the pre-migration
|
|
1278
|
+
// reference. These guards ensure the w13 SSOT refactoring (deleting
|
|
1279
|
+
// CameraDataPod, deriving factory base from Camera token defaults) does
|
|
1280
|
+
// not change any field-level behavior.
|
|
1281
|
+
//
|
|
1282
|
+
// (b) Camera.fields reflection guard: 17 f32 fields, each with type:'f32'.
|
|
1283
|
+
// After w13 deletes CameraDataPod, the factory return type is also verified
|
|
1284
|
+
// to be derivable from the Camera token (no standalone interface).
|
|
1285
|
+
//
|
|
1286
|
+
// Anchors: requirements AC-07 (camera SSOT guard), plan-strategy D-A4
|
|
1287
|
+
// (keep factories, delete CameraDataPod), plan-tasks.json w14/w13.
|
|
1288
|
+
|
|
1289
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1290
|
+
// w14-a: 21-field value snapshot (AC-07 invariant guard for w13 SSOT
|
|
1291
|
+
// refactoring — every field value must stay byte-identical)
|
|
1292
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1293
|
+
|
|
1294
|
+
describe('camera factory 20-field snapshot (w14 AC-07 invariant)', () => {
|
|
1295
|
+
it('perspective({ fov: Math.PI/3, aspect: 16/9 }) — all 20 fields match reference', () => {
|
|
1296
|
+
const pod = perspective({ fov: Math.PI / 3, aspect: 16 / 9 });
|
|
1297
|
+
// Perspective quartet — caller-supplied
|
|
1298
|
+
expect(pod.fov).toBeCloseTo(Math.PI / 3, 6);
|
|
1299
|
+
expect(pod.aspect).toBeCloseTo(16 / 9, 6);
|
|
1300
|
+
expect(pod.near).toBeCloseTo(0.1, 6);
|
|
1301
|
+
expect(pod.far).toBe(100);
|
|
1302
|
+
// Projection discriminator
|
|
1303
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_PERSPECTIVE);
|
|
1304
|
+
// Ortho quartet defaults
|
|
1305
|
+
expect(pod.left).toBe(-1);
|
|
1306
|
+
expect(pod.right).toBe(1);
|
|
1307
|
+
expect(pod.bottom).toBe(-1);
|
|
1308
|
+
expect(pod.top).toBe(1);
|
|
1309
|
+
// Tonemap trio defaults
|
|
1310
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1311
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1312
|
+
expect(pod.whitePoint).toBeCloseTo(4.0, 6);
|
|
1313
|
+
// Post-processing defaults
|
|
1314
|
+
expect(pod.antialias).toBe(ANTIALIAS_NONE);
|
|
1315
|
+
expect(pod.historyVersion).toBe(0);
|
|
1316
|
+
expect(pod.bloom).toBe(BLOOM_DISABLED);
|
|
1317
|
+
expect(pod.bloomThreshold).toBeCloseTo(1.0, 6);
|
|
1318
|
+
expect(pod.bloomIntensity).toBeCloseTo(1.0, 6);
|
|
1319
|
+
expect(pod.bloomBlurRadius).toBeCloseTo(4.0, 6);
|
|
1320
|
+
// Clear-color default: transparent black array [0,0,0,0] (feat-20260709 M3;
|
|
1321
|
+
// E9 factory-pathway carries the new array default via
|
|
1322
|
+
// cameraPodFromDefaults()).
|
|
1323
|
+
expect(Array.from(pod.clearColor)).toEqual([0, 0, 0, 0]);
|
|
1324
|
+
// aspect-sync opt-out default (feat-20260617 / M3)
|
|
1325
|
+
expect(pod.autoAspect).toBe(true);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
it('perspective({ fov: 45, aspect: 4/3, near: 0.01, far: 1000 }) — explicit overrides, rest defaults', () => {
|
|
1329
|
+
const pod = perspective({ fov: 45, aspect: 4 / 3, near: 0.01, far: 1000 });
|
|
1330
|
+
expect(pod.fov).toBe(45);
|
|
1331
|
+
expect(pod.aspect).toBeCloseTo(4 / 3, 6);
|
|
1332
|
+
expect(pod.near).toBeCloseTo(0.01, 6);
|
|
1333
|
+
expect(pod.far).toBe(1000);
|
|
1334
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_PERSPECTIVE);
|
|
1335
|
+
expect(pod.left).toBe(-1);
|
|
1336
|
+
expect(pod.right).toBe(1);
|
|
1337
|
+
expect(pod.bottom).toBe(-1);
|
|
1338
|
+
expect(pod.top).toBe(1);
|
|
1339
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1340
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1341
|
+
expect(pod.whitePoint).toBeCloseTo(4.0, 6);
|
|
1342
|
+
expect(pod.antialias).toBe(ANTIALIAS_NONE);
|
|
1343
|
+
expect(pod.bloom).toBe(BLOOM_DISABLED);
|
|
1344
|
+
expect(pod.bloomThreshold).toBeCloseTo(1.0, 6);
|
|
1345
|
+
expect(pod.bloomIntensity).toBeCloseTo(1.0, 6);
|
|
1346
|
+
expect(pod.bloomBlurRadius).toBeCloseTo(4.0, 6);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('orthographic({ left: -10, right: 10, bottom: -10, top: 10 }) — all 20 fields match reference', () => {
|
|
1350
|
+
const pod = orthographic({ left: -10, right: 10, bottom: -10, top: 10 });
|
|
1351
|
+
// Ortho bounds — caller-supplied
|
|
1352
|
+
expect(pod.left).toBe(-10);
|
|
1353
|
+
expect(pod.right).toBe(10);
|
|
1354
|
+
expect(pod.bottom).toBe(-10);
|
|
1355
|
+
expect(pod.top).toBe(10);
|
|
1356
|
+
expect(pod.near).toBeCloseTo(0.1, 6);
|
|
1357
|
+
expect(pod.far).toBe(100);
|
|
1358
|
+
// Perspective fields sentinel
|
|
1359
|
+
expect(pod.fov).toBe(0);
|
|
1360
|
+
expect(pod.aspect).toBe(1);
|
|
1361
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_ORTHOGRAPHIC);
|
|
1362
|
+
// Tonemap trio defaults
|
|
1363
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1364
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1365
|
+
expect(pod.whitePoint).toBeCloseTo(4.0, 6);
|
|
1366
|
+
// Post-processing defaults
|
|
1367
|
+
expect(pod.antialias).toBe(ANTIALIAS_NONE);
|
|
1368
|
+
expect(pod.historyVersion).toBe(0);
|
|
1369
|
+
expect(pod.bloom).toBe(BLOOM_DISABLED);
|
|
1370
|
+
expect(pod.bloomThreshold).toBeCloseTo(1.0, 6);
|
|
1371
|
+
expect(pod.bloomIntensity).toBeCloseTo(1.0, 6);
|
|
1372
|
+
expect(pod.bloomBlurRadius).toBeCloseTo(4.0, 6);
|
|
1373
|
+
// Clear-color default: transparent black array [0,0,0,0] (feat-20260709 M3).
|
|
1374
|
+
expect(Array.from(pod.clearColor)).toEqual([0, 0, 0, 0]);
|
|
1375
|
+
// aspect-sync opt-out default (feat-20260617 / M3): the sidecar only
|
|
1376
|
+
// touches perspective cameras, but the column default is shared.
|
|
1377
|
+
expect(pod.autoAspect).toBe(true);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it('orthographic({ ..., near: -1, far: 1 }) — explicit near/far overrides', () => {
|
|
1381
|
+
const pod = orthographic({ left: 0, right: 800, bottom: 600, top: 0, near: -1, far: 1 });
|
|
1382
|
+
expect(pod.left).toBe(0);
|
|
1383
|
+
expect(pod.right).toBe(800);
|
|
1384
|
+
expect(pod.bottom).toBe(600);
|
|
1385
|
+
expect(pod.top).toBe(0);
|
|
1386
|
+
expect(pod.near).toBe(-1);
|
|
1387
|
+
expect(pod.far).toBe(1);
|
|
1388
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_ORTHOGRAPHIC);
|
|
1389
|
+
expect(pod.fov).toBe(0);
|
|
1390
|
+
expect(pod.aspect).toBe(1);
|
|
1391
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('perspective + orthographic 20-field counts (20 Camera columns)', () => {
|
|
1395
|
+
const p = perspective({ fov: 60, aspect: 4 / 3 });
|
|
1396
|
+
const o = orthographic({ left: -1, right: 1, bottom: -1, top: 1 });
|
|
1397
|
+
// Both return exactly 20 fields (17 f32 + historyVersion u32 + one clearColor array +
|
|
1398
|
+
// autoAspect bool column; feat-20260709 M3 collapsed the 4-scalar
|
|
1399
|
+
// clear-color quartet into one inline array<f32,4>).
|
|
1400
|
+
expect(Object.keys(p).length).toBe(20);
|
|
1401
|
+
expect(Object.keys(o).length).toBe(20);
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1406
|
+
// w14-b: Camera.fields reflection guard (AC-07 SSOT — 17 f32 fields)
|
|
1407
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1408
|
+
|
|
1409
|
+
describe('Camera.fields reflection (w14 AC-07 SSOT)', () => {
|
|
1410
|
+
it('Camera.fields has exactly 20 keys matching the Camera column set', () => {
|
|
1411
|
+
const keys = Object.keys(Camera.fields).sort();
|
|
1412
|
+
expect(keys).toEqual([
|
|
1413
|
+
'antialias',
|
|
1414
|
+
'aspect',
|
|
1415
|
+
'autoAspect',
|
|
1416
|
+
'bloom',
|
|
1417
|
+
'bloomBlurRadius',
|
|
1418
|
+
'bloomIntensity',
|
|
1419
|
+
'bloomThreshold',
|
|
1420
|
+
'bottom',
|
|
1421
|
+
'clearColor',
|
|
1422
|
+
'exposure',
|
|
1423
|
+
'far',
|
|
1424
|
+
'fov',
|
|
1425
|
+
'historyVersion',
|
|
1426
|
+
'left',
|
|
1427
|
+
'near',
|
|
1428
|
+
'projection',
|
|
1429
|
+
'right',
|
|
1430
|
+
'tonemap',
|
|
1431
|
+
'top',
|
|
1432
|
+
'whitePoint',
|
|
1433
|
+
]);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it('every Camera.fields entry has the declared numeric, bool, or array storage kind', () => {
|
|
1437
|
+
for (const key of Object.keys(Camera.fields) as Array<keyof typeof Camera.fields>) {
|
|
1438
|
+
const expected =
|
|
1439
|
+
key === 'autoAspect'
|
|
1440
|
+
? 'bool'
|
|
1441
|
+
: key === 'clearColor'
|
|
1442
|
+
? 'array<f32, 4>'
|
|
1443
|
+
: key === 'historyVersion'
|
|
1444
|
+
? 'u32'
|
|
1445
|
+
: 'f32';
|
|
1446
|
+
expect(Camera.fields[key].type).toBe(expected);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
it('Camera.fields is frozen', () => {
|
|
1451
|
+
expect(Object.isFrozen(Camera.fields)).toBe(true);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it("Camera.fields defaults match factory reference values (confirmed by w14-a's snapshot)", () => {
|
|
1455
|
+
// The per-field defaults stored in Camera.fields after M3 migration
|
|
1456
|
+
// should align with what perspective() / orthographic() use as
|
|
1457
|
+
// their base values. Note: fov / aspect / near / far have no default
|
|
1458
|
+
// (perspective quartet is explicit-only per OOS-5).
|
|
1459
|
+
const d = Camera.fields;
|
|
1460
|
+
expect(d.projection.default).toBe(0);
|
|
1461
|
+
expect(d.left.default).toBe(-1);
|
|
1462
|
+
expect(d.right.default).toBe(1);
|
|
1463
|
+
expect(d.bottom.default).toBe(-1);
|
|
1464
|
+
expect(d.top.default).toBe(1);
|
|
1465
|
+
expect(d.tonemap.default).toBe(0);
|
|
1466
|
+
expect(d.exposure.default).toBeCloseTo(1.0, 6);
|
|
1467
|
+
expect(d.whitePoint.default).toBeCloseTo(4.0, 6);
|
|
1468
|
+
expect(d.antialias.default).toBe(0);
|
|
1469
|
+
expect(d.historyVersion.default).toBe(0);
|
|
1470
|
+
expect(d.bloom.default).toBe(0);
|
|
1471
|
+
expect(d.bloomThreshold.default).toBeCloseTo(1.0, 6);
|
|
1472
|
+
expect(d.bloomIntensity.default).toBeCloseTo(1.0, 6);
|
|
1473
|
+
expect(d.bloomBlurRadius.default).toBeCloseTo(4.0, 6);
|
|
1474
|
+
// Clear-color default: explicit layer-2 transparent black array (feat-20260709
|
|
1475
|
+
// M3; array layer-3 fallback is all-zero so the default MUST be explicit,
|
|
1476
|
+
// D-5).
|
|
1477
|
+
expect(Array.from(d.clearColor.default as Float32Array)).toEqual([0, 0, 0, 0]);
|
|
1478
|
+
// aspect-sync opt-out default (feat-20260617 / M3).
|
|
1479
|
+
expect(d.autoAspect.default).toBe(true);
|
|
1480
|
+
// Perspective quartet defaults intentionally absent (OOS-5).
|
|
1481
|
+
expect(d.fov.default).toBeUndefined();
|
|
1482
|
+
expect(d.aspect.default).toBeUndefined();
|
|
1483
|
+
expect(d.near.default).toBeUndefined();
|
|
1484
|
+
expect(d.far.default).toBeUndefined();
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
describe('componentDefinition(Camera).defaults — frozen token defaults map (AC-07 + feat-20260528-fxaa-post-processing + feat-20260531-bloom + feat-20260608-clear-color)', () => {
|
|
1489
|
+
it('componentDefinition(Camera).defaults equals { projection: 0, left: -1, right: 1, bottom: -1, top: 1, tonemap: 0, exposure: 1.0, whitePoint: 4.0, antialias: 0, bloom: 0, bloomThreshold: 1.0, bloomIntensity: 1.0, bloomBlurRadius: 4.0, clearColor: [0,0,0,0], autoAspect: true }', () => {
|
|
1490
|
+
expect(componentDefinition(Camera).defaults).toEqual({
|
|
1491
|
+
projection: 0,
|
|
1492
|
+
left: -1,
|
|
1493
|
+
right: 1,
|
|
1494
|
+
bottom: -1,
|
|
1495
|
+
top: 1,
|
|
1496
|
+
tonemap: 0,
|
|
1497
|
+
exposure: 1.0,
|
|
1498
|
+
whitePoint: 4.0,
|
|
1499
|
+
antialias: 0,
|
|
1500
|
+
historyVersion: 0,
|
|
1501
|
+
bloom: 0,
|
|
1502
|
+
bloomThreshold: 1.0,
|
|
1503
|
+
bloomIntensity: 1.0,
|
|
1504
|
+
bloomBlurRadius: 4.0,
|
|
1505
|
+
clearColor: new Float32Array([0, 0, 0, 0]),
|
|
1506
|
+
autoAspect: true,
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
it('componentDefinition(Camera).defaults is deep-frozen (Object.isFrozen returns true)', () => {
|
|
1511
|
+
expect(componentDefinition(Camera).defaults).toBeDefined();
|
|
1512
|
+
// Object.isFrozen is true for any frozen object; defineComponent
|
|
1513
|
+
// freezes the per-component defaults map at registration time.
|
|
1514
|
+
expect(Object.isFrozen(componentDefinition(Camera).defaults)).toBe(true);
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it('componentDefinition(Camera).defaults does NOT carry fov / aspect / near / far (perspective quartet stays explicit per OOS-5)', () => {
|
|
1518
|
+
const d = componentDefinition(Camera).defaults as
|
|
1519
|
+
| Readonly<Record<string, unknown>>
|
|
1520
|
+
| undefined;
|
|
1521
|
+
expect(d).toBeDefined();
|
|
1522
|
+
if (d === undefined) return;
|
|
1523
|
+
expect('fov' in d).toBe(false);
|
|
1524
|
+
expect('aspect' in d).toBe(false);
|
|
1525
|
+
expect('near' in d).toBe(false);
|
|
1526
|
+
expect('far' in d).toBe(false);
|
|
1527
|
+
});
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
describe('Camera 4-field perspective spawn — token defaults fill ortho quartet (AC-07 runtime)', () => {
|
|
1531
|
+
it('world.spawn({ component: Camera, data: { fov, aspect, near, far } }) yields projection === 0 + ortho defaults', () => {
|
|
1532
|
+
const world = new World();
|
|
1533
|
+
const e = world
|
|
1534
|
+
.spawn({
|
|
1535
|
+
component: Camera,
|
|
1536
|
+
data: {
|
|
1537
|
+
fov: Math.PI / 4,
|
|
1538
|
+
aspect: 16 / 9,
|
|
1539
|
+
near: 0.1,
|
|
1540
|
+
far: 100,
|
|
1541
|
+
},
|
|
1542
|
+
})
|
|
1543
|
+
.unwrap();
|
|
1544
|
+
const row = world.get(e, Camera).unwrap();
|
|
1545
|
+
|
|
1546
|
+
// Layer-1: explicit values pass through (f32 round on 16 / 9).
|
|
1547
|
+
expect(row.fov).toBeCloseTo(Math.PI / 4, 6);
|
|
1548
|
+
expect(row.aspect).toBeCloseTo(16 / 9, 6);
|
|
1549
|
+
expect(row.near).toBeCloseTo(0.1, 6);
|
|
1550
|
+
expect(row.far).toBe(100);
|
|
1551
|
+
|
|
1552
|
+
// Layer-2 (token defaults): projection + 4 ortho fields + 3 tonemap fields.
|
|
1553
|
+
expect(row.projection).toBe(CAMERA_PROJECTION_PERSPECTIVE);
|
|
1554
|
+
expect(row.left).toBe(-1);
|
|
1555
|
+
expect(row.right).toBe(1);
|
|
1556
|
+
expect(row.bottom).toBe(-1);
|
|
1557
|
+
expect(row.top).toBe(1);
|
|
1558
|
+
expect(row.tonemap).toBe(TONEMAP_NONE);
|
|
1559
|
+
expect(row.exposure).toBeCloseTo(1.0, 6);
|
|
1560
|
+
expect(row.whitePoint).toBeCloseTo(4.0, 6);
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
it('explicit ortho override path: caller passes projection + 4 ortho fields, defaults yield', () => {
|
|
1564
|
+
// Token defaults must NOT clobber explicit caller input -- this is
|
|
1565
|
+
// the layer-1 wins clause from research section F1.
|
|
1566
|
+
const world = new World();
|
|
1567
|
+
const e = world
|
|
1568
|
+
.spawn({
|
|
1569
|
+
component: Camera,
|
|
1570
|
+
data: {
|
|
1571
|
+
fov: 0,
|
|
1572
|
+
aspect: 1,
|
|
1573
|
+
near: 0.1,
|
|
1574
|
+
far: 100,
|
|
1575
|
+
projection: 1,
|
|
1576
|
+
left: -10,
|
|
1577
|
+
right: 10,
|
|
1578
|
+
bottom: -10,
|
|
1579
|
+
top: 10,
|
|
1580
|
+
},
|
|
1581
|
+
})
|
|
1582
|
+
.unwrap();
|
|
1583
|
+
const row = world.get(e, Camera).unwrap();
|
|
1584
|
+
expect(row.projection).toBe(1);
|
|
1585
|
+
expect(row.left).toBe(-10);
|
|
1586
|
+
expect(row.right).toBe(10);
|
|
1587
|
+
expect(row.bottom).toBe(-10);
|
|
1588
|
+
expect(row.top).toBe(10);
|
|
1589
|
+
});
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// feat-20260519-tonemap-reinhard-mvp / M1 / T-M1.1 + T-M1.2.
|
|
1593
|
+
//
|
|
1594
|
+
// Camera tonemap field surface (AC-01 + AC-04 + AC-05 + AC-06). Three
|
|
1595
|
+
// schema columns (tonemap / exposure / whitePoint) plus a string-literal
|
|
1596
|
+
// closed union map (TONEMAP_NONE / TONEMAP_REINHARD_EXTENDED +
|
|
1597
|
+
// tonemapFromF32) following the same shape as projection +
|
|
1598
|
+
// cameraProjectionFromF32. Defaults: { tonemap: 0, exposure: 1.0,
|
|
1599
|
+
// whitePoint: 4.0 } belong to the layer-2 token defaults map (D-1 + D-7
|
|
1600
|
+
// in plan-strategy section 2.3). Spawn-time numeric range fail-fast is
|
|
1601
|
+
// out of scope per O1 (plan-decisions L-O1) — shader floor max(Y, 1e-5)
|
|
1602
|
+
// guards exposure / whitePoint == 0 from NaN.
|
|
1603
|
+
describe('Camera tonemap mapping (AC-01)', () => {
|
|
1604
|
+
it('TONEMAP_NONE === 0 + TONEMAP_REINHARD_EXTENDED === 1 (numeric encoding)', () => {
|
|
1605
|
+
expect(TONEMAP_NONE).toBe(0);
|
|
1606
|
+
expect(TONEMAP_REINHARD_EXTENDED).toBe(1);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
it('tonemapFromF32(0) === "none" + tonemapFromF32(1) === "reinhard-extended"', () => {
|
|
1610
|
+
expect(tonemapFromF32(0)).toBe('none');
|
|
1611
|
+
expect(tonemapFromF32(1)).toBe('reinhard-extended');
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it('tonemapFromF32 maps all 8 modes correctly', () => {
|
|
1615
|
+
expect(tonemapFromF32(2)).toBe('linear');
|
|
1616
|
+
expect(tonemapFromF32(3)).toBe('cineon');
|
|
1617
|
+
expect(tonemapFromF32(4)).toBe('aces-filmic');
|
|
1618
|
+
expect(tonemapFromF32(5)).toBe('agx');
|
|
1619
|
+
expect(tonemapFromF32(6)).toBe('neutral');
|
|
1620
|
+
expect(TONEMAP_REINHARD).toBe(7);
|
|
1621
|
+
expect(tonemapFromF32(TONEMAP_REINHARD)).toBe('reinhard');
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it('tonemapFromF32 falls back to "none" for out-of-range numeric (defensive)', () => {
|
|
1625
|
+
expect(tonemapFromF32(-1)).toBe('none');
|
|
1626
|
+
expect(tonemapFromF32(8)).toBe('none');
|
|
1627
|
+
expect(tonemapFromF32(NaN)).toBe('none');
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
describe('Camera 7-field perspective + opt-in tonemap spawn (AC-01 runtime)', () => {
|
|
1632
|
+
it('spawn with tonemap "reinhard-extended" path keeps numeric encoding 1', () => {
|
|
1633
|
+
const world = new World();
|
|
1634
|
+
const e = world
|
|
1635
|
+
.spawn({
|
|
1636
|
+
component: Camera,
|
|
1637
|
+
data: {
|
|
1638
|
+
fov: Math.PI / 4,
|
|
1639
|
+
aspect: 16 / 9,
|
|
1640
|
+
near: 0.1,
|
|
1641
|
+
far: 100,
|
|
1642
|
+
tonemap: TONEMAP_REINHARD_EXTENDED,
|
|
1643
|
+
exposure: 1.0,
|
|
1644
|
+
whitePoint: 4.0,
|
|
1645
|
+
},
|
|
1646
|
+
})
|
|
1647
|
+
.unwrap();
|
|
1648
|
+
const row = world.get(e, Camera).unwrap();
|
|
1649
|
+
expect(row.tonemap).toBe(TONEMAP_REINHARD_EXTENDED);
|
|
1650
|
+
expect(row.exposure).toBeCloseTo(1.0, 6);
|
|
1651
|
+
expect(row.whitePoint).toBeCloseTo(4.0, 6);
|
|
1652
|
+
// Layer-2: ortho quartet still defaulted.
|
|
1653
|
+
expect(row.left).toBe(-1);
|
|
1654
|
+
expect(row.right).toBe(1);
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
it('explicit non-default exposure + whitePoint pass through layer-1 wins', () => {
|
|
1658
|
+
const world = new World();
|
|
1659
|
+
const e = world
|
|
1660
|
+
.spawn({
|
|
1661
|
+
component: Camera,
|
|
1662
|
+
data: {
|
|
1663
|
+
fov: Math.PI / 4,
|
|
1664
|
+
aspect: 16 / 9,
|
|
1665
|
+
near: 0.1,
|
|
1666
|
+
far: 100,
|
|
1667
|
+
tonemap: TONEMAP_REINHARD_EXTENDED,
|
|
1668
|
+
exposure: 2.5,
|
|
1669
|
+
whitePoint: 8.0,
|
|
1670
|
+
},
|
|
1671
|
+
})
|
|
1672
|
+
.unwrap();
|
|
1673
|
+
const row = world.get(e, Camera).unwrap();
|
|
1674
|
+
expect(row.exposure).toBeCloseTo(2.5, 6);
|
|
1675
|
+
expect(row.whitePoint).toBeCloseTo(8.0, 6);
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// M2 / w9: Camera.perspective / Camera.orthographic static factory tests
|
|
1680
|
+
// (feat-20260525-boilerplate-reduction-pod-defaults-factories)
|
|
1681
|
+
//
|
|
1682
|
+
// Covers AC-05 + AC-06 (perspective + orthographic returns with defaults).
|
|
1683
|
+
// Plan-strategy section 5.2 unit-test requirement + section 5.3 testing points.
|
|
1684
|
+
describe('perspective factory', () => {
|
|
1685
|
+
it('perspective({ fov: 60, aspect: 4/3 }) returns POD with projection=0, near=0.1, far=100', () => {
|
|
1686
|
+
const pod = perspective({ fov: 60, aspect: 4 / 3 });
|
|
1687
|
+
expect(pod.fov).toBe(60);
|
|
1688
|
+
expect(pod.aspect).toBeCloseTo(4 / 3, 6);
|
|
1689
|
+
expect(pod.near).toBeCloseTo(0.1, 6);
|
|
1690
|
+
expect(pod.far).toBe(100);
|
|
1691
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_PERSPECTIVE);
|
|
1692
|
+
expect(pod.left).toBe(-1);
|
|
1693
|
+
expect(pod.right).toBe(1);
|
|
1694
|
+
expect(pod.bottom).toBe(-1);
|
|
1695
|
+
expect(pod.top).toBe(1);
|
|
1696
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1697
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1698
|
+
expect(pod.whitePoint).toBeCloseTo(4.0, 6);
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
it('perspective({ fov: 45, aspect: 16/9, near: 0.01, far: 1000 }) returns explicit overrides', () => {
|
|
1702
|
+
const pod = perspective({ fov: 45, aspect: 16 / 9, near: 0.01, far: 1000 });
|
|
1703
|
+
expect(pod.fov).toBe(45);
|
|
1704
|
+
expect(pod.aspect).toBeCloseTo(16 / 9, 6);
|
|
1705
|
+
expect(pod.near).toBeCloseTo(0.01, 6);
|
|
1706
|
+
expect(pod.far).toBe(1000);
|
|
1707
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_PERSPECTIVE);
|
|
1708
|
+
// Ortho quartet + tonemap get defaults.
|
|
1709
|
+
expect(pod.left).toBe(-1);
|
|
1710
|
+
expect(pod.right).toBe(1);
|
|
1711
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1712
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
it('perspective return value has all 20 Camera fields', () => {
|
|
1716
|
+
const pod = perspective({ fov: 60, aspect: 4 / 3 });
|
|
1717
|
+
const keys = Object.keys(pod).sort();
|
|
1718
|
+
expect(keys).toEqual([
|
|
1719
|
+
'antialias',
|
|
1720
|
+
'aspect',
|
|
1721
|
+
'autoAspect',
|
|
1722
|
+
'bloom',
|
|
1723
|
+
'bloomBlurRadius',
|
|
1724
|
+
'bloomIntensity',
|
|
1725
|
+
'bloomThreshold',
|
|
1726
|
+
'bottom',
|
|
1727
|
+
'clearColor',
|
|
1728
|
+
'exposure',
|
|
1729
|
+
'far',
|
|
1730
|
+
'fov',
|
|
1731
|
+
'historyVersion',
|
|
1732
|
+
'left',
|
|
1733
|
+
'near',
|
|
1734
|
+
'projection',
|
|
1735
|
+
'right',
|
|
1736
|
+
'tonemap',
|
|
1737
|
+
'top',
|
|
1738
|
+
'whitePoint',
|
|
1739
|
+
]);
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
describe('orthographic factory', () => {
|
|
1744
|
+
it('orthographic({ left: -10, right: 10, bottom: -10, top: 10 }) returns POD with projection=1, near=0.1, far=100', () => {
|
|
1745
|
+
const pod = orthographic({ left: -10, right: 10, bottom: -10, top: 10 });
|
|
1746
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_ORTHOGRAPHIC);
|
|
1747
|
+
expect(pod.left).toBe(-10);
|
|
1748
|
+
expect(pod.right).toBe(10);
|
|
1749
|
+
expect(pod.bottom).toBe(-10);
|
|
1750
|
+
expect(pod.top).toBe(10);
|
|
1751
|
+
expect(pod.near).toBeCloseTo(0.1, 6);
|
|
1752
|
+
expect(pod.far).toBe(100);
|
|
1753
|
+
// Perspective fields get sensible defaults (fov=0, aspect=1 for ortho).
|
|
1754
|
+
expect(pod.fov).toBe(0);
|
|
1755
|
+
expect(pod.aspect).toBe(1);
|
|
1756
|
+
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1757
|
+
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1758
|
+
expect(pod.whitePoint).toBeCloseTo(4.0, 6);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it('orthographic({ left: 0, right: 800, bottom: 600, top: 0, near: -1, far: 1 }) returns explicit overrides', () => {
|
|
1762
|
+
const pod = orthographic({
|
|
1763
|
+
left: 0,
|
|
1764
|
+
right: 800,
|
|
1765
|
+
bottom: 600,
|
|
1766
|
+
top: 0,
|
|
1767
|
+
near: -1,
|
|
1768
|
+
far: 1,
|
|
1769
|
+
});
|
|
1770
|
+
expect(pod.left).toBe(0);
|
|
1771
|
+
expect(pod.right).toBe(800);
|
|
1772
|
+
expect(pod.bottom).toBe(600);
|
|
1773
|
+
expect(pod.top).toBe(0);
|
|
1774
|
+
expect(pod.near).toBe(-1);
|
|
1775
|
+
expect(pod.far).toBe(1);
|
|
1776
|
+
expect(pod.projection).toBe(CAMERA_PROJECTION_ORTHOGRAPHIC);
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
{
|
|
1782
|
+
// --- from layer.test.ts ---
|
|
1783
|
+
// feat-20260525-boilerplate-reduction-pod-defaults-factories / M1 / w3b.
|
|
1784
|
+
//
|
|
1785
|
+
// Layer defaults fill regression barrier (AC-04 sweep table #14).
|
|
1786
|
+
// Three concerns:
|
|
1787
|
+
//
|
|
1788
|
+
// (a) `componentDefinition(Layer).defaults` is a frozen map asserting `{ value: 0 }`
|
|
1789
|
+
// (default game layer, charter P1 progressive disclosure).
|
|
1790
|
+
//
|
|
1791
|
+
// (b) `world.spawn({ component: Layer, data: {} })` yields `value === 0`
|
|
1792
|
+
// (the default game layer convention).
|
|
1793
|
+
//
|
|
1794
|
+
// (c) `world.spawn` with an explicit `value: 100` preserves the explicit
|
|
1795
|
+
// value -- layer-2 defaults do NOT overwrite layer-1 explicit input.
|
|
1796
|
+
//
|
|
1797
|
+
// Anchors: requirements AC-04 (sweep table #14 Layer defaults-added);
|
|
1798
|
+
// plan-strategy section 2 sweep pre-judgment table.
|
|
1799
|
+
|
|
1800
|
+
describe('componentDefinition(Layer).defaults -- frozen map assertion (AC-04 #14)', () => {
|
|
1801
|
+
it('componentDefinition(Layer).defaults equals { value: 0 }', () => {
|
|
1802
|
+
expect(componentDefinition(Layer).defaults).toEqual({ value: 0 });
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
it('componentDefinition(Layer).defaults is deep-frozen (Object.isFrozen returns true)', () => {
|
|
1806
|
+
expect(componentDefinition(Layer).defaults).toBeDefined();
|
|
1807
|
+
expect(Object.isFrozen(componentDefinition(Layer).defaults as object)).toBe(true);
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
describe('Layer spawn with data: {} -- default game layer (AC-04)', () => {
|
|
1812
|
+
it('spawn Layer with data: {} yields value === 0', () => {
|
|
1813
|
+
const world = new World();
|
|
1814
|
+
const e = world.spawn({ component: Layer, data: {} }).unwrap();
|
|
1815
|
+
const row = world.get(e, Layer).unwrap();
|
|
1816
|
+
|
|
1817
|
+
expect(row.value).toBe(0);
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
describe('Layer spawn with explicit value -- layer-1 wins (AC-04)', () => {
|
|
1822
|
+
it('spawn Layer with explicit value: 100 preserves the explicit value', () => {
|
|
1823
|
+
const world = new World();
|
|
1824
|
+
const e = world.spawn({ component: Layer, data: { value: 100 } }).unwrap();
|
|
1825
|
+
const row = world.get(e, Layer).unwrap();
|
|
1826
|
+
|
|
1827
|
+
expect(row.value).toBe(100);
|
|
1828
|
+
});
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
{
|
|
1833
|
+
// --- from mesh-renderer.test.ts ---
|
|
1834
|
+
// feat-20260608-mesh-multi-section-primitive-multi-material-slot M2 / w8.
|
|
1835
|
+
//
|
|
1836
|
+
// MeshRenderer schema migration barrier: material field removed, materials
|
|
1837
|
+
// array added.
|
|
1838
|
+
//
|
|
1839
|
+
// (a) `componentDefinition(MeshRenderer).defaults` is a frozen map asserting
|
|
1840
|
+
// `{ materials: [] }` (empty array routes to D-Q7 case B default
|
|
1841
|
+
// material path).
|
|
1842
|
+
//
|
|
1843
|
+
// (b) `world.spawn({ component: MeshRenderer, data: {} })` produces a row
|
|
1844
|
+
// with `materials === []` (the D-Q7 case B path, mid-grey default).
|
|
1845
|
+
//
|
|
1846
|
+
// (c) `world.spawn` with an explicit materials array round-trips: write
|
|
1847
|
+
// and read-back the materials array.
|
|
1848
|
+
//
|
|
1849
|
+
// (d) `world.spawn` with the old `material` field (singular) is a TS
|
|
1850
|
+
// compile-time error (verified via test-d.ts).
|
|
1851
|
+
|
|
1852
|
+
describe('componentDefinition(MeshRenderer).defaults — frozen map assertion (w8)', () => {
|
|
1853
|
+
it('componentDefinition(MeshRenderer).defaults equals { materials: [] }', () => {
|
|
1854
|
+
expect(componentDefinition(MeshRenderer).defaults).toEqual({ materials: [] });
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
it('componentDefinition(MeshRenderer).defaults is deep-frozen (Object.isFrozen returns true)', () => {
|
|
1858
|
+
expect(componentDefinition(MeshRenderer).defaults).toBeDefined();
|
|
1859
|
+
expect(Object.isFrozen(componentDefinition(MeshRenderer).defaults as object)).toBe(true);
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
describe('MeshRenderer spawn with data: {} — D-Q7 case B path (w8)', () => {
|
|
1864
|
+
it('spawn MeshRenderer with data: {} produces materials column === empty Uint32Array', () => {
|
|
1865
|
+
const world = new World();
|
|
1866
|
+
const e = world.spawn({ component: MeshRenderer, data: {} }).unwrap();
|
|
1867
|
+
const row = world.get(e, MeshRenderer).unwrap();
|
|
1868
|
+
|
|
1869
|
+
// materials column defaults to empty array for D-Q7 case B;
|
|
1870
|
+
// runtime type is Uint32Array (handles stored as u32).
|
|
1871
|
+
expect(row.materials).toBeInstanceOf(Uint32Array);
|
|
1872
|
+
expect((row.materials as unknown as Uint32Array).length).toBe(0);
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
describe('MeshRenderer spawn with explicit materials array — round-trip (w8)', () => {
|
|
1877
|
+
it('spawn with explicit materials array preserves the handle values as Uint32Array', () => {
|
|
1878
|
+
const world = new World();
|
|
1879
|
+
|
|
1880
|
+
const matHandle = world.allocSharedRef('MaterialAsset', {
|
|
1881
|
+
kind: 'material',
|
|
1882
|
+
baseColor: [1, 0, 0, 1],
|
|
1883
|
+
passes: [{ name: 'forward', program: { module: 'project::standard' } }],
|
|
1884
|
+
} as never);
|
|
1885
|
+
|
|
1886
|
+
expect(matHandle).toBeGreaterThan(0);
|
|
1887
|
+
|
|
1888
|
+
const e = world.spawn({ component: MeshRenderer, data: { materials: [matHandle] } }).unwrap();
|
|
1889
|
+
const row = world.get(e, MeshRenderer).unwrap();
|
|
1890
|
+
|
|
1891
|
+
// Runtime: Uint32Array with the handle value
|
|
1892
|
+
expect(row.materials).toBeInstanceOf(Uint32Array);
|
|
1893
|
+
expect((row.materials as unknown as Uint32Array)[0]).toBe(matHandle);
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
it('spawn with multiple materials round-trips Uint32Array', () => {
|
|
1897
|
+
const world = new World();
|
|
1898
|
+
|
|
1899
|
+
const m0 = world.allocSharedRef('MaterialAsset', {
|
|
1900
|
+
kind: 'material',
|
|
1901
|
+
baseColor: [1, 0, 0, 1],
|
|
1902
|
+
} as never);
|
|
1903
|
+
const m1 = world.allocSharedRef('MaterialAsset', {
|
|
1904
|
+
kind: 'material',
|
|
1905
|
+
baseColor: [0, 1, 0, 1],
|
|
1906
|
+
} as never);
|
|
1907
|
+
|
|
1908
|
+
const e = world.spawn({ component: MeshRenderer, data: { materials: [m0, m1] } }).unwrap();
|
|
1909
|
+
const row = world.get(e, MeshRenderer).unwrap();
|
|
1910
|
+
|
|
1911
|
+
expect(row.materials).toBeInstanceOf(Uint32Array);
|
|
1912
|
+
expect((row.materials as unknown as Uint32Array).length).toBe(2);
|
|
1913
|
+
expect((row.materials as unknown as Uint32Array)[0]).toBe(m0);
|
|
1914
|
+
expect((row.materials as unknown as Uint32Array)[1]).toBe(m1);
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
{
|
|
1920
|
+
// --- from skybox-background.test.ts ---
|
|
1921
|
+
// feat-20260531-skybox-env-background / M1 / w2.
|
|
1922
|
+
//
|
|
1923
|
+
// SkyboxBackground component unit tests (TDD red phase -- the component
|
|
1924
|
+
// source file does not exist yet; this test will turn green after w1
|
|
1925
|
+
// creates skybox-background.ts, barrel, and top-level re-export).
|
|
1926
|
+
//
|
|
1927
|
+
// Covers:
|
|
1928
|
+
// (a) Spawning an entity with SkyboxBackground can be queried via
|
|
1929
|
+
// world.query row iteration (AC-01).
|
|
1930
|
+
// (b) skyboxModeFromF32(0) returns 'cubemap' with TS narrow type
|
|
1931
|
+
// (no `as` cast, AC-01).
|
|
1932
|
+
// (c) SKYBOX_MODE_CUBEMAP === 0 invariant (non-zero sentinel guard,
|
|
1933
|
+
// AC-01).
|
|
1934
|
+
// (d) defaults: { mode: SKYBOX_MODE_CUBEMAP } takes effect -- spawn
|
|
1935
|
+
// without `mode` yields mode === 0 (AC-01).
|
|
1936
|
+
// (e) The mapper function uses exhaustive switch (no `default` branch,
|
|
1937
|
+
// AC-02).
|
|
1938
|
+
//
|
|
1939
|
+
// Anchors: requirements AC-01 / AC-02; plan-strategy D-5 (f32 enum
|
|
1940
|
+
// column + mapper); research Finding 6 (camera.ts:51,105,145,221-237
|
|
1941
|
+
// cameraProjectionFromF32 pattern); plan-tasks.json w2 acceptanceCheck.
|
|
1942
|
+
|
|
1943
|
+
describe('SkyboxBackground — component schema (AC-01)', () => {
|
|
1944
|
+
it('SKYBOX_MODE_CUBEMAP equals 0 (non-zero guard)', () => {
|
|
1945
|
+
expect(SKYBOX_MODE_CUBEMAP).toBe(0);
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
it('SkyboxBackground is a defineComponent token', () => {
|
|
1949
|
+
expect(SkyboxBackground).toBeDefined();
|
|
1950
|
+
expect(typeof SkyboxBackground.name).toBe('string');
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
it('defaults: { mode: SKYBOX_MODE_CUBEMAP } yields mode===0 on spawn without mode', () => {
|
|
1954
|
+
const world = new World();
|
|
1955
|
+
const e = world
|
|
1956
|
+
.spawn({
|
|
1957
|
+
component: SkyboxBackground,
|
|
1958
|
+
data: {
|
|
1959
|
+
equirect: 42 as unknown as never, // Handle<EquirectAsset> stored as u32
|
|
1960
|
+
},
|
|
1961
|
+
})
|
|
1962
|
+
.unwrap();
|
|
1963
|
+
const row = world.get(e, SkyboxBackground).unwrap();
|
|
1964
|
+
expect(row.mode).toBe(SKYBOX_MODE_CUBEMAP);
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
it('entity with SkyboxBackground is hit by a single-component query', () => {
|
|
1968
|
+
const world = new World();
|
|
1969
|
+
world.spawn({
|
|
1970
|
+
component: SkyboxBackground,
|
|
1971
|
+
data: {
|
|
1972
|
+
equirect: 7 as unknown as never,
|
|
1973
|
+
mode: SKYBOX_MODE_CUBEMAP,
|
|
1974
|
+
},
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
const query = world.query({ with: [SkyboxBackground] }).unwrap();
|
|
1978
|
+
expect([...query].length).toBe(1);
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
it('world.get returns the equirect handle (u32-stored)', () => {
|
|
1982
|
+
const world = new World();
|
|
1983
|
+
const handle = 99;
|
|
1984
|
+
const e = world
|
|
1985
|
+
.spawn({
|
|
1986
|
+
component: SkyboxBackground,
|
|
1987
|
+
data: { equirect: handle as unknown as never, mode: 0 },
|
|
1988
|
+
})
|
|
1989
|
+
.unwrap();
|
|
1990
|
+
const row = world.get(e, SkyboxBackground).unwrap();
|
|
1991
|
+
expect(row.equirect).toBe(handle);
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
describe('skyboxModeFromF32 — mapper (AC-01 / AC-02)', () => {
|
|
1996
|
+
it('skyboxModeFromF32(0) returns "cubemap" (string literal, not widened to string)', () => {
|
|
1997
|
+
const result = skyboxModeFromF32(0);
|
|
1998
|
+
expect(result).toBe('cubemap');
|
|
1999
|
+
// TS narrow: type is literal 'cubemap', not widened `string`.
|
|
2000
|
+
expectTypeOf(result).toEqualTypeOf<'cubemap'>();
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
it('skyboxModeFromF32(unknown) also returns "cubemap" (only mode exists)', () => {
|
|
2004
|
+
// Currently only one mode exists; any value maps to 'cubemap'.
|
|
2005
|
+
const result = skyboxModeFromF32(99);
|
|
2006
|
+
expect(result).toBe('cubemap');
|
|
2007
|
+
expectTypeOf(result).toEqualTypeOf<'cubemap'>();
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
it('switch on SkyboxMode is exhaustive (no `as` cast in test)', () => {
|
|
2011
|
+
// This demonstrates that the consumer-side switch on SkyboxMode is
|
|
2012
|
+
// exhaustive -- when a future mode is added, TS will catch the missing
|
|
2013
|
+
// branch at compile time.
|
|
2014
|
+
const mode: SkyboxMode = skyboxModeFromF32(0);
|
|
2015
|
+
let hit = false;
|
|
2016
|
+
switch (mode) {
|
|
2017
|
+
case 'cubemap':
|
|
2018
|
+
hit = true;
|
|
2019
|
+
break;
|
|
2020
|
+
// No `default` branch -- TS enforces exhaustiveness when new
|
|
2021
|
+
// members are added to SkyboxMode.
|
|
2022
|
+
}
|
|
2023
|
+
expect(hit).toBe(true);
|
|
2024
|
+
});
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
{
|
|
2029
|
+
// --- from sort-key.test.ts ---
|
|
2030
|
+
// feat-20260525-boilerplate-reduction-pod-defaults-factories / M1 / w3c.
|
|
2031
|
+
//
|
|
2032
|
+
// SortKey defaults fill regression barrier (AC-04 sweep table #15).
|
|
2033
|
+
// Three concerns:
|
|
2034
|
+
//
|
|
2035
|
+
// (a) `componentDefinition(SortKey).defaults` is a frozen map asserting `{ value: 0 }`
|
|
2036
|
+
// (value 0 means 'no override, use mode formula' per transparent-sort
|
|
2037
|
+
// algorithm semantics, charter P1 progressive disclosure).
|
|
2038
|
+
//
|
|
2039
|
+
// (b) `world.spawn({ component: SortKey, data: {} })` yields `value === 0`
|
|
2040
|
+
// (the 'no override' sentinel).
|
|
2041
|
+
//
|
|
2042
|
+
// (c) `world.spawn` with an explicit `value: -100` preserves the explicit
|
|
2043
|
+
// value -- layer-2 defaults do NOT overwrite layer-1 explicit input.
|
|
2044
|
+
//
|
|
2045
|
+
// Anchors: requirements AC-04 (sweep table #15 SortKey defaults-added);
|
|
2046
|
+
// plan-strategy section 2 sweep pre-judgment table.
|
|
2047
|
+
|
|
2048
|
+
describe('componentDefinition(SortKey).defaults -- frozen map assertion (AC-04 #15)', () => {
|
|
2049
|
+
it('componentDefinition(SortKey).defaults equals { value: 0 }', () => {
|
|
2050
|
+
expect(componentDefinition(SortKey).defaults).toEqual({ value: 0 });
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
it('componentDefinition(SortKey).defaults is deep-frozen (Object.isFrozen returns true)', () => {
|
|
2054
|
+
expect(componentDefinition(SortKey).defaults).toBeDefined();
|
|
2055
|
+
expect(Object.isFrozen(componentDefinition(SortKey).defaults as object)).toBe(true);
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
describe('SortKey spawn with data: {} -- no-override sentinel (AC-04)', () => {
|
|
2060
|
+
it('spawn SortKey with data: {} yields value === 0', () => {
|
|
2061
|
+
const world = new World();
|
|
2062
|
+
const e = world.spawn({ component: SortKey, data: {} }).unwrap();
|
|
2063
|
+
const row = world.get(e, SortKey).unwrap();
|
|
2064
|
+
|
|
2065
|
+
expect(row.value).toBe(0);
|
|
2066
|
+
});
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
describe('SortKey spawn with explicit value -- layer-1 wins (AC-04)', () => {
|
|
2070
|
+
it('spawn SortKey with explicit value: -100 preserves the explicit value', () => {
|
|
2071
|
+
const world = new World();
|
|
2072
|
+
const e = world.spawn({ component: SortKey, data: { value: -100 } }).unwrap();
|
|
2073
|
+
const row = world.get(e, SortKey).unwrap();
|
|
2074
|
+
|
|
2075
|
+
expect(row.value).toBe(-100);
|
|
2076
|
+
});
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
{
|
|
2081
|
+
// --- from sprite-components-schema.test.ts ---
|
|
2082
|
+
// feat-20260521-sprite-atlas-animation / M2 / T-10.
|
|
2083
|
+
//
|
|
2084
|
+
// TDD red phase: this runtime suite exercises `world.spawn` + `world.get`
|
|
2085
|
+
// round-trips for the two new sprite-only ECS components — both of which
|
|
2086
|
+
// land in T-11 (SpriteRegionOverride) + T-12 (SpriteAnimation). Before
|
|
2087
|
+
// those impl tasks land the imports `../sprite-region-override` /
|
|
2088
|
+
// `../sprite-animation` fail to resolve and the suite stays red. After
|
|
2089
|
+
// T-11 + T-12 the imports resolve, `defineComponent` accepts the schema
|
|
2090
|
+
// keywords (no `SchemaUnsupportedFieldError` / `ManagedArrayElementType-
|
|
2091
|
+
// NotAllowedError`), and the spawn / get assertions turn green.
|
|
2092
|
+
//
|
|
2093
|
+
// Why a runtime test on top of the T-08 / T-09 type-d coverage?
|
|
2094
|
+
// Plan-strategy section 4 risk R-SCHEMA-1 + R-SCHEMA-2 explicitly cite the
|
|
2095
|
+
// ECS schema whitelist (`SchemaFieldType`) as the structural failure mode:
|
|
2096
|
+
// the type-d files prove the literals look right at TS edge, but the
|
|
2097
|
+
// runtime `defineComponent` call still has to walk the schema and accept
|
|
2098
|
+
// `'array<f32, 4>'` (fixed-length) + `'array<f32>'` (variable) + the
|
|
2099
|
+
// scalar `'u32'` / `'f32'` quartet without throwing. T-10 is the
|
|
2100
|
+
// behavioural witness for those two risks.
|
|
2101
|
+
//
|
|
2102
|
+
// Default-value coverage (component-default-fallback layer-3, AC-02 +
|
|
2103
|
+
// requirements section 2.3 defaults column):
|
|
2104
|
+
// - `currentFrame: 'u32'` -> 0 (scalar layer-3 fallback)
|
|
2105
|
+
// - `accumDt: 'f32'` -> 0 (scalar layer-3 fallback)
|
|
2106
|
+
// - `playbackMode: 'u32'` -> 0 (= SPRITE_PLAYBACK_MODE_LOOP, the
|
|
2107
|
+
// default playbackMode per AC-02 +
|
|
2108
|
+
// requirements section 2.3 footnote
|
|
2109
|
+
// "default 'loop'")
|
|
2110
|
+
// The spawn payload thus only needs to carry frameCount / frameDuration /
|
|
2111
|
+
// regions for an entity to be observable; AI users get a meaningful
|
|
2112
|
+
// 4-step minimal walk-cycle by passing one Float32Array of length
|
|
2113
|
+
// frameCount * 4.
|
|
2114
|
+
//
|
|
2115
|
+
// Anchors: plan-tasks.json T-10 (acceptanceCheck: T-11 + T-12 land then
|
|
2116
|
+
// region round-trip + 6-field default-value + length round-trip + no
|
|
2117
|
+
// SchemaUnsupportedFieldError); plan-strategy section 4 R-SCHEMA-1 + R-
|
|
2118
|
+
// SCHEMA-2 reaction; research F-5; requirements section AC-01 + AC-02 +
|
|
2119
|
+
// section 2.3 (defaults column) + section 2.4; charter F1 + P3.
|
|
2120
|
+
|
|
2121
|
+
describe('SpriteRegionOverride — defineComponent does not throw (R-SCHEMA-2)', () => {
|
|
2122
|
+
it("name + schema lock match the M2 D-6 contract (region: 'array<f32, 4>')", () => {
|
|
2123
|
+
expect(SpriteRegionOverride.name).toBe('SpriteRegionOverride');
|
|
2124
|
+
expect(Object.keys(componentSchema(SpriteRegionOverride)).length).toBe(1);
|
|
2125
|
+
expect((componentSchema(SpriteRegionOverride) as Record<string, unknown>).region).toBe(
|
|
2126
|
+
'array<f32, 4>',
|
|
2127
|
+
);
|
|
2128
|
+
});
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
describe('SpriteRegionOverride — spawn + get round-trip (AC-01 + AC-03 producer)', () => {
|
|
2132
|
+
it('region [0.5, 0, 0.5, 1] round-trips byte-for-byte', () => {
|
|
2133
|
+
const world = new World();
|
|
2134
|
+
const region = new Float32Array([0.5, 0, 0.5, 1]);
|
|
2135
|
+
const e = world.spawn({ component: SpriteRegionOverride, data: { region } }).unwrap();
|
|
2136
|
+
const snap = world.get(e, SpriteRegionOverride).unwrap();
|
|
2137
|
+
expect(snap.region).toBeInstanceOf(Float32Array);
|
|
2138
|
+
expect(snap.region.length).toBe(4);
|
|
2139
|
+
expect(snap.region[0]).toBeCloseTo(0.5, 6);
|
|
2140
|
+
expect(snap.region[1]).toBeCloseTo(0, 6);
|
|
2141
|
+
expect(snap.region[2]).toBeCloseTo(0.5, 6);
|
|
2142
|
+
expect(snap.region[3]).toBeCloseTo(1, 6);
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
it('region overwrite via world.set updates the snapshot', () => {
|
|
2146
|
+
const world = new World();
|
|
2147
|
+
const e = world
|
|
2148
|
+
.spawn({
|
|
2149
|
+
component: SpriteRegionOverride,
|
|
2150
|
+
data: { region: new Float32Array([0, 0, 1, 1]) },
|
|
2151
|
+
})
|
|
2152
|
+
.unwrap();
|
|
2153
|
+
|
|
2154
|
+
world
|
|
2155
|
+
.set(e, SpriteRegionOverride, { region: new Float32Array([0.25, 0.25, 0.5, 0.5]) })
|
|
2156
|
+
.unwrap();
|
|
2157
|
+
|
|
2158
|
+
const snap = world.get(e, SpriteRegionOverride).unwrap();
|
|
2159
|
+
expect(snap.region.length).toBe(4);
|
|
2160
|
+
expect(snap.region[0]).toBeCloseTo(0.25, 6);
|
|
2161
|
+
expect(snap.region[2]).toBeCloseTo(0.5, 6);
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
describe('SpriteAnimation — defineComponent does not throw (R-SCHEMA-1)', () => {
|
|
2166
|
+
it('name + schema lock match the M2 D-5 + D-6 contract (6 fields)', () => {
|
|
2167
|
+
expect(SpriteAnimation.name).toBe('SpriteAnimation');
|
|
2168
|
+
|
|
2169
|
+
const schema = componentSchema(SpriteAnimation) as Record<string, unknown>;
|
|
2170
|
+
expect(Object.keys(schema).length).toBe(6);
|
|
2171
|
+
expect(schema.frameCount).toBe('u32');
|
|
2172
|
+
expect(schema.frameDuration).toBe('f32');
|
|
2173
|
+
expect(schema.currentFrame).toBe('u32');
|
|
2174
|
+
expect(schema.accumDt).toBe('f32');
|
|
2175
|
+
expect(schema.regions).toBe('array<f32>');
|
|
2176
|
+
expect(schema.playbackMode).toBe('u32');
|
|
2177
|
+
});
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
describe('SpriteAnimation — spawn + get round-trip (AC-02 + section 2.3)', () => {
|
|
2181
|
+
it('4-frame walk cycle round-trips through ECS columns', () => {
|
|
2182
|
+
const world = new World();
|
|
2183
|
+
const frameCount = 4;
|
|
2184
|
+
const regions = new Float32Array(frameCount * 4);
|
|
2185
|
+
for (let i = 0; i < frameCount; i++) {
|
|
2186
|
+
regions[i * 4 + 0] = i * 0.25;
|
|
2187
|
+
regions[i * 4 + 1] = 0;
|
|
2188
|
+
regions[i * 4 + 2] = 0.25;
|
|
2189
|
+
regions[i * 4 + 3] = 1;
|
|
2190
|
+
}
|
|
2191
|
+
const e = world
|
|
2192
|
+
.spawn({
|
|
2193
|
+
component: SpriteAnimation,
|
|
2194
|
+
data: {
|
|
2195
|
+
frameCount,
|
|
2196
|
+
frameDuration: 0.1,
|
|
2197
|
+
currentFrame: 0,
|
|
2198
|
+
accumDt: 0,
|
|
2199
|
+
regions,
|
|
2200
|
+
playbackMode: SPRITE_PLAYBACK_MODE_LOOP,
|
|
2201
|
+
},
|
|
2202
|
+
})
|
|
2203
|
+
.unwrap();
|
|
2204
|
+
const snap = world.get(e, SpriteAnimation).unwrap();
|
|
2205
|
+
expect(snap.frameCount).toBe(frameCount);
|
|
2206
|
+
expect(snap.frameDuration).toBeCloseTo(0.1, 6);
|
|
2207
|
+
expect(snap.currentFrame).toBe(0);
|
|
2208
|
+
expect(snap.accumDt).toBeCloseTo(0, 6);
|
|
2209
|
+
expect(snap.regions).toBeInstanceOf(Float32Array);
|
|
2210
|
+
expect(snap.regions.length).toBe(frameCount * 4);
|
|
2211
|
+
expect(snap.regions[2 * 4 + 0]).toBeCloseTo(0.5, 6);
|
|
2212
|
+
expect(snap.playbackMode).toBe(SPRITE_PLAYBACK_MODE_LOOP);
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
it('clamp playback mode encodes as numeric 1 in the column', () => {
|
|
2216
|
+
const world = new World();
|
|
2217
|
+
const e = world
|
|
2218
|
+
.spawn({
|
|
2219
|
+
component: SpriteAnimation,
|
|
2220
|
+
data: {
|
|
2221
|
+
frameCount: 3,
|
|
2222
|
+
frameDuration: 0.1,
|
|
2223
|
+
regions: new Float32Array(3 * 4),
|
|
2224
|
+
playbackMode: SPRITE_PLAYBACK_MODE_CLAMP,
|
|
2225
|
+
},
|
|
2226
|
+
})
|
|
2227
|
+
.unwrap();
|
|
2228
|
+
const snap = world.get(e, SpriteAnimation).unwrap();
|
|
2229
|
+
expect(snap.playbackMode).toBe(1);
|
|
2230
|
+
expect(snap.playbackMode).toBe(SPRITE_PLAYBACK_MODE_CLAMP);
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
it('layer-3 defaults fill currentFrame=0 / accumDt=0 / playbackMode=0 when omitted', () => {
|
|
2234
|
+
const world = new World();
|
|
2235
|
+
const e = world
|
|
2236
|
+
.spawn({
|
|
2237
|
+
component: SpriteAnimation,
|
|
2238
|
+
data: {
|
|
2239
|
+
frameCount: 2,
|
|
2240
|
+
frameDuration: 0.05,
|
|
2241
|
+
regions: new Float32Array([0, 0, 0.5, 1, 0.5, 0, 0.5, 1]),
|
|
2242
|
+
},
|
|
2243
|
+
})
|
|
2244
|
+
.unwrap();
|
|
2245
|
+
const snap = world.get(e, SpriteAnimation).unwrap();
|
|
2246
|
+
expect(snap.currentFrame).toBe(0);
|
|
2247
|
+
expect(snap.accumDt).toBeCloseTo(0, 6);
|
|
2248
|
+
expect(snap.playbackMode).toBe(SPRITE_PLAYBACK_MODE_LOOP);
|
|
2249
|
+
expect(snap.regions.length).toBe(2 * 4);
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
it('variable regions length round-trip — frameCount * 4 invariant is data not schema', () => {
|
|
2253
|
+
// Schema does not constrain regions.length; the M4 sprite-animation-tick
|
|
2254
|
+
// system enforces `regions.length === frameCount * 4` at first
|
|
2255
|
+
// observation (D-1 fail-fast path). This test only locks the bytes
|
|
2256
|
+
// round-trip — feeding 12 floats with frameCount === 3 returns 12 floats.
|
|
2257
|
+
const world = new World();
|
|
2258
|
+
const regions = new Float32Array([0, 0, 0.33, 1, 0.33, 0, 0.33, 1, 0.66, 0, 0.34, 1]);
|
|
2259
|
+
const e = world
|
|
2260
|
+
.spawn({
|
|
2261
|
+
component: SpriteAnimation,
|
|
2262
|
+
data: {
|
|
2263
|
+
frameCount: 3,
|
|
2264
|
+
frameDuration: 0.2,
|
|
2265
|
+
regions,
|
|
2266
|
+
},
|
|
2267
|
+
})
|
|
2268
|
+
.unwrap();
|
|
2269
|
+
const snap = world.get(e, SpriteAnimation).unwrap();
|
|
2270
|
+
expect(snap.regions.length).toBe(12);
|
|
2271
|
+
expect(snap.regions[4]).toBeCloseTo(0.33, 6);
|
|
2272
|
+
});
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
{
|
|
2277
|
+
// --- from sprite-playback-mode.test.ts ---
|
|
2278
|
+
// feat-20260521-sprite-atlas-animation / M1 / T-06.
|
|
2279
|
+
//
|
|
2280
|
+
// TDD red phase: packages/runtime/src/components/sprite-playback-mode.ts
|
|
2281
|
+
// does not yet exist — these constant + mapper assertions stay red until
|
|
2282
|
+
// T-07 lands the SSOT (plan-strategy section 2 D-5 / requirements
|
|
2283
|
+
// section AC-02 + section 2.3).
|
|
2284
|
+
//
|
|
2285
|
+
// The shape mirrors `packages/render/src/components/camera.ts:72-90`
|
|
2286
|
+
// Tonemap block (TONEMAP_NONE = 0 / TONEMAP_REINHARD_EXTENDED = 1 +
|
|
2287
|
+
// `type Tonemap = 'none' | 'reinhard-extended'` + `tonemapFromF32`)
|
|
2288
|
+
// because ECS schema whitelist SchemaFieldType does not accept string-
|
|
2289
|
+
// literal unions for `playbackMode` (research F-2 + F-5). The mapper
|
|
2290
|
+
// pattern lets the ECS column stay `'u32'` while AI users still consume
|
|
2291
|
+
// `'loop' | 'clamp'` literal-union narrowing in TS land.
|
|
2292
|
+
//
|
|
2293
|
+
// Anchors: plan-strategy section 2 D-5 + section 3.1 PR block SPM (sprite-
|
|
2294
|
+
// playback-mode) + section 4 risk R-SCHEMA-1 reaction; research
|
|
2295
|
+
// F-2 + F-5; requirements section AC-02 + section 2.3 playbackMode
|
|
2296
|
+
// row; charter P4 consistent abstraction (same shape as M1
|
|
2297
|
+
// tonemap-encoding so AI users keep one mental model).
|
|
2298
|
+
|
|
2299
|
+
describe('sprite-playback-mode SSOT — constants', () => {
|
|
2300
|
+
it('SPRITE_PLAYBACK_MODE_LOOP === 0 (u32 column encoding)', () => {
|
|
2301
|
+
expect(SPRITE_PLAYBACK_MODE_LOOP).toBe(0);
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
it('SPRITE_PLAYBACK_MODE_CLAMP === 1 (u32 column encoding)', () => {
|
|
2305
|
+
expect(SPRITE_PLAYBACK_MODE_CLAMP).toBe(1);
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
it('constants are typed as numeric literals (not widened to number)', () => {
|
|
2309
|
+
expectTypeOf(SPRITE_PLAYBACK_MODE_LOOP).toEqualTypeOf<0>();
|
|
2310
|
+
expectTypeOf(SPRITE_PLAYBACK_MODE_CLAMP).toEqualTypeOf<1>();
|
|
2311
|
+
});
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
describe('sprite-playback-mode SSOT — SpritePlaybackMode type', () => {
|
|
2315
|
+
it("SpritePlaybackMode is exactly 'loop' | 'clamp'", () => {
|
|
2316
|
+
expectTypeOf<SpritePlaybackMode>().toEqualTypeOf<'loop' | 'clamp'>();
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
it("'loop' is assignable to SpritePlaybackMode", () => {
|
|
2320
|
+
const mode: SpritePlaybackMode = 'loop';
|
|
2321
|
+
expect(mode).toBe('loop');
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
it("'clamp' is assignable to SpritePlaybackMode", () => {
|
|
2325
|
+
const mode: SpritePlaybackMode = 'clamp';
|
|
2326
|
+
expect(mode).toBe('clamp');
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
describe('spritePlaybackModeFromU32 — u32 to string-literal translator', () => {
|
|
2331
|
+
it('spritePlaybackModeFromU32(0) === "loop"', () => {
|
|
2332
|
+
expect(spritePlaybackModeFromU32(0)).toBe('loop');
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
it('spritePlaybackModeFromU32(1) === "clamp"', () => {
|
|
2336
|
+
expect(spritePlaybackModeFromU32(1)).toBe('clamp');
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
it('out-of-range numerics fall back to "loop" (charter P4 no silent exception)', () => {
|
|
2340
|
+
// Mirror cameraProjectionFromF32 / tonemapFromF32: defensive default
|
|
2341
|
+
// for uninitialised / stale numeric values. The schema layer already
|
|
2342
|
+
// guarantees the column carries a u32, but AI users may pass through a
|
|
2343
|
+
// hand-set numeric that escapes the [0, 1] range.
|
|
2344
|
+
expect(spritePlaybackModeFromU32(2)).toBe('loop');
|
|
2345
|
+
expect(spritePlaybackModeFromU32(-1)).toBe('loop');
|
|
2346
|
+
expect(spritePlaybackModeFromU32(NaN)).toBe('loop');
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
it('return type narrows to SpritePlaybackMode (string-literal union)', () => {
|
|
2350
|
+
expectTypeOf(spritePlaybackModeFromU32(0)).toEqualTypeOf<SpritePlaybackMode>();
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
it('round-trip: SPRITE_PLAYBACK_MODE_LOOP -> "loop" / SPRITE_PLAYBACK_MODE_CLAMP -> "clamp"', () => {
|
|
2354
|
+
expect(spritePlaybackModeFromU32(SPRITE_PLAYBACK_MODE_LOOP)).toBe('loop');
|
|
2355
|
+
expect(spritePlaybackModeFromU32(SPRITE_PLAYBACK_MODE_CLAMP)).toBe('clamp');
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
{
|
|
2361
|
+
// --- from transform.test.ts ---
|
|
2362
|
+
// feat-20260525-boilerplate-reduction-pod-defaults-factories / M1 / w1.
|
|
2363
|
+
//
|
|
2364
|
+
// Transform defaults fill regression barrier (AC-02 + AC-03). Three
|
|
2365
|
+
// concerns:
|
|
2366
|
+
//
|
|
2367
|
+
// (a) `world.spawn({ component: Transform, data: {} })` returns identity
|
|
2368
|
+
// transform (pos/quat/scale all identity values), verifying that the
|
|
2369
|
+
// layer-2 defaults map fills every schema field.
|
|
2370
|
+
//
|
|
2371
|
+
// (b) `world.spawn({ component: Transform, data: { pos: [0, 0, 2] } })`
|
|
2372
|
+
// returns pos=[0,0,2] with remaining fields at identity defaults,
|
|
2373
|
+
// verifying that layer-1 explicit values take priority over layer-2
|
|
2374
|
+
// defaults.
|
|
2375
|
+
//
|
|
2376
|
+
// (c) `world.set` partial patch on Transform does not affect unfilled
|
|
2377
|
+
// columns, verifying the column-wise set semantics.
|
|
2378
|
+
//
|
|
2379
|
+
// Anchors: requirements AC-02 / AC-03; plan-strategy section 5.1 TDD
|
|
2380
|
+
// red-green-refactor + section 5.2 unit test requirements.
|
|
2381
|
+
|
|
2382
|
+
describe('Transform defaults fill — identity spawn (AC-02)', () => {
|
|
2383
|
+
it('world.spawn({ component: Transform, data: {} }) returns identity transform', () => {
|
|
2384
|
+
const world = new World();
|
|
2385
|
+
const e = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
2386
|
+
const row = world.get(e, Transform).unwrap();
|
|
2387
|
+
|
|
2388
|
+
expect(Array.from(row.pos)).toEqual([0, 0, 0]);
|
|
2389
|
+
expect(Array.from(row.quat)).toEqual([0, 0, 0, 1]);
|
|
2390
|
+
expect(Array.from(row.scale)).toEqual([1, 1, 1]);
|
|
2391
|
+
});
|
|
2392
|
+
});
|
|
2393
|
+
|
|
2394
|
+
describe('Transform partial override — layer-1 wins over layer-2 defaults (AC-03)', () => {
|
|
2395
|
+
it('world.spawn with only pos=[0,0,2] yields identity on all other fields', () => {
|
|
2396
|
+
const world = new World();
|
|
2397
|
+
const e = world.spawn({ component: Transform, data: { pos: [0, 0, 2] } }).unwrap();
|
|
2398
|
+
const row = world.get(e, Transform).unwrap();
|
|
2399
|
+
|
|
2400
|
+
expect(Array.from(row.pos)).toEqual([0, 0, 2]);
|
|
2401
|
+
expect(Array.from(row.quat)).toEqual([0, 0, 0, 1]);
|
|
2402
|
+
expect(Array.from(row.scale)).toEqual([1, 1, 1]);
|
|
2403
|
+
});
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
describe('Transform new schema — local pos/quat/scale array cols + world array<f32,16> (AC-01)', () => {
|
|
2407
|
+
it('spawn data:{} yields identity local TRS + identity world mat4 (16 contiguous f32)', () => {
|
|
2408
|
+
const world = new World();
|
|
2409
|
+
const e = world.spawn({ component: Transform, data: {} }).unwrap();
|
|
2410
|
+
const row = world.get(e, Transform).unwrap();
|
|
2411
|
+
|
|
2412
|
+
// local pos/quat/scale inline array columns at identity.
|
|
2413
|
+
expect(Array.from(row.pos)).toEqual([0, 0, 0]);
|
|
2414
|
+
expect(Array.from(row.quat)).toEqual([0, 0, 0, 1]);
|
|
2415
|
+
expect(Array.from(row.scale)).toEqual([1, 1, 1]);
|
|
2416
|
+
|
|
2417
|
+
// world array<f32,16> resolves to a Float32Array view of 16 contiguous
|
|
2418
|
+
// floats. Default fill is the identity mat4 (column-major).
|
|
2419
|
+
const w = row.world as Float32Array;
|
|
2420
|
+
expect(w).toBeInstanceOf(Float32Array);
|
|
2421
|
+
expect(w.length).toBe(16);
|
|
2422
|
+
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2423
|
+
for (let i = 0; i < 16; i++) {
|
|
2424
|
+
expect(w[i]).toBeCloseTo(identity[i] as number, 5);
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
describe('Transform world.set — partial patch does not affect unfilled columns', () => {
|
|
2430
|
+
it('world.set partial patch on Transform leaves unreferenced columns unchanged', () => {
|
|
2431
|
+
const world = new World();
|
|
2432
|
+
const e = world.spawn({ component: Transform, data: { pos: [5, 3, 10] } }).unwrap();
|
|
2433
|
+
|
|
2434
|
+
// Partial set: only the pos column (the patch unit is the whole inline
|
|
2435
|
+
// array field; quat/scale columns are not referenced).
|
|
2436
|
+
world.set(e, Transform, { pos: [25, 3, 10] }).unwrap();
|
|
2437
|
+
|
|
2438
|
+
const after = world.get(e, Transform).unwrap();
|
|
2439
|
+
expect(Array.from(after.pos)).toEqual([25, 3, 10]);
|
|
2440
|
+
// Identity defaults still in place for unset columns.
|
|
2441
|
+
expect(Array.from(after.quat)).toEqual([0, 0, 0, 1]);
|
|
2442
|
+
expect(Array.from(after.scale)).toEqual([1, 1, 1]);
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
{
|
|
2448
|
+
// --- from mesh-renderer-multi-material.test.ts ---
|
|
2449
|
+
// mesh-renderer-multi-material.test.ts — unit tests for render-system-extract
|
|
2450
|
+
// count-mismatch validation (feat-20260608-mesh-multi-section-primitive-multi-material-slot
|
|
2451
|
+
// M2 / w12).
|
|
2452
|
+
//
|
|
2453
|
+
// Anchors: requirements AC-05 (a); plan-strategy §2 D-3 (read-side interception);
|
|
2454
|
+
// plan-strategy §5.3 key test points "AC-03/AC-05 three triggers".
|
|
2455
|
+
|
|
2456
|
+
describe('render-system-extract count-mismatch (w12, AC-05 a)', () => {
|
|
2457
|
+
it('single mesh single material: extract succeeds', () => {
|
|
2458
|
+
const world = new World();
|
|
2459
|
+
const assets = createTestRenderAssetOwner(
|
|
2460
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock ShaderRegistry
|
|
2461
|
+
{ findMaterialArtifact: () => ({ ok: false }) } as any,
|
|
2462
|
+
);
|
|
2463
|
+
|
|
2464
|
+
const matHandle = world.allocSharedRef('MaterialAsset', {
|
|
2465
|
+
kind: 'material',
|
|
2466
|
+
baseColor: [1, 0, 0, 1],
|
|
2467
|
+
passes: [{ name: 'forward', program: { module: 'project::standard' } }],
|
|
2468
|
+
} as never);
|
|
2469
|
+
|
|
2470
|
+
const meshHandle = world.allocSharedRef('MeshAsset', {
|
|
2471
|
+
kind: 'mesh',
|
|
2472
|
+
vertices: new Float32Array(4 * 12),
|
|
2473
|
+
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
2474
|
+
attributes: { position: new Float32Array(4 * 3) },
|
|
2475
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
2476
|
+
submeshes: [
|
|
2477
|
+
{
|
|
2478
|
+
indexOffset: 0,
|
|
2479
|
+
indexCount: 6,
|
|
2480
|
+
vertexCount: 4,
|
|
2481
|
+
materialSlot: 0,
|
|
2482
|
+
topology: 'triangle-list' as const,
|
|
2483
|
+
},
|
|
2484
|
+
],
|
|
2485
|
+
} as never);
|
|
2486
|
+
|
|
2487
|
+
world.spawn(
|
|
2488
|
+
{ component: Transform, data: {} },
|
|
2489
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
2490
|
+
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
2491
|
+
);
|
|
2492
|
+
|
|
2493
|
+
// Extract should not crash; count matches (1 submesh, 1 material).
|
|
2494
|
+
// The renderable may be empty (material resolves to 0 passes with mock
|
|
2495
|
+
// ShaderRegistry) but the key assertion: no crash and count-mismatch
|
|
2496
|
+
// error is NOT triggered.
|
|
2497
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
2498
|
+
expect(frame.renderables).toBeDefined();
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
it('materials: [] with submeshes: [1] routes through D-Q7 case B mid-grey default (preserves legacy single-mesh-no-material path)', () => {
|
|
2502
|
+
const world = new World();
|
|
2503
|
+
|
|
2504
|
+
const assets = createTestRenderAssetOwner(
|
|
2505
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock ShaderRegistry
|
|
2506
|
+
{ findMaterialArtifact: () => ({ ok: false }) } as any,
|
|
2507
|
+
);
|
|
2508
|
+
|
|
2509
|
+
const meshHandle = world.allocSharedRef('MeshAsset', {
|
|
2510
|
+
kind: 'mesh',
|
|
2511
|
+
vertices: new Float32Array(4 * 12),
|
|
2512
|
+
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
2513
|
+
attributes: { position: new Float32Array(4 * 3) },
|
|
2514
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
2515
|
+
submeshes: [
|
|
2516
|
+
{
|
|
2517
|
+
indexOffset: 0,
|
|
2518
|
+
indexCount: 6,
|
|
2519
|
+
vertexCount: 4,
|
|
2520
|
+
materialSlot: 0,
|
|
2521
|
+
topology: 'triangle-list' as const,
|
|
2522
|
+
},
|
|
2523
|
+
],
|
|
2524
|
+
} as never);
|
|
2525
|
+
|
|
2526
|
+
world.spawn(
|
|
2527
|
+
{ component: Transform, data: {} },
|
|
2528
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
2529
|
+
{ component: MeshRenderer, data: { materials: [] } },
|
|
2530
|
+
);
|
|
2531
|
+
|
|
2532
|
+
// materials.length=0 routes through D-Q7 case B (defaultMaterialSnapshot
|
|
2533
|
+
// mid-grey unlit) without triggering mesh-renderer-material-count-mismatch
|
|
2534
|
+
// -- count-mismatch only applies to non-empty materials whose length
|
|
2535
|
+
// disagrees with submeshes.length. Backward-compat preserved for legacy
|
|
2536
|
+
// `data: {}` spawn shape (charter P5 consistent abstraction).
|
|
2537
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
2538
|
+
expect(frame.renderables.length).toBe(1);
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
it('materials overflow emits a diagnostic and ignores the extra override', () => {
|
|
2542
|
+
const world = new World();
|
|
2543
|
+
|
|
2544
|
+
const assets = createTestRenderAssetOwner(
|
|
2545
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock ShaderRegistry
|
|
2546
|
+
{ findMaterialArtifact: () => ({ ok: false }) } as any,
|
|
2547
|
+
);
|
|
2548
|
+
|
|
2549
|
+
const matHandle = world.allocSharedRef('MaterialAsset', {
|
|
2550
|
+
kind: 'material',
|
|
2551
|
+
baseColor: [1, 0, 0, 1],
|
|
2552
|
+
passes: [{ name: 'forward', program: { module: 'project::standard' } }],
|
|
2553
|
+
} as never);
|
|
2554
|
+
|
|
2555
|
+
const meshHandle = world.allocSharedRef('MeshAsset', {
|
|
2556
|
+
kind: 'mesh',
|
|
2557
|
+
vertices: new Float32Array(4 * 12),
|
|
2558
|
+
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
2559
|
+
attributes: { position: new Float32Array(4 * 3) },
|
|
2560
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
2561
|
+
submeshes: [
|
|
2562
|
+
{
|
|
2563
|
+
indexOffset: 0,
|
|
2564
|
+
indexCount: 6,
|
|
2565
|
+
vertexCount: 4,
|
|
2566
|
+
materialSlot: 0,
|
|
2567
|
+
topology: 'triangle-list' as const,
|
|
2568
|
+
},
|
|
2569
|
+
],
|
|
2570
|
+
} as never);
|
|
2571
|
+
|
|
2572
|
+
world.spawn(
|
|
2573
|
+
{ component: Transform, data: {} },
|
|
2574
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
2575
|
+
{ component: MeshRenderer, data: { materials: [matHandle, matHandle] } },
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
2579
|
+
// Owner validation is diagnostic and non-fatal: the malformed extra
|
|
2580
|
+
// override is ignored while the valid first material still renders.
|
|
2581
|
+
expect(frame.renderables).toHaveLength(1);
|
|
2582
|
+
});
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
2586
|
+
// feat-20260709 M3 / w11: Camera clearColor + GlyphText color + Tilemap
|
|
2587
|
+
// tileSize vec collapse -- schema shape, explicit layer-2 defaults, and
|
|
2588
|
+
// spawn-omit equivalence (AC-01 / E1 / E9). D-5 (non-zero array defaults
|
|
2589
|
+
// declared explicitly at layer 2, since the array layer-3 fallback is
|
|
2590
|
+
// all-zero).
|
|
2591
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
2592
|
+
describe('M3 vec-collapse schema + defaults (w11 AC-01 / E1 / E9)', () => {
|
|
2593
|
+
it('Camera.clearColor is array<f32,4> with explicit layer-2 default [0,0,0,0]', () => {
|
|
2594
|
+
expect(componentSchema(Camera).clearColor).toBe('array<f32, 4>');
|
|
2595
|
+
expect(Array.from(Camera.fields.clearColor.default as Float32Array)).toEqual([0, 0, 0, 0]);
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
it('GlyphText.color is array<f32,4> with explicit layer-2 default [1,1,1,1]; per-axis scalars gone', () => {
|
|
2599
|
+
expect(componentSchema(GlyphText).color).toBe('array<f32, 4>');
|
|
2600
|
+
expect('colorR' in componentSchema(GlyphText)).toBe(false);
|
|
2601
|
+
expect('colorG' in componentSchema(GlyphText)).toBe(false);
|
|
2602
|
+
expect('colorB' in componentSchema(GlyphText)).toBe(false);
|
|
2603
|
+
expect('colorA' in componentSchema(GlyphText)).toBe(false);
|
|
2604
|
+
expect(Array.from(GlyphText.fields.color.default as Float32Array)).toEqual([1, 1, 1, 1]);
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
it('Tilemap.tileSize is array<f32,2> with explicit layer-2 default [1,1]; per-axis scalars gone', () => {
|
|
2608
|
+
expect(componentSchema(Tilemap).tileSize).toBe('array<f32, 2>');
|
|
2609
|
+
expect('tileSizeX' in componentSchema(Tilemap)).toBe(false);
|
|
2610
|
+
expect('tileSizeY' in componentSchema(Tilemap)).toBe(false);
|
|
2611
|
+
expect(Array.from(Tilemap.fields.tileSize.default as Float32Array)).toEqual([1, 1]);
|
|
2612
|
+
});
|
|
2613
|
+
|
|
2614
|
+
it('E1: Camera spawned with clearColor omitted resolves to [0,0,0,0]', () => {
|
|
2615
|
+
const world = new World();
|
|
2616
|
+
const e = world
|
|
2617
|
+
.spawn({ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } })
|
|
2618
|
+
.unwrap();
|
|
2619
|
+
const row = world.get(e, Camera).unwrap();
|
|
2620
|
+
expect(Array.from(row.clearColor)).toEqual([0, 0, 0, 0]);
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
it('E1: GlyphText spawned with color omitted resolves to [1,1,1,1] (equal to old scalar default)', () => {
|
|
2624
|
+
const world = new World();
|
|
2625
|
+
const e = world
|
|
2626
|
+
.spawn({ component: GlyphText, data: { fontHandle: 0 as never, text: 'x', fontSize: 16 } })
|
|
2627
|
+
.unwrap();
|
|
2628
|
+
const row = world.get(e, GlyphText).unwrap();
|
|
2629
|
+
expect(Array.from(row.color)).toEqual([1, 1, 1, 1]);
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
it('E1: Tilemap spawned with tileSize omitted resolves to [1,1] (equal to old scalar default)', () => {
|
|
2633
|
+
const world = new World();
|
|
2634
|
+
const e = world.spawn({ component: Tilemap, data: { cols: 4, rows: 4 } }).unwrap();
|
|
2635
|
+
const row = world.get(e, Tilemap).unwrap();
|
|
2636
|
+
expect(Array.from(row.tileSize)).toEqual([1, 1]);
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
it('E9: perspective() / orthographic() factories flow the new clearColor array default through cameraPodFromDefaults() (zero signature change)', () => {
|
|
2640
|
+
const p = perspective({ fov: Math.PI / 3, aspect: 4 / 3 });
|
|
2641
|
+
const o = orthographic({ left: -1, right: 1, bottom: -1, top: 1 });
|
|
2642
|
+
// The factory output carries the array-shaped clearColor field verbatim
|
|
2643
|
+
// from Camera.fields defaults -- no per-axis scalar keys survive.
|
|
2644
|
+
expect(Array.from(p.clearColor)).toEqual([0, 0, 0, 0]);
|
|
2645
|
+
expect(Array.from(o.clearColor)).toEqual([0, 0, 0, 0]);
|
|
2646
|
+
expect('clearR' in p).toBe(false);
|
|
2647
|
+
expect('clearR' in o).toBe(false);
|
|
2648
|
+
});
|
|
2649
|
+
|
|
2650
|
+
it('E9: factory spread + clearColor override lands the authored color', () => {
|
|
2651
|
+
const world = new World();
|
|
2652
|
+
const e = world
|
|
2653
|
+
.spawn({
|
|
2654
|
+
component: Camera,
|
|
2655
|
+
data: {
|
|
2656
|
+
...perspective({ fov: Math.PI / 3, aspect: 4 / 3 }),
|
|
2657
|
+
clearColor: [0.4, 0.6, 1, 1],
|
|
2658
|
+
},
|
|
2659
|
+
})
|
|
2660
|
+
.unwrap();
|
|
2661
|
+
const row = world.get(e, Camera).unwrap();
|
|
2662
|
+
expect(Array.from(row.clearColor)).toEqual([Math.fround(0.4), Math.fround(0.6), 1, 1]);
|
|
2663
|
+
});
|
|
2664
|
+
});
|
|
2665
|
+
}
|