@woosh/meep-engine 2.168.0 → 2.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Correctness review — `engine/intelligence`, `engine/navigation`, `engine/grid`
|
|
2
|
+
|
|
3
|
+
Reviewed every non-spec source file under the three trees. Findings below are correctness only;
|
|
4
|
+
each has a concrete failure scenario. Several were confirmed by running the real code
|
|
5
|
+
(probe scripts under `scratchpad/probe/`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
### AI-1 `ParallelBehavior` double-finalizes its children on every early-exit path
|
|
10
|
+
- **Severity**: CRITICAL
|
|
11
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/composite/ParallelBehavior.js:215-226` (and the call sites at `:143`, `:158`, `:164`, `:232`)
|
|
12
|
+
- **Defect**: `__finalizeActiveChildren()` walks the set bits of `activeSet` and calls `finalize()` on each child, but never clears those bits. On the `RequireOne` early-exit paths `tick()` calls it and returns a terminal status; the parent then calls `ParallelBehavior.finalize()`, which calls `__finalizeActiveChildren()` again over the *same still-set bits*. Every child that was still running is finalized twice.
|
|
13
|
+
- **Failure scenario**: default policies (`ParallelBehavior.from([...])` → success `RequireAll`, failure `RequireOne`).
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const wr = WeightedRandomBehavior.from([WeightedElement.from(DelayBehavior.from(5), 1)]);
|
|
17
|
+
const par = ParallelBehavior.from([wr, FailingBehavior.INSTANCE]);
|
|
18
|
+
const seq = SequenceBehavior.from([par]);
|
|
19
|
+
seq.initialize({});
|
|
20
|
+
seq.tick(0.1); // -> Failed (FailingBehavior trips the RequireOne failure policy)
|
|
21
|
+
seq.finalize(); // TypeError: Cannot read properties of undefined (reading 'finalize')
|
|
22
|
+
```
|
|
23
|
+
`WeightedRandomBehavior.finalize()` nulls `__selected` on the first call and dereferences it on the second. Verified by running the above — it throws. In `BehaviorSystem.unlink()` the `finalize()` call is *not* wrapped in a try/catch, so the throw escapes entity teardown.
|
|
24
|
+
Even where the second finalize does not throw, it is still a double `finalize()` on every subtree — e.g. `WaitForEventBehavior` unregisters its listener twice, `AbstractDecoratorBehavior` propagates the double-finalize down its source chain.
|
|
25
|
+
- **Fix**: clear the bit as each child is finalized inside `__finalizeActiveChildren`:
|
|
26
|
+
```js
|
|
27
|
+
for (let i = activeSet.nextSetBit(0); i !== -1; i = activeSet.nextSetBit(i + 1)) {
|
|
28
|
+
activeSet.set(i, false);
|
|
29
|
+
children[i].finalize();
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
(`nextSetBit(i + 1)` still advances correctly after clearing bit `i`.)
|
|
33
|
+
- **Confidence**: Confirmed
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
### AI-2 `ConditionalBehavior` re-initializes its condition on every tick — a long-running condition can never complete
|
|
38
|
+
- **Severity**: MAJOR
|
|
39
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/util/ConditionalBehavior.js:28`
|
|
40
|
+
- **Defect**: `tick()` unconditionally calls `condition.initialize(this.context)` before ticking it. Any condition whose evaluation spans more than one tick has its state reset every frame, so it re-starts from scratch forever. The `else { //waiting? return s; }` branch shows a `Running` condition is meant to be supported. Additionally the per-tick `initialize` is unpaired — the `Running` branch never calls `condition.finalize()`.
|
|
41
|
+
- **Failure scenario**:
|
|
42
|
+
```js
|
|
43
|
+
const c = ConditionalBehavior.from(DelayBehavior.from(1), someAction);
|
|
44
|
+
c.initialize({});
|
|
45
|
+
c.tick(0.25) // Running (elapsed reset to 0, then 0.25)
|
|
46
|
+
c.tick(0.25) // Running (elapsed reset to 0 again)
|
|
47
|
+
... // never succeeds
|
|
48
|
+
```
|
|
49
|
+
Verified: 100 ticks of 0.25 s against a 1 s `DelayBehavior` condition still returns `Running` (4 ticks should suffice). With `WaitForEventBehavior` as the condition it is worse — `initialize` calls `ecd.addEntityEventListener(...)` once per frame, so the listener list on the target entity grows without bound.
|
|
50
|
+
- **Fix**: initialize the condition once, in `ConditionalBehavior.initialize(context)`, and re-initialize it only after it has resolved and been finalized (i.e. track a `#condition_running` state, or move the init into the `Succeeded`/`Failed` branches that already call `condition.finalize()`).
|
|
51
|
+
- **Confidence**: Confirmed
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### AI-3 `StateNode.traverse` leaves the shared stack pointer below its entry value
|
|
56
|
+
- **Severity**: MAJOR
|
|
57
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/mcts/StateNode.js:335-363`
|
|
58
|
+
- **Defect**: the loop condition `while (stack_pointer-- > stackOffset)` decrements once more on the failing test, so on exit the module-static `stack_pointer` is `stackOffset - 1`, not `stackOffset`. Two consequences: (a) every completed traversal shifts the shared pointer one slot further negative, permanently retaining a `StateNode` reference at that (string-keyed) index — an unbounded leak that pins whole MCTS subtrees via `parent`/`moves`; (b) a nested traversal corrupts the enclosing one.
|
|
59
|
+
- **Failure scenario**: root with three materialised children `a`, `b`, `c`; the visitor calls `a.traverse(...)` when it reaches the root (a natural pattern — e.g. summarising a subtree while walking the tree).
|
|
60
|
+
```
|
|
61
|
+
observed outer visit order: root, c, b // 'a' is silently skipped
|
|
62
|
+
expected: root, a, b, c
|
|
63
|
+
```
|
|
64
|
+
Verified by running it. The inner traversal returns with the pointer one below where the outer loop left it, so the outer loop's next push overwrites a pending entry.
|
|
65
|
+
- **Fix**: restore the pointer explicitly. Replace the loop with
|
|
66
|
+
```js
|
|
67
|
+
while (stack_pointer > stackOffset) {
|
|
68
|
+
n = stack[--stack_pointer];
|
|
69
|
+
...
|
|
70
|
+
}
|
|
71
|
+
stack_pointer = stackOffset;
|
|
72
|
+
```
|
|
73
|
+
and clear (`stack[stack_pointer] = null`) as entries are consumed so references are not retained.
|
|
74
|
+
- **Confidence**: Confirmed
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### AI-4 `RepeatBehaviorSerializationAdapter` cannot encode the default `limit = Infinity`; it round-trips to `0`
|
|
79
|
+
- **Severity**: MAJOR
|
|
80
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/decorator/RepeatBehaviorSerializationAdapter.js:20`
|
|
81
|
+
- **Defect**: `buffer.writeUintVar(value.limit)` is called with `Infinity` for every default-constructed `RepeatBehavior` (`RepeatBehavior.from(source)` → `limit = Infinity`, and `BehaviorComponent.loop(action)` builds exactly that). `writeUintVar` narrows via `ToInt32`: `Infinity & 0x7f === 0` and `Infinity >> 7 === 0`, so it emits a single `0x00` byte and `readUintVar` returns `0`.
|
|
82
|
+
- **Failure scenario**:
|
|
83
|
+
```js
|
|
84
|
+
new Entity().add(BehaviorComponent.loop(dt => tickSomething(dt))).build(ecd);
|
|
85
|
+
// save game, reload
|
|
86
|
+
```
|
|
87
|
+
After the round-trip `limit === 0`. `RepeatBehavior.tick` then does `__iterator++` → `1 >= 0` → returns `Succeeded`, so the "run forever" loop runs its body exactly once and the `BehaviorComponent` is flagged `Resolved` — the entity's per-frame logic silently stops after one frame. (In a development build the stripped-in-production `assert.isNonNegativeInteger` in `writeUintVar` turns this into an outright serialization throw instead; both builds are broken, differently.)
|
|
88
|
+
- **Fix**: encode the unbounded case explicitly, e.g. write a one-byte flag (`0 = unbounded`, `1 = bounded` followed by the `uintVar`), and reconstruct `Infinity` on read. `RepeatUntilSuccessBehavior` has the same `Infinity` default and would need the same treatment if it gains an adapter.
|
|
89
|
+
- **Confidence**: Confirmed
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### AI-5 `PathFollower` save upgrade drops the position/rotation write masks — pre-v3 saves load a follower that never moves
|
|
94
|
+
- **Severity**: MAJOR
|
|
95
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/navigation/ecs/path_following/PathFollowerSerializationUpgrader_2_3.js:13-14` (root cause also visible at `PathFollowerSerializationUpgrader_0_1.js:22-23`)
|
|
96
|
+
- **Defect**: in formats 0–2 the write masks lived in two separate bytes (`rotationAlignment`, `positionWriting`) alongside a `uint8 flags` that could only hold bits 1–8. The 2→3 upgrader widens `flags` to `uint16` and then *discards* both mask bytes ("now in flags") without folding them in. Nothing else supplies those bits, and `PathFollowerSerializationAdapter.deserialize` assigns `value.flags` directly, so the constructor's `DEFAULT_FLAGS` never applies.
|
|
97
|
+
- **Failure scenario**: load a save written at component version ≤ 2. The upgraded `flags` contains only `Active` (bit 1) — every `WritePositionX/Y/Z` (16/32/64) and `WriteRotationX/Y/Z` (128/256/512) bit is clear. In `performStep` all three `if (!getFlag(WritePosition*)) nextPosition.set*(position.*)` branches fire, so `nextPosition.equals(position)` is true and the whole write block (including `position.copy(nextPosition)`) is skipped. The follower's `position` scalar advances and `EndReached` eventually fires, but the entity's `Transform` is never touched — the moving object stands still and then "arrives".
|
|
98
|
+
- **Fix**: in `PathFollowerSerializationUpgrader_2_3`, translate the two legacy bytes into the new flag bits before writing:
|
|
99
|
+
```js
|
|
100
|
+
const legacy_flags = source.readUint8();
|
|
101
|
+
const rot = source.readUint8(); // was read after the floats; keep the field order
|
|
102
|
+
const pos = source.readUint8();
|
|
103
|
+
let flags = legacy_flags;
|
|
104
|
+
if (pos & 1) flags |= PathFollowerFlags.WritePositionX;
|
|
105
|
+
if (pos & 2) flags |= PathFollowerFlags.WritePositionY;
|
|
106
|
+
if (pos & 4) flags |= PathFollowerFlags.WritePositionZ;
|
|
107
|
+
if (rot & 1) flags |= PathFollowerFlags.WriteRotationX;
|
|
108
|
+
... etc
|
|
109
|
+
```
|
|
110
|
+
(the byte order in the v2 stream is flags, speed, rotationSpeed, rotationAlignment, positionWriting, maxMoveDistance — read them in that order and emit the combined `uint16` first).
|
|
111
|
+
- **Confidence**: Confirmed (by reading; the flag-value/`performStep` interaction is unambiguous)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### AI-6 `RepeatBehavior` / `RepeatUntilSuccessBehavior` re-initialize the source without finalizing it
|
|
116
|
+
- **Severity**: MAJOR
|
|
117
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/decorator/RepeatBehavior.js:82`; same at `RepeatUntilSuccessBehavior.js:72`
|
|
118
|
+
- **Defect**: on each loop iteration the decorator calls `source.initialize(this.context)` without first calling `source.finalize()`. Over `N` iterations the source receives `N` initializes and exactly one finalize (from `AbstractDecoratorBehavior.finalize`). Every behaviour that acquires a resource in `initialize` and releases it in `finalize` leaks one acquisition per iteration.
|
|
119
|
+
- **Failure scenario**: measured with an instrumented source —
|
|
120
|
+
```js
|
|
121
|
+
RepeatBehavior.from(probe, 3); // -> probe.initialize called 3 times, probe.finalize called 1 time
|
|
122
|
+
```
|
|
123
|
+
Concretely with a `SelectorBehavior` source: the selector's *successful* child is deliberately left un-finalized by `SelectorBehavior.tick` (the parent is expected to finalize it). `RepeatBehavior` instead calls `selector.initialize(...)`, which resets `__currentBehaviour` to child 0 — so the previously-successful child is never finalized at all. If that child is a `WaitForEventBehavior` the `ecd` listener registered by its `initialize` is never removed, and one stale listener accumulates per loop iteration.
|
|
124
|
+
- **Fix**: `source.finalize(); source.initialize(this.context);` in both decorators.
|
|
125
|
+
- **Confidence**: Confirmed
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### AI-7 Blackboard proxy is not enumerable — `Object.keys`/`for..in`/spread/`JSON.stringify` all see an empty object
|
|
130
|
+
- **Severity**: MAJOR
|
|
131
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/blackboard/make_blackboard_proxy.js:41-45`
|
|
132
|
+
- **Defect**: the handler defines `ownKeys` but no `getOwnPropertyDescriptor`. Every enumeration operation follows `ownKeys` with a `[[GetOwnProperty]]` on each key; with no trap that forwards to the target `Blackboard` instance, which has no such own property, so the key is filtered out as non-existent. The proxy's stated purpose — "connecting blackboard to systems that are not explicitly designed to work with a blackboard" — fails for exactly those systems.
|
|
133
|
+
- **Failure scenario**:
|
|
134
|
+
```js
|
|
135
|
+
const bb = new Blackboard();
|
|
136
|
+
bb.acquireNumber('hp', 10);
|
|
137
|
+
bb.acquireString('name', 'bob');
|
|
138
|
+
const p = bb.getValueProxy();
|
|
139
|
+
p.hp // 10 (get trap works)
|
|
140
|
+
bb.getKeys() // ["hp","name"]
|
|
141
|
+
Object.keys(p) // [] <-- wrong
|
|
142
|
+
JSON.stringify(p) // "{}" <-- wrong
|
|
143
|
+
({...p}) // {} <-- wrong
|
|
144
|
+
for (const k in p){} // no iterations
|
|
145
|
+
```
|
|
146
|
+
Verified by running it. Any consumer that snapshots the blackboard by spreading/stringifying the proxy silently persists or transmits an empty object.
|
|
147
|
+
- **Fix**: add a `getOwnPropertyDescriptor` trap:
|
|
148
|
+
```js
|
|
149
|
+
getOwnPropertyDescriptor(target, p) {
|
|
150
|
+
if (!target.contains(p, DataType.Any)) return undefined;
|
|
151
|
+
return { value: target.acquire(p, DataType.Any).getValue(), enumerable: true, configurable: true, writable: true };
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
(`configurable: true` is required — the target is extensible and does not actually own the key.)
|
|
155
|
+
- **Confidence**: Confirmed
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### AI-8 `FilterBehavior.from` evaluates conditions in reverse order
|
|
160
|
+
- **Severity**: MAJOR
|
|
161
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/util/FilterBehavior.js:18` + `:34`
|
|
162
|
+
- **Defect**: `addCondition` uses `unshift`, so `from(conditions, actions)` inserts each condition at the head in turn and the list ends up reversed. `FilterBehavior` extends `SequenceBehavior`, which runs children in array order and short-circuits on the first failure, so the author-declared guard order is inverted.
|
|
163
|
+
- **Failure scenario**: the standard guard idiom breaks —
|
|
164
|
+
```js
|
|
165
|
+
FilterBehavior.from(
|
|
166
|
+
[ new ConditionBehavior(() => ctx.target !== null),
|
|
167
|
+
new ConditionBehavior(() => ctx.target.hp > 0) ], // depends on the first guard
|
|
168
|
+
[ attackAction ]
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
measured execution order is `c2 -> c1 -> a1`, so the `.hp` guard runs first and throws `TypeError: Cannot read properties of null` when `ctx.target` is null — which `ActionBehavior`/`BehaviorSystem` swallow into a `Failed`, silently disabling the branch. Verified by running an instrumented version.
|
|
172
|
+
- **Fix**: keep conditions in declaration order — insert at the boundary between conditions and actions, e.g. track `#condition_count` and `this.__children.splice(this.#condition_count++, 0, condition)`.
|
|
173
|
+
- **Confidence**: Confirmed
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### AI-9 `StateGraphSystem.link` calls `addEntityEventListener()` with no arguments
|
|
178
|
+
- **Severity**: MAJOR
|
|
179
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/controller/graph/StateGraphSystem.js:13`
|
|
180
|
+
- **Defect**: `this.entityManager.dataset.addEntityEventListener();` — no entity, event name, or listener. The asserts in `addEntityEventListener` are compiled out in production, so control reaches `if (!this.entityExists(undefined)) throw new Error(...)`.
|
|
181
|
+
- **Failure scenario**: register `StateGraphSystem` and add a `StateGraph` component to any entity → `link` throws `Error: Entity 'undefined' does not exist` out of the ECS link path, aborting entity construction. The system is unusable as shipped.
|
|
182
|
+
- **Fix**: the file is an unfinished stub (`unlink` is empty, `StateGraphDescription.build()` is empty and the class is not even exported). Either implement the listener registration from `StateGraph.inputs` (`for (const [state, events] of ...) dataset.addEntityEventListener(entity, event, handler, this)`) with the matching `removeEntityEventListener` in `unlink`, or remove the system rather than shipping a link that throws.
|
|
183
|
+
- **Confidence**: Confirmed
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### AI-10 `WaitForEventBehavior.target` is dropped by serialization and permanently overwritten by `initialize`
|
|
188
|
+
- **Severity**: MAJOR
|
|
189
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/behavior/ecs/WaitForEventBehaviorSerializationAdapter.js:15,24` and `WaitForEventBehavior.js:74-76`
|
|
190
|
+
- **Defect**: two related problems with the `target` field.
|
|
191
|
+
1. The adapter serializes only `event`. `target` is silently reset to its class-field default `-1` ("listen on self") on load.
|
|
192
|
+
2. `initialize` does `if (this.target < 0) { this.target = this.entity; }` — it *mutates* the configured field instead of resolving into a local. Once initialized the behaviour is permanently bound to the first entity it ran on.
|
|
193
|
+
- **Failure scenario**: (1) a behaviour authored as `WaitForEventBehavior.fromJSON({ event: 'gate_opened', target: 42 })` — a cutscene actor waiting on a gate entity — round-trips through a save and comes back listening for `'gate_opened'` on *itself*. The event never arrives; `WaitForEventBehavior` is documented as having no failure condition, so the containing sequence hangs on that node for the rest of the session. (2) if a prebuilt tree object is reused to build a second entity (e.g. a cached prefab tree), the second entity's `WaitForEventBehavior` still registers its listener on the *first* entity's id.
|
|
194
|
+
- **Fix**: serialize `target` (`writeInt32`/`readInt32`, or a one-byte "self" flag plus the id), and in `initialize` resolve into a private field — `this.#resolved_target = this.target < 0 ? this.entity : this.target;` — used by the listener add/remove, leaving `this.target` as pure configuration.
|
|
195
|
+
- **Confidence**: Confirmed
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### AI-11 `GridObstacle.traverseMask` passes `index + 1` to the visitor
|
|
200
|
+
- **Severity**: MINOR
|
|
201
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/grid/obstacle/GridObstacle.js:80-84`
|
|
202
|
+
- **Defect**: `index++` runs before the visitor call, so the fourth argument is always one greater than the cell's actual index into `this.data`.
|
|
203
|
+
- **Failure scenario**: cell `(0,0)` (`data[0]`) is reported to the visitor as `index = 1`. A visitor that uses the index to write back — `(x, y, value, index) => obstacle.data[index] = 0` to clear a footprint — clears cell `(1,0)` for every cell it visits and writes one element past the end of `data` for the last cell (a silent no-op on a `Uint8Array`, so the last cell is never cleared). No in-repo caller uses the 4th argument today, but it is public API and the documented contract (`@param index`) is violated.
|
|
204
|
+
- **Fix**: pass the pre-increment value: `visitor.call(thisArg, j + offsetX, i + offsetY, value, index++)` (or hoist `const cell_index = index++;` before the call).
|
|
205
|
+
- **Confidence**: Confirmed
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### AI-12 `GridPosition2Transform.flags` is not copied by `copy()`/`clone()` and not serialized
|
|
210
|
+
- **Severity**: MINOR
|
|
211
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/grid/grid2transform/GridPosition2Transform.js:80-84`, `:60-74`; `GridPosition2TransformSerializationAdapter.js:15-67`
|
|
212
|
+
- **Defect**: `copy`, `toJSON`, `fromJSON`, `equals`, `hash` and the binary adapter all handle only `offset`. `flags` reverts to `DEFAULT_FLAGS` (which includes `Immediate`) on clone or load.
|
|
213
|
+
- **Failure scenario**: `CharacterMoveAsynchronousAction` clears `GridPosition2TransformFlags.Immediate` while a character is animating between cells (`CharacterMoveAsynchronousAction.js:357`), precisely so the transform is *not* snapped to the grid cell. Save while a move is in flight (or clone the component) → the flag comes back set → `GridPosition2TransformSystem.link` runs `sync()` immediately and teleports the character to the destination cell centre, discarding the interpolated position.
|
|
214
|
+
- **Fix**: serialize `flags` (one byte, packed into the existing header's spare bits or written separately with a version bump) and add `this.flags = other.flags;` to `copy()`; include it in `equals`/`hash`.
|
|
215
|
+
- **Confidence**: Confirmed
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### AI-13 `ResourceAllocationSolver.addResource` mutates the caller's `Resource` objects
|
|
220
|
+
- **Severity**: MINOR
|
|
221
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/intelligence/resource/ResourceAllocationSolver.js:64-74`
|
|
222
|
+
- **Defect**: the first `Resource` seen for a given type is stored by reference; a later resource of the same type is folded in with `existingResource.amount += resource.amount`, writing into the caller's object. `solve()` carefully clones before spending, showing the intent not to mutate inputs.
|
|
223
|
+
- **Failure scenario**:
|
|
224
|
+
```js
|
|
225
|
+
const pool = [new Resource(5, 'gold'), new Resource(3, 'gold')];
|
|
226
|
+
await allocator.allocate(pool); // solver sees 8 gold; pool[0].amount is now 8
|
|
227
|
+
await allocator.allocate(pool); // solver now sees 11 gold, then 14, ...
|
|
228
|
+
```
|
|
229
|
+
Each `allocate` call inflates the caller's pool, so tactical modules are granted resources that do not exist.
|
|
230
|
+
- **Fix**: store a clone — `this.resources.set(type, resource.clone())` — and keep the `+=` on that owned copy.
|
|
231
|
+
- **Confidence**: Confirmed
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### AI-14 `Transform2GridPositionSystem` never performs an initial synchronisation on link
|
|
236
|
+
- **Severity**: MINOR
|
|
237
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/grid/transform2grid/Transform2GridPositionSystem.js:107-119` (`Synchronizer.link`, `:74-76`)
|
|
238
|
+
- **Defect**: `link()` only subscribes to `transform.position.onChanged`. The `GridPosition` is left at whatever it was until the transform is *next* written. Compare `GridPosition2TransformSystem`, which has an explicit `Immediate` flag for this; `Transform2GridPositionFlags` is an empty object, suggesting the counterpart was never implemented.
|
|
239
|
+
- **Failure scenario**:
|
|
240
|
+
```js
|
|
241
|
+
new Entity()
|
|
242
|
+
.add(new Transform().position.set(10, 0, 10) && transform)
|
|
243
|
+
.add(new GridPosition()) // (0,0)
|
|
244
|
+
.add(new Transform2GridPosition())
|
|
245
|
+
.build(ecd);
|
|
246
|
+
```
|
|
247
|
+
The entity sits at world (10, 10) but its `GridPosition` stays `(0,0)`. `GridPositionSystem` indexes it into the quadtree at cell (0,0), so `getEntitiesAt(result, 10, 10)` misses it and `getEntitiesAt(result, 0, 0)` returns a phantom. The spec (`Transform2GridPositionSystem.spec.js:39`) only ever exercises the post-link write path, so this is untested.
|
|
248
|
+
- **Fix**: call `synchronizer.write()` at the end of `Synchronizer.link()` (mirroring `Vector2.process`, which `GridPositionSystem` already uses for exactly this reason).
|
|
249
|
+
- **Confidence**: Confirmed
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### AI-15 `PathFollowerFlags.Finished` and `PathFollower.position` are never reset by the library
|
|
254
|
+
- **Severity**: MINOR
|
|
255
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/navigation/ecs/path_following/PathFollowingSystem.js:56-57`, `:162-168`
|
|
256
|
+
- **Defect**: `performStep` sets `Finished` and pins `position = path_length`; nothing in the library ever clears either. `__visit_entity` then tests `Finished` after *every* `performStep`, not just when this frame set it.
|
|
257
|
+
- **Failure scenario**: reuse a follower — assign a new `Path` component and re-set `PathFollowerFlags.Active`. On the very next update `performStep` starts from the stale `position` (`= old path length`), so `next_offset >= path_length` immediately, and `__visit_entity` sees the still-set `Finished`, clears `Active` and fires a spurious `PathFollowerEventType.EndReached` before the entity has moved a single unit. The only in-repo consumer works around this by clearing the flag by hand (`CharacterMoveAsynchronousAction.js:600`); library users have no documented reset protocol.
|
|
258
|
+
- **Fix**: clear `Finished` (and reset `position` to 0) when the follower transitions to active, e.g. in `__visit_entity` gate on a per-frame local (`const finished_now = performStep(...)`) rather than the persistent flag, and expose a `PathFollower.restart()` that clears `Finished` and zeroes `position`.
|
|
259
|
+
- **Confidence**: Confirmed
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Status of the two "already known, unconfirmed" items
|
|
264
|
+
|
|
265
|
+
- **grid A\* inadmissible squared-distance heuristic** — **FIXED**. `find_path_on_grid_astar.js:170-186` now returns Manhattan distance, with a comment explaining the admissibility argument (min step cost 1 on a 4-connected grid); `find_path_on_grid_astar.spec.js:60-91` pins the previously non-optimal weighted-grid route to the Dijkstra cost of 8.
|
|
266
|
+
- **`OverrideContextBehavior` initialising its source twice with the wrong context first** — **FIXED**. `OverrideContextBehavior.js:29-38` now calls `this.__source.initialize(child_context)` and then `Behavior.prototype.initialize.call(this, context)` directly, deliberately bypassing `AbstractDecoratorBehavior.initialize`, with a comment stating why.
|
|
267
|
+
|
|
268
|
+
## Checked and found clean (no finding)
|
|
269
|
+
|
|
270
|
+
- `find_path_on_grid_astar` open/closed handling, lazy-push duplicate skipping, blocked-cell closing, `compute_path` gradient descent and bend-only waypoint reconstruction, start-equals-goal, unreachable-goal termination.
|
|
271
|
+
- `Path` sampling (`find_index_and_normalized_distance` avoids the 0/0 on duplicate points; the catmull-rom index clamping and the `computeLength`/`getLowIndexOfNearestPoint` buffer swaps are all correct).
|
|
272
|
+
- `SequenceBehavior` / `SelectorBehavior` initialize/finalize pairing on every reachable exit path; empty-composite handling.
|
|
273
|
+
- `clip_soup_against_overhangs`: half-plane decomposition, scratch-buffer capacity bounds, BVH height-band vs. exact band test agreement, barycentric reconstruction and winding preservation.
|
|
274
|
+
- `optimize_cyclic_sequence_pairwise`: the move/undo pair is self-inverse and the removal+insertion gain exactly equals the true (directed) cyclic edge-set delta, including the wrap cases.
|
|
275
|
+
- MCTS `computeNodeSelectionScore` UCB1 term (no `log(0)`, no divide-by-zero — every materialised node is on the backpropagation path), `backpropagate` sign and walk, `expand`'s no-moves terminal caching.
|
|
276
|
+
- `Blackboard.copy`/`clone`/`reset` coalescence and garbage sweep; `BlackboardStack` lookup order; `AbstractBlackboard.incrementNumber` (`Vector1._add` does dispatch `onChanged`, so the storage adapter is notified).
|
|
277
|
+
- `GridObstacle.resize` (old-width stride is used for the source rows) and `isPointAdjacent` bounds handling.
|
|
278
|
+
- `Transform2GridPositionSystem.write` — the `default:` clause sits above `case Continuous:` in source order but `switch` matches cases before `default`, so `Continuous` is handled correctly.
|
|
279
|
+
- All `Path` and `GridPosition` serialization upgrader chains (field order and widths line up end to end); only the `PathFollower` chain is broken (AI-5).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Correctness review — `engine/animation/` + `engine/interpolation/`
|
|
2
|
+
|
|
3
|
+
Scope: every non-spec source file under
|
|
4
|
+
`meep/src/engine/animation/` and `meep/src/engine/interpolation/`
|
|
5
|
+
(plus the prioritised `core/math/spline/spline3_hermite_reverse.js`,
|
|
6
|
+
`spline3_hermite_apply_transform.js`, `spline3_hermite_bounds.js`).
|
|
7
|
+
|
|
8
|
+
Specs were read as evidence only.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Findings
|
|
13
|
+
|
|
14
|
+
### ANIM-1 `animation_curve_nearest_point` minimises in normalized-time space but selects in real-time space
|
|
15
|
+
- **Severity**: CRITICAL
|
|
16
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/curve/animation_curve_nearest_point.js:44-60`
|
|
17
|
+
- **Defect**: The per-segment search is handed `ref_t = inverseLerp(key0.time, key1.time, ref_time)` — the reference point re-expressed with its x-axis squeezed into `[0,1]` — while the value axis is left in real units. `spline3_hermite_nearest_point` therefore minimises `(t − ref_t)² + (p(t) − ref_value)²`, a distance in a distorted metric, but the winner is then scored at line 60 with `v2_distance_sqr(curve_time, curve_value, …)` in real `(time, value)` units. The two metrics only coincide when the segment duration is exactly 1, which is the only case the spec file exercises.
|
|
18
|
+
- **Failure scenario**: `const c = AnimationCurve.linear(0, 0, 10, 10);` (keys `(0,0,in 0,out 1)` and `(10,10,in 1,out 0)`, a straight line `y = x`). Call `animation_curve_nearest_point(c, 10, 0)`. In normalized space the segment is `p(t) = 10t`, so minimising `(t−1)² + (10t)²` gives `t = 1/101 ≈ 0.0099`; that maps back to `curve_time ≈ 0.099`, whose real distance² to `(10,0)` is `98.05` — better than the `100` of the seeded candidate `(0,0)`, so it wins. **Returned `0.099`.** The true nearest point on `y = x` to `(10,0)` is `(5,5)` at distance² `50`, i.e. **time `5`**. Downstream, `detectClosestCurveTimePoint` (`curve/draw/isInjectedKeyframeInBounds.js:56`) turns this into a user-visible failure: on any curve whose keys are not ~1 apart, clicking directly on the plotted line reports a curve point tens of pixels away, `mousePosition.distanceTo(...) <= 10` fails, and a keyframe cannot be injected by clicking the curve.
|
|
19
|
+
- **Fix**: Search in a space where both axes carry the same unit. Divide the value axis by `time_delta` as well, then de-normalize:
|
|
20
|
+
```js
|
|
21
|
+
const inv = time_delta === 0 ? 0 : 1 / time_delta;
|
|
22
|
+
const t = spline3_hermite_nearest_point(
|
|
23
|
+
ref_t, ref_value * inv,
|
|
24
|
+
key0.value * inv, key1.value * inv,
|
|
25
|
+
key0.outTangent, key1.inTangent // == m0*inv, m1*inv
|
|
26
|
+
);
|
|
27
|
+
```
|
|
28
|
+
and keep line 58 evaluating the *unscaled* `spline3_hermite(t, key0.value, key1.value, m0, m1)`. (Scaling the whole plane by `1/time_delta` scales every distance by the same factor, so the argmin is preserved and now matches the real-space metric used at line 60.)
|
|
29
|
+
- **Confidence**: Confirmed
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### ANIM-2 `AnimationTrackPlayback` constructor seeks to absolute 0, not `track.timeStart`
|
|
34
|
+
- **Severity**: MAJOR
|
|
35
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/keyed2/AnimationTrackPlayback.js:36`
|
|
36
|
+
- **Defect**: Line 16 correctly sets `this.position = track.timeStart`, then line 36 overwrites it with `this.setPosition(0)`. For a track whose first key is not at t=0, `setPosition(0)` runs `keyLowerBoundIndexAt(0)`, which returns `-1` ("no key at or before this time"). `__lastKeyIndex` is then `-1`, and `readKeyValues(-1, result)` computes `offset = -propertyCount` and reads off the front of `keyValues`, producing `undefined` → `NaN` in the `Float64Array` value buffer. `reset()` (line 70) and `AnimationBehavior.initialize` (`keyed2/behavior/AnimationBehavior.js:37`) both use `track.timeStart`, so the constructor is the odd one out.
|
|
37
|
+
- **Failure scenario**:
|
|
38
|
+
```js
|
|
39
|
+
const t = new AnimationTrack(['alpha']);
|
|
40
|
+
t.addKey(1, [10]); // timeStart = 1
|
|
41
|
+
t.addKey(2, [20]); // timeEnd = 2
|
|
42
|
+
t.addTransition(0, TransitionFunctions.Linear);
|
|
43
|
+
|
|
44
|
+
const p = new AnimationTrackPlayback(t, cb, null);
|
|
45
|
+
// __lastKeyIndex === -1, __lastTransitionIndex === -1, __nextKeyTime === 1
|
|
46
|
+
p.update(); // cb(NaN) <-- expected cb(10)
|
|
47
|
+
p.advance(0.016); // cb(NaN)
|
|
48
|
+
```
|
|
49
|
+
This is reached on the shipping paths that call `update()` before the first tick: `playTrackRealTime.js:26` ("tick animation to make sure the state is synchronized") and `GUIEngine.js:535` ("force view status to initial key of animation"). With such a track the latter writes `el.style.opacity = NaN` and `view.scale.set(NaN, NaN)`. Playbacks driven through `AnimationBehavior` are shielded because `initialize()` re-seeks to `timeStart`; `playAnimationTrack`/direct `advance()` are not.
|
|
50
|
+
- **Fix**: Change line 36 to `this.setPosition(track.timeStart);` (line 16 then becomes redundant). Note the existing spec `"playback starts at position 0 by default"` only covers a track whose `timeStart` is already 0, so it stays green.
|
|
51
|
+
- **Confidence**: Confirmed
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### ANIM-3 `advanceKey()` leaves a stale `__lastTransitionIndex` when the transition scan runs off the end
|
|
56
|
+
- **Severity**: MAJOR
|
|
57
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/keyed2/AnimationTrackPlayback.js:135-146`
|
|
58
|
+
- **Defect**: The scan
|
|
59
|
+
```js
|
|
60
|
+
for (let i = this.__lastTransitionIndex + 1; i < transitionCount; i++) { … }
|
|
61
|
+
```
|
|
62
|
+
only writes `__lastTransitionIndex = -1` in the `startKeyIndex > __lastKeyIndex` branch. If the loop exhausts without entering either branch — which happens whenever the new key is at or past the last entry in `transitionKeys` — the previous transition index survives. `readCurrentValues` then takes the transition branch for a key that has no transition, interpolating where `setPosition` (via `transitionIndexAt`, which returns `-1` for the same time) would hold. The loop-wrap case at lines 112-117 has the same root: `__lastKeyIndex` rewinds to `0` but the scan cursor is never rewound to `0`, so a transition on key 0 is missed after the first loop.
|
|
63
|
+
- **Failure scenario A (no loop)**:
|
|
64
|
+
```js
|
|
65
|
+
const t = new AnimationTrack(['scale']);
|
|
66
|
+
t.addKey(0, [1]); t.addKey(1, [0]); t.addKey(2, [5]);
|
|
67
|
+
t.addTransition(0, TransitionFunctions.Linear); // transitionKeys = [0]
|
|
68
|
+
const p = new AnimationTrackPlayback(t, cb, null); // __lastTransitionIndex = 0
|
|
69
|
+
p.advanceKey(); // __lastKeyIndex = 1; loop runs from i=1, transitionCount=1 → no iterations
|
|
70
|
+
p.update();
|
|
71
|
+
```
|
|
72
|
+
`__lastTransitionIndex` is still `0`, so the segment key1→key2 is Linear-interpolated from `0` toward `5`; seeking to the same time with `setPosition(1.0)` yields `__lastTransitionIndex === -1` and holds `0`. Two different poses for the same time on the same track.
|
|
73
|
+
- **Failure scenario B (loop wrap)**: same track plus `t.addTransition(1, TransitionFunctions.EaseOut)` and `p.setLoop(true)`. Advance to the last key (`__lastTransitionIndex === 1`), then `advanceKey()` wraps `__lastKeyIndex` to `0`; the scan starts at `i = 2 >= transitionCount`, so `__lastTransitionIndex` stays `1`. The key0→key1 segment is now driven by `transitionFunctions[1]` (EaseOut) instead of `transitionFunctions[0]` (Linear) — the second and every later loop iteration eases differently from the first.
|
|
74
|
+
- **Fix**: Clear the cursor before scanning and rewind it on wrap:
|
|
75
|
+
```js
|
|
76
|
+
if (wrapped) { this.__lastTransitionIndex = -1; } // in the loop branch, before += 1
|
|
77
|
+
const scan_start = this.__lastTransitionIndex + 1;
|
|
78
|
+
this.__lastTransitionIndex = -1; // default: no transition
|
|
79
|
+
for (let i = scan_start; i < transitionCount; i++) {
|
|
80
|
+
const startKeyIndex = transitionKeys[i];
|
|
81
|
+
if (startKeyIndex === this.__lastKeyIndex) { this.__lastTransitionIndex = i; break; }
|
|
82
|
+
if (startKeyIndex > this.__lastKeyIndex) { break; }
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
- **Confidence**: Confirmed
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### ANIM-4 `AnimationClipBinding.bind()` cannot bind any clip the engine actually produces
|
|
90
|
+
- **Severity**: MAJOR
|
|
91
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/clip/AnimationClipBinding.js:64-76`
|
|
92
|
+
- **Defect**: Two independent breaks on the same path.
|
|
93
|
+
1. `binding_registry[track.type]` keys off `AnimationTrack.type`, which defaults to `""` (`clip/AnimationTrack.js:31`). The only producer of `AnimationClip`/`AnimationTrack` in the tree is `convert_three_clip` → `AnimationTrack.from(curves, track_name)` (`clip/ecd_bind_animation_curve.js:106`), which sets `curves` and `name` and **never sets `type`**. Every track therefore looks up `""`.
|
|
94
|
+
2. Even with a populated registry, the contract is `new WriterClass()` followed by `writer.bind(root, track.path)`. The only writers that can express a vector/quaternion target — `BoundVector3Writer` and `BoundQuaternionWriter` — take their target in the *constructor* and do not override `bind()`, so they inherit `BoundValueWriter.bind` which unconditionally `throw new Error('Not Implemented')` (`curve/binding/BoundValueWriter.js:19`). The one writer that does implement `bind()`, `BoundPlainNumericPropertyWriter`, writes a single scalar (`this.parent[this.property_name] = data[0]`), so it cannot carry a 3- or 4-component track.
|
|
95
|
+
- **Failure scenario**: Import a glTF, get `const cb = convert_three_clip(node, threeClip)` (this is what `SGMeshAnimationControllerSystem.js:78` does), then retarget it to a second skeleton, which is exactly what `bind()` exists for:
|
|
96
|
+
```js
|
|
97
|
+
cb.bind(other_node, { position: BoundVector3Writer, quaternion: BoundQuaternionWriter });
|
|
98
|
+
```
|
|
99
|
+
→ throws `No binding class in registry for track type ''` on the first track. Supply `{ "": BoundVector3Writer }` to get past that and it throws `Not Implemented` from `writer.bind`. Note that `bind()` also `splice`s `this.tracks` empty *before* the throw (line 54), so the binding is left destroyed — a clip that rendered fine before the retarget attempt now writes nothing.
|
|
100
|
+
- **Fix**: (a) set `type` in `convert_three_clip` from the property name it already computes (`three_track.name.split('.')` last part, or `three_track.ValueTypeName`) before `AnimationTrack.from(...)`; (b) give `BoundVector3Writer`/`BoundQuaternionWriter` no-arg constructors plus a real `bind(root, path)` that resolves the target the way `bind_property_writer` does, so they satisfy the registry contract; and (c) build the new track list into a local array and only assign it over `this.tracks` once every track bound, so a failure leaves the previous binding intact.
|
|
101
|
+
- **Confidence**: Confirmed
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### ANIM-5 CUBICSPLINE quaternion tracks are not hemisphere-aligned
|
|
106
|
+
- **Severity**: MAJOR
|
|
107
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/clip/ecd_bind_animation_curve.js:66-67`
|
|
108
|
+
- **Defect**: The alignment guard is `interpolation === AnimationInterpolationKind.Linear && track.ValueTypeName === 'quaternion'`. A glTF `CUBICSPLINE` rotation track resolves to `AnimationInterpolationKind.CubicTangents` (line 26/32) and so skips `hemisphere_align_quaternion_values` entirely, yet it is still decomposed into four independent scalar Hermite curves (line 74-89) and reassembled by `BoundQuaternionWriter.write` → `fromArray` + `normalize`. Component-wise interpolation across a key pair with a negative dot product takes the long arc. (This is *not* the already-fixed LINEAR case; the in-code comment acknowledges the CubicTangents path as "not yet aligned".)
|
|
109
|
+
- **Failure scenario**: A Blender-exported glTF with `CUBICSPLINE` rotation on a bone, with adjacent keys `q0 = (0, 0, 0, 1)` (identity) and `q1 = (0, 0, −0.0872, −0.9962)` (a −10° Z rotation written in the negative hemisphere; `dot = −0.9962`). Per-component Hermite at the midpoint gives roughly `(0, 0, −0.0436, 0.0019)`; after `normalize()` that is ≈ `(0, 0, −0.999, 0.044)`, i.e. ~175° of Z rotation. The bone whips through nearly a full turn between two keys that are 10° apart, and passes through a near-degenerate quaternion where the normalize is numerically unstable.
|
|
110
|
+
- **Fix**: Align CubicTangents quaternion tracks too, but negate the *whole key triple* — a CUBICSPLINE key is `[inTangent, value, outTangent]` per component, so a sign flip on the value must flip that key's in- and out-tangents as well or the tangents will fight the values. That needs a CubicTangents-aware variant of `hemisphere_align_quaternion_values` operating on a stride of `3 * component_count` and deciding the flip from the value sub-block.
|
|
111
|
+
- **Confidence**: Confirmed
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### ANIM-6 `acquireInjectedKeyframePosition` uses `time === 0` / `time >= 0` as "unset" sentinels
|
|
116
|
+
- **Severity**: MAJOR
|
|
117
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/curve/draw/isInjectedKeyframeInBounds.js:76-86`
|
|
118
|
+
- **Defect**: `prevKeyframeRef` and `nextKeyframeRef` are `new Keyframe()`, i.e. `time = 0, value = 0`. The bracketing scan then uses those zeros as sentinels: `kf.time >= prevKeyframeRef.time` treats "0" as "nothing found yet", and `nextKeyframeRef.time === 0` is the "still unset" test. Both are wrong for a curve that has keys at or before t = 0 — `0` is a legitimate keyframe time, and a legitimate keyframe time can be negative.
|
|
119
|
+
- **Failure scenario**: A curve with keys at times `−2, −1, 0, 1` (the editor's frame pans freely into negative time, and `position_canvas_to_curve` maps whatever is on screen). The user clicks on the curve at `timeSplicePosition = −1.5`, which passes the `curveTimePos > curve.keys[0].time` gate. The scan then:
|
|
120
|
+
- `kf(−2)`: `−2 <= −1.5` ✓ but `−2 >= 0` ✗ → **prev is never captured**, stays the phantom `(0, 0)`;
|
|
121
|
+
- `kf(−1)`: `−1 >= −1.5` ✓ and `next.time === 0` ✓ → next = `kf(−1)`.
|
|
122
|
+
|
|
123
|
+
`segmentTimeDiff = −1 − 0 = −1`, `segmentTimeTravel = −1.5 − 0 = −1.5`, `segmentTimeSplitRatio = 1.5`. `animation_curve_subdivide` is then called with `t = 1.5` (its `assert.ok(t >= 0 && t <= 1)` is compiled out in prod) between the phantom `(0,0)` and `kf(−1)`, so the injected keyframe gets an extrapolated value computed from a keyframe that does not exist. The key lands on the curve at the right time with a visibly wrong value and wrong tangents.
|
|
124
|
+
A second, independent instance of the same sentinel: with keys at `−1, 0, 1` and a splice at `−0.5`, `kf(0)` is copied into `nextKeyframeRef` but leaves `nextKeyframeRef.time === 0`, so `kf(1)` overwrites it — the "next" key is two keys away.
|
|
125
|
+
- **Fix**: Drop the sentinels and bracket by index, e.g. `const i = curve.getKeyIndexLow(timeSplicePosition);` then `prev = curve.keys[i]`, `next = curve.keys[i + 1]` (with an early-out when `i + 1 >= curve.length`). `getKeyIndexLow` already does this correctly and handles duplicate times.
|
|
126
|
+
- **Confidence**: Confirmed
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### ANIM-7 `isNotAlreadyAligned` is never called, and the comparison it makes is inverted
|
|
131
|
+
- **Severity**: MINOR
|
|
132
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/animation/curve/draw/build_tangent_editor.js:243, 254`
|
|
133
|
+
- **Defect**: Two errors that partially mask each other.
|
|
134
|
+
1. Line 254: `if (enableTangentAlignment.getValue() === true && isNotAlreadyAligned)` tests the *function object*, not its result. A function is always truthy, so the guard is a no-op.
|
|
135
|
+
2. Line 243: the predicate itself is `!(keyframe.inTangent === -keyframe.outTangent)`. Alignment in this editor means `inTangent === outTangent` — that is what the drag-time alignment branch produces (`updateTangentValues`, lines 55-63, sets `inTangent = outTangent`) and what `AnimationCurve.alignTangents` produces for a smooth key. The `-` makes the predicate report a *corner* (`in = 1, out = −1`) as "already aligned" and a correctly aligned key (`in = out = 2`) as "not aligned".
|
|
136
|
+
- **Failure scenario**: Select a keyframe whose tangents are already equal, e.g. `Keyframe.from(0.5, 1, 2, 2)`, and tick the "align tangents" checkbox. `modifyTangentAlignmentPosition` runs, computes `avgTangent = 2`, writes the same values back, and still calls `actionProcessor.mark('auto move tangents')` + `actionProcessor.do(new TangentChangeAction(...))`. An undo entry that changes nothing is pushed onto the stack; Ctrl+Z afterwards consumes a step with no visible effect. Conversely, if the missing `()` is added without also fixing the `-`, a corner key (`in = 1, out = −1`) would silently refuse to align — the checkbox would appear to do nothing.
|
|
137
|
+
- **Fix**: `… && isNotAlreadyAligned()` **and** change the predicate body to `return keyframe.inTangent !== keyframe.outTangent;`. (Separately, `Math.abs(newTangent) > Infinity` on line 249 is never true; if a non-finite guard is wanted it should be `!Number.isFinite(newTangent)`.)
|
|
138
|
+
- **Confidence**: Confirmed
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Status of the "already known, unconfirmed" items
|
|
143
|
+
|
|
144
|
+
| Item | Status |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `AnimationGraph` JSON dropping state `name`/`tags` | **FIXED** — `writeAnimationGraphDefinitionToJSON.js:69-71` now emits `name` and `tags` for states (and `name`/`tags` for clips); `readAnimationGraphDefinitionFromJSON.js:80-88` destructures `{ motion, type, name, tags = [] }` and assigns both back. |
|
|
147
|
+
| `startingSate` typo | **STILL PRESENT** — the misspelling is load-bearing across the field (`AnimationGraphDefinition.js:29`), the JSON key (`writeAnimationGraphDefinitionToJSON.js:100` / `readAnimationGraphDefinitionFromJSON.js:25,130`), the binary adapter (`AnimationGraphDefinitionSerializationAdapter.js:154,295`) and both readers (`AnimationGraph.js:160,350`). Consistent everywhere, so no behavioural defect — renaming requires a JSON migration. |
|
|
148
|
+
| `AnimationClip.repeatCount` default | **FIXED** — `engine/ecs/animation/AnimationClip.js:9` initialises `new ObservedInteger(1)` and `fromJSON` (lines 83-87) falls back to `this.repeatCount.set(1)` when the field is absent. `toJSON`/`toBinaryBuffer`/`fromBinaryBuffer` all carry the field, so the three paths agree. |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Areas checked and found clean
|
|
153
|
+
|
|
154
|
+
- **`engine/interpolation/` (whole package)** — `InterpolationSystem` link/unlink is symmetric, `snap` is consumed exactly once per set, per-source playout windows are sampled once per frame and cleared each update, `PoseInterpolationAdapter` does hemisphere-corrected nlerp with renormalize and reduces correctly when `first_offset === second_offset`, `TransformPoseSerializationAdapter` round-trips the same 7 × Float64 layout it declares, `InterpolationLog.interpolate` never reads a stale scratch offset for a tick it failed to locate, and `locate`'s page walk matches the page layout `begin_tick`/`begin_record` write.
|
|
155
|
+
- **`spline3_hermite_reverse`** — verified against `R(u) = H(1−u)`: `R(0) = p1`, `R(1) = p0`, `R'(0) = −m1`, `R'(1) = −m0`; the written order `[p1, p0, −m1, −m0]` matches the `[p0, p1, m0, m1]` layout, and double-reversal is exactly the identity.
|
|
156
|
+
- **`spline3_hermite_apply_transform`** — column-major indexing is right (`m4[0,4,8,12]` for x′), endpoints take the translation column and tangents do not, and the 12 outputs are in the declared `p0/p1/m0/m1` order.
|
|
157
|
+
- **`spline3_hermite_bounds`** (scale-relative change) — the derivative quadratic `a = 3(m0+m1+2p0−2p1)`, `b = −2(2m0+m1+3p0−3p1)`, `c = m0` matches `H′(t)` exactly; the relative `epsilon = max3(|a|,|b|,|c|)·1e-12` correctly degrades to the linear branch without losing interior extrema on small-coordinate curves.
|
|
158
|
+
- **`spline3_hermite_nearest_point`** quintic coefficients — expanded by hand, all six match `(t − ref_t) + (p(t) − ref_p)·p′(t)`. The bug in ANIM-1 is entirely at the call site.
|
|
159
|
+
- **`curve_from_track_data_discrete`** — the reverse `splice` produces `[(t0,v0),(t1,v0),(t1,v1),(t2,v1),(t2,v2)]`; walked through `getKeyIndexLow`/`evaluate` at `t < t1`, `t = t1`, `t1 < t < t2`, `t = t2` and every case gives correct step behaviour with no zero-span division.
|
|
160
|
+
- **`curve_from_track_data_linear`** + `alignTangents` — for each segment `k0.outTangent == k1.inTangent == (v1−v0)/(t1−t0)`, so the Hermite degenerates to exactly the straight line; coincident keys are handled by the `time_span === 0 → 0` branch.
|
|
161
|
+
- **`Tween`** ping-pong turnaround — at `timeElapsed = N·duration`, `cycleWhole = N` and `cycleFraction = 0` give `state = 1` for odd `N` and `0` for even `N`, which is the correct reversal at both ends; the non-oscillating branch fires `ended` once (`destroy()` removes the driving entity in the same call).
|
|
162
|
+
- **`AnimationTrackPlayback.advance`** event semantics — `ended` fires exactly once per forward crossing for a delta larger than the whole clip, and does not re-fire while parked past `timeEnd`; the negative-delta branch correctly avoids the one-shot.
|
|
163
|
+
- **`downsample_float_array_curve_by_error`** — ping-pong buffer sizes (`n/2` and `n/4`) are never overrun as `current_size` halves, and the `target_samples === 1` special case stops the loop before `downsample_float_array` can be handed an odd size.
|
|
164
|
+
- **`animation_curve_optimize`** — the `i--; key_count--` removal step keeps the cursor correct, and the first/last keys are structurally protected.
|
|
165
|
+
- **`animation_curve_fit`** — `calculateTangent` boundary/interior cases are consistent with the tangent convention `evaluate_two_key_curve` uses (`slope × segment duration`), the recursion strictly shrinks, and `splitIndex === -1` is unreachable when `maxSegmentError > maxError`.
|
|
166
|
+
- **`table_find_min_index_in_ordered_column`** — returns the correct lower bound (`minIndex − 1`) on a miss and `−1` before the first sample, which `findLowSampleIndexByTime` clamps.
|
|
167
|
+
- **`AnimationCurveView.draw_key_tangents`** — `in_angle = −atan2(inTangent, −1)` is the angle of `(−1, −m)`, correct for a backward-pointing handle in curve space before the y-flipping transform; `build_tangent_editor.handleTangentUpdate`'s screen-space angles (`π − atan2(in,1)`, `−atan2(out,1)`) are the correct y-down equivalents.
|