@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,176 @@
|
|
|
1
|
+
# Correctness review — `meep/src/generation`
|
|
2
|
+
|
|
3
|
+
Scope: every non-spec source file under
|
|
4
|
+
`H:/git/moh/app/src/mir-engine/meep/src/generation/` (~12.2k LOC, 160 files).
|
|
5
|
+
`REVIEW_01_ACTION_PLAN.md` items and `COORDINATES.md` conventions were read first;
|
|
6
|
+
already-addressed items are not re-reported.
|
|
7
|
+
|
|
8
|
+
Method: full read of the package. Determinism claims about Mulberry32 were
|
|
9
|
+
verified by executing the generator (`core/math/random/seededRandom_Mulberry32.js`)
|
|
10
|
+
directly under node; the measurements are quoted in the findings.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
### GEN-1 Theme child seeds are `random()` floats; Mulberry32 collapses them to one stream
|
|
15
|
+
- **Severity**: CRITICAL
|
|
16
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/theme/ThemeEngine.js:212`, `H:/git/moh/app/src/mir-engine/meep/src/generation/theme/TerrainTheme.js:29`
|
|
17
|
+
- **Defect**: Both sites use the *output* of a `seededRandom` stream (a float in `[0,1)`) as the *seed* of a child stream. `seededRandom_Mulberry32` does `_seed += 0x6D2B79F5` and then feeds `_seed` through `Math.imul`/`^`/`>>>`, all of which apply ToInt32/ToUint32 and discard the fractional part. Every seed in `[0,1)` therefore produces a byte-identical sequence — identical to `seededRandom(0)`.
|
|
18
|
+
- **Failure scenario**: `themeEngine.random.setCurrentSeed(12345)` → `initializeThemes` derives theme seed `0.9797282677609473`; with world seed `999` it derives `0.9699058223050088`. Both are passed to `Theme.initialize(grid, ecd, seed)` → `TerrainTheme.initialize` → `seededRandom(seed)`. Verified output of the first three draws for seeds `0.1234`, `0.9876` and `0` is identical: `0.26642920868471265, 0.0003297457005828619, 0.2232720274478197`. Consequently, for the shipped `AlienTheme0` / `SnowyTheme`:
|
|
19
|
+
- every `CellFilterSimplexNoise` reachable from `Theme.terrain.rules` and `Theme.nodes` (`injectBases.js:101`, `injectEnemies.js:355`, `injectTreasure.js:556`, `injectBuffObjects.js:45`, `injectPlayerCharacter.js:88`) is built from the *same* permutation table, and that table never changes with the world seed;
|
|
20
|
+
- `MarkerNodeTransformRotateRandom` and `MarkerNodeEntityProcessorRandomRotation` (`theme-1/injectDecor.js`) emit the same rotation sequence for every world seed.
|
|
21
|
+
Two runs with different world seeds produce identical theme-layer noise and identical decor rotations.
|
|
22
|
+
- **Fix**: derive integer child seeds at both sites, e.g.
|
|
23
|
+
`const seed = randomIntegerBetween(this.random, 0, 0x7FFFFFFF);` in `ThemeEngine.initializeThemes` and
|
|
24
|
+
`const ruleSeed = randomIntegerBetween(random, 0, 0x7FFFFFFF);` in `TerrainTheme.initialize`.
|
|
25
|
+
(Same one-line shape; `(random() * 0x100000000) | 0` also works.)
|
|
26
|
+
- **Confidence**: Confirmed
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### GEN-2 `GridTaskGenerateRoads` never initializes its `traversable` matcher
|
|
31
|
+
- **Severity**: CRITICAL
|
|
32
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/grid/generation/road/GridTaskGenerateRoads.js:513-608`
|
|
33
|
+
- **Defect**: `build()` creates `tInitializeActions` for `this.actions` but there is no `this.traversable.initialize(grid, seed)` anywhere. `traversable` is a required `from({...})` parameter (phase 5.1) and is used by `tBuildDistanceField` (line 567, wrapped in a fresh `CellMatcherNot` that is also never initialized), by the Dijkstra expansion (lines 213, 276) and by `drawPath` (line 425). Every sibling task in the package that accepts a `CellMatcher` initializes it (`GridTaskConnectRooms.build:469`, `GridTaskBuildSourceDistanceMap.build:117`, `CellSupplierBestN.initialize:97`, `GridCellPlacementRule.initialize:91`).
|
|
34
|
+
- **Failure scenario**:
|
|
35
|
+
```js
|
|
36
|
+
const roads = GridTaskGenerateRoads.from({
|
|
37
|
+
traversable: CellMatcherLayerBitMaskTest.from(TRAVERSABLE_BIT, 'tags'),
|
|
38
|
+
actions: [GridCellActionPlaceTags.from(ROAD_BIT, 'tags')]
|
|
39
|
+
});
|
|
40
|
+
executeTaskTreeSync(roads.build(grid, ecd, 0));
|
|
41
|
+
```
|
|
42
|
+
`tBuildDistanceField` → `buildUnsignedDistanceField` → `traversable.match(grid, 0, 0, 0)` → `GridLayerCellMatcher.__layer` is still `null` → `TypeError: Cannot read properties of null (reading 'sampler')`. `SampleGenerator0` only survives because it passes the module-level singleton `matcher_tag_traversable`, which `GridTaskConnectRooms` happens to bind first; a per-run matcher factory (the pattern review-01 §3.7 mandates) crashes. `GridTaskGenerateRoads.spec.js` misses it because it passes `CellMatcherAny.INSTANCE`, which needs no initialization.
|
|
43
|
+
- **Fix**: build the negated matcher once and initialize both in an init task that `tBuildDistanceField` / `tBuildPaths` / `tDrawPaths` depend on:
|
|
44
|
+
```js
|
|
45
|
+
const not_traversable = CellMatcherNot.from(traversable);
|
|
46
|
+
const tInitializeMatchers = actionTask(() => {
|
|
47
|
+
traversable.initialize(grid, seed);
|
|
48
|
+
not_traversable.initialize(grid, seed);
|
|
49
|
+
});
|
|
50
|
+
// tBuildDistanceField uses not_traversable; add tInitializeMatchers as a dependency of
|
|
51
|
+
// tBuildDistanceField, tBuildPaths and tDrawPaths.
|
|
52
|
+
```
|
|
53
|
+
- **Confidence**: Confirmed
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### GEN-3 `ThemeEngine.applyNodes` indexes samplers with fractional marker coordinates
|
|
58
|
+
- **Severity**: MAJOR
|
|
59
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/theme/ThemeEngine.js:166` (`getThemesByPosition`) and `:466`
|
|
60
|
+
- **Defect**: `applyNodes` passes `node.position.x/y` — continuous grid coordinates — straight into `Sampler2D.readChannel`, which computes `index = (y * width + x) * itemSize + channel`. With a fractional coordinate the index is fractional and a typed-array read returns `undefined`, not a value.
|
|
61
|
+
- `getThemesByPosition:168`: `if (filled === 0) continue;` — `undefined === 0` is `false`, so the mask refinement is skipped entirely and *every* theme whose quadtree bounds contain the point is accepted.
|
|
62
|
+
- `applyNodes:466-470`: `influenceSum += undefined` → `NaN`, so `influenceSum > 0` is false and the code always takes the "all themes at zero influence" branch (`randomIntegerBetween`), discarding the distance-field weighting.
|
|
63
|
+
`applyTerrainThemes` and `applyCellRules` are unaffected (they iterate integer cell indices).
|
|
64
|
+
- **Failure scenario**: markers produced by `GridTaskDensityMarkerDistribution` are always at fractional positions (`processArea:225`, `_x = x0 + u * width`), and they are the bulk of the marker set in `AlienGridGenerator`. Given two `AreaTheme`s whose bounds both cover grid cell (12,8) but where only theme A's mask is set there, a marker at (12.37, 8.91) reports `matchingThemeCount === 2` and is then assigned to A or B with 50/50 probability. With a single partially-filled mask, a marker at a fractional position outside the mask is themed as if it were inside. (The shipped game escapes this only because `LevelGenerator.js:169` fills one mask over the entire grid.)
|
|
65
|
+
- **Fix**: quantise to the cell the marker sits in before probing the mask/distance field, e.g. in `applyNodes`
|
|
66
|
+
```js
|
|
67
|
+
const x = Math.round(nodePosition.x);
|
|
68
|
+
const y = Math.round(nodePosition.y);
|
|
69
|
+
```
|
|
70
|
+
and use those for both `getThemesByPosition` and `areaMask.distanceField.readChannel`. (Rounding matches `COORDINATES.md`: integer grid coordinates are cell centers.)
|
|
71
|
+
- **Confidence**: Confirmed
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### GEN-4 `GridCellActionPlaceMarkerGroup` never initializes its children
|
|
76
|
+
- **Severity**: MAJOR
|
|
77
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/markers/GridCellActionPlaceMarkerGroup.js:11-81`
|
|
78
|
+
- **Defect**: the class inherits `GridCellAction.initialize`, which only runs asserts. It never forwards to `this.children`, so the wrapped `GridCellActionPlaceMarker`s (and therefore their `transformers`, which `GridCellActionPlaceMarker.initialize:50` is responsible for) are never initialized. Every other composite in the package forwards (`GridCellActionSequence:31`, `CellActionSelectRandom:41`, `MarkerNodeEmitterFromAction:33`).
|
|
79
|
+
- **Failure scenario**:
|
|
80
|
+
```js
|
|
81
|
+
const group = GridCellActionPlaceMarkerGroup.from([
|
|
82
|
+
GridCellActionPlaceMarker.from({
|
|
83
|
+
type: 'town',
|
|
84
|
+
transformers: [MarkerNodeTransformerRecordUniqueRandomEnum.from('name', ['a', 'b'])]
|
|
85
|
+
})
|
|
86
|
+
]);
|
|
87
|
+
group.initialize(grid, 0);
|
|
88
|
+
group.execute(grid, 1, 1, 0);
|
|
89
|
+
```
|
|
90
|
+
`MarkerNodeTransformerRecordUniqueRandomEnum.unused` is still the class-field default `[]` (it is only filled in `initialize`), so `transform` throws `Error('All values used up')` on the very first placement. With a `MarkerNodeTransformerYRotateByFilter(CellFilterSimplexNoise…)` instead, `this.noise` is still `null` and `execute` throws `TypeError: this.noise is not a function`. The two sample call sites (`mir_generator_place_bases.js:78`, `mir_generator_place_buff_objects.js:98`) only escape because their group children carry no transformers.
|
|
91
|
+
- **Fix**:
|
|
92
|
+
```js
|
|
93
|
+
initialize(data, seed) {
|
|
94
|
+
super.initialize(data, seed);
|
|
95
|
+
const children = this.children;
|
|
96
|
+
for (let i = 0; i < children.length; i++) {
|
|
97
|
+
children[i].initialize(data, seed);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
- **Confidence**: Confirmed
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### GEN-5 `CellFilterCache.execute` ignores `scale` when sampling the cache
|
|
106
|
+
- **Severity**: MAJOR
|
|
107
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/filtering/numeric/CellFilterCache.js:73-75`
|
|
108
|
+
- **Defect**: `initialize` builds a `scale·w × scale·h` cache where texel `(x,y)` holds `source(x/scale, y/scale)` (lines 43-67 — the correct ratio mapping per `COORDINATES.md`). `execute` then reads `this.__cache.sampleChannelBilinear(x, y, 0)` with the raw *grid* coordinate, i.e. it applies `grid_to_texel(v, 1)` instead of `grid_to_texel(v, scale)`.
|
|
109
|
+
- **Failure scenario**: 16×16 grid, `source` = the ramp filter `h(x,y) = x`, `CellFilterCache.from(source, 2)`. Cache is 32×32 with `cache[x] = x/2`. `filter.execute(grid, 10, 0, 0)` returns `5` instead of `10`. Cells `x = 0..15` sample cache texels `0..15`, i.e. only the left half of the grid's value range — the filter is magnified 2× and the right/bottom halves of the map are silently duplicated from the top-left quadrant. Any `scale !== 1` is affected; `scale === 1` is correct, which is why all current call sites (`MohFilterMinorSlope.js:22`, `MohGridFilterTechnology.js:42`, `AlienGridGenerator.js`) are unaffected.
|
|
110
|
+
- **Fix**: `return this.__cache.sampleChannelBilinear(x * this.scale, y * this.scale, 0);` (or `grid_to_texel(x, this.scale)`).
|
|
111
|
+
- **Confidence**: Confirmed
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### GEN-6 Density-distribution per-tile seeds collapse to ~2048 distinct RNG streams
|
|
116
|
+
- **Severity**: MAJOR
|
|
117
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/grid/generation/GridTaskDensityMarkerDistribution.js:343`
|
|
118
|
+
- **Defect**: `const seed = Math.floor(random() * Number.MAX_SAFE_INTEGER);`. `random()` returns `k / 2^32` for an integer `k < 2^32`, so the product is `k·2^21` (± 1 ulp) — the low 21 bits are always zero. Mulberry32 then does `_seed += 0x6D2B79F5` and truncates to int32, keeping only `k mod 2^11`. Effective seed space ≈ 2^11.
|
|
119
|
+
- **Failure scenario**: measured — feeding 4096 consecutive draws of `seededRandom(12345)` through this expression yields 4096 distinct raw seeds but only **1776 distinct effective int32 values**, consistent with 2048 buckets. A 128×128 grid is split into 64 tiles of 16×16 (`tile_size = 16`, lines 312/333/338), so the expected number of colliding tile pairs is `C(64,2)/2048 ≈ 0.98` — roughly 63 % of runs have at least one pair of tiles sharing an identical RNG stream. Two such tiles execute `processArea` with the same `(u, v)` sequence, the same density rolls and the same `scale.sampleRandom` draws, so wherever the density filter is locally flat they receive markers at identical intra-tile offsets — a visibly repeated decor cluster. `AlienGridGenerator` / `AlienCombatSceneGenerator` instantiate ~20 of these tasks per generation.
|
|
120
|
+
- **Fix**: `const seed = randomIntegerBetween(random, 0, 0x7FFFFFFF);` (or `(random() * 0x100000000) | 0`) so the full int32 range reaches Mulberry32.
|
|
121
|
+
- **Confidence**: Confirmed (entropy collapse measured; the visual repetition follows from identical streams over identical tile geometry)
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### GEN-7 `ThemeEngine.apply` never passes a seed to `applyNodes`
|
|
126
|
+
- **Severity**: MINOR
|
|
127
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/theme/ThemeEngine.js:633` (declaration at `:410`)
|
|
128
|
+
- **Defect**: `applyNodes(grid, ecd, seed = 0)` is called as `this.applyNodes(grid, ecd)`, so its `seededRandom(seed)` is always seeded `0` regardless of `ThemeEngine.seed`. Unlike GEN-1 the stream is well-formed; it is simply constant across world seeds.
|
|
129
|
+
- **Failure scenario**: two `AreaTheme`s with overlapping masks over the same region. Run the same generator with world seed `1` and world seed `2`: every marker inside the overlap is assigned to the same theme in both runs (the marker set differs, but the theme-selection draw sequence is identical), so the theme mix at boundaries never varies with the seed.
|
|
130
|
+
- **Fix**: `const tNodes = this.applyNodes(grid, ecd, randomIntegerBetween(this.random, 0, 0x7FFFFFFF));` (pairs with the GEN-1 fix).
|
|
131
|
+
- **Confidence**: Confirmed
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### GEN-8 `CellSupplierBestN` throws when `limit === 0`
|
|
136
|
+
- **Severity**: MINOR
|
|
137
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/grid/generation/grid/select/CellSupplierBestN.js:133-139`
|
|
138
|
+
- **Defect**: the eviction ladder is
|
|
139
|
+
```js
|
|
140
|
+
if (fitness_scratch.length < this.limit) { /* push */ }
|
|
141
|
+
else if (fitness_scratch[0].score < fitness) { fitness_scratch.shift(); }
|
|
142
|
+
else { continue; }
|
|
143
|
+
```
|
|
144
|
+
With `limit === 0` the first branch is false on the very first candidate and `fitness_scratch[0]` is `undefined`. `from()` only guards with `assert.isNonNegativeInteger(limit)`, which admits 0 and is stripped in production anyway.
|
|
145
|
+
- **Failure scenario**: `CellSupplierBestN.from({ fitness: CellFilterLiteralFloat.ONE, limit: 0 })` (e.g. a caller computing `limit` from a difficulty budget that evaluates to 0) → `initialize(grid, 0)` on any grid with at least one predicate-matching cell → `TypeError: Cannot read properties of undefined (reading 'score')`. The intended result is an empty selection.
|
|
146
|
+
- **Fix**: make the ladder handle a zero capacity, e.g. start with `if (this.limit === 0) { return; }` before the scan loop, or change the first branch to `if (fitness_scratch.length < this.limit) { … } else if (this.limit > 0 && fitness_scratch[0].score < fitness) { … }`.
|
|
147
|
+
- **Confidence**: Confirmed
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### GEN-9 `MarkerNodeEmitterPredicated` never initializes its predicate
|
|
152
|
+
- **Severity**: MINOR
|
|
153
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/generation/markers/emitter/MarkerNodeEmitterPredicated.js:44-46`
|
|
154
|
+
- **Defect**: `initialize(data, seed)` forwards only to `this.source`; `this.nodePredicate.initialize(grid, seed)` is never called. The `GridDataNodePredicate` interface declares `initialize(grid, seed)` and both combinators implement the recursion (`GridDataNodePredicateBinary:38`, `GridDataNodePredicateNot:34`), so the recursion exists but its root is never driven. `MarkerNodeEmitterPredicated` is the only consumer of predicates in the package.
|
|
155
|
+
- **Failure scenario**: any predicate that binds per-run state on initialize — the same shape as `GridLayerCellMatcher.initialize` (resolve `layerId` → `__layer`) — keeps `__layer === null` and throws `TypeError: Cannot read properties of null` on the first `evaluate`. The two shipped predicates (`…Overlaps`, `…Any`) have empty `initialize` bodies, so no in-repo configuration fails today; the defect is only reachable from library-user predicates.
|
|
156
|
+
- **Fix**:
|
|
157
|
+
```js
|
|
158
|
+
initialize(data, seed) {
|
|
159
|
+
this.source.initialize(data, seed);
|
|
160
|
+
this.nodePredicate.initialize(data, seed);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
- **Confidence**: Likely (contract violation confirmed by reading; no in-repo predicate exercises it)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Areas checked and found clean
|
|
168
|
+
|
|
169
|
+
- **Cellular automata** (`automata/CaveGeneratorCellularAutomata.js`, `grid/generation/discrete/GridTaskCellularAutomata.js`): true double-buffered synchronous step, correct edge/corner neighbour enumeration, OOB = dead as documented, thresholds match the doc comment, `front`/`back` swap read correctly by `tWriteTags`.
|
|
170
|
+
- **Coordinate conventions**: `grid_to_texel`/`texel_to_grid` used consistently by `GridCellActionWriteFilterToLayer`, `CellMatcherLayerBitMaskTest` (OOB → no match), `AbstractCellFilterSampleGridLayer`, `MarkerNodeActionPaintTerrain` (region clamped, symmetric rounding), `populateSampler2DFromCellFilter`. `ContinuousGridCellActionSetTerrainHeight`'s corner mapping is the documented deliberate deviation. Row/column stride (`index % width`, `(index / width) | 0`) is consistent everywhere including non-square grids.
|
|
171
|
+
- **Grid tasks / scheduling**: `GridTaskGroup` sibling-only dependency resolution, `GridTaskSequence` chaining, `TaskGroup.addDependency` group flattening, `countTask`/`randomCountTask` initializer-time limit resolution — all consistent; no task reads a layer written by a task it does not depend on.
|
|
172
|
+
- **Placement / rejection sampling**: `GridTaskExecuteRuleTimes` closes every attempted cell and terminates via `closedCount >= gridSize`; `GridTaskDensityMarkerDistribution.processArea` bounds both the iteration and the rejection budget; `GridTaskConnectRooms.tMain` makes monotone progress in `connected`; `buildPathFromDistanceMap` is strictly decreasing so it cannot loop.
|
|
173
|
+
- **Weighted selection**: `random_pick_weighted_index` cumulative bound and zero-total guard are correct and shared by both selects; the RNG is not consumed on the zero-weight path.
|
|
174
|
+
- **Comparators**: `byScore`/`byIndex` in `CellSupplierBestN`, the priority sort in `applyNodes`, and the layer-descriptor sort in `applyTerrainThemes` are all well-formed numeric comparators.
|
|
175
|
+
- **Module-static scratch**: `GridData.marker_collection`, `CellFilterCurvature`'s normals, `CellFilterAngleToNormal.normal`, `CellFilterLookupTable.sample`, `RoadConnectionNetwork.scratch_paths`, `CellSupplierPathUpHill.scratch_neighbours`, both `evaluatedWeights` arrays — none can be re-entered by the code paths that reach them (`computeFilterSurfaceNormal` writes its result only after all sampling, as documented).
|
|
176
|
+
- **Determinism sources**: no `Math.random`, `Date.now`, `performance.now` or `new Date()` anywhere in the package; every iteration over a `Map`/quadtree is insertion-ordered; `ArrayIteratorRandom` defaults to `seededRandom(0)` rather than an unseeded source.
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# Correctness review — `core/geom/` 2D half + shared primitives
|
|
2
|
+
|
|
3
|
+
Scope: everything under `core/geom/2d/`, plus the loose files directly in `core/geom/`
|
|
4
|
+
(`Vector1.js`, `Vector2.js`, `Vector3.js`, `Vector4.js`, `Quaternion.js`, `ConicRay.js`,
|
|
5
|
+
`normalize_angle_rad.js`) and the shared sub-packages `vec/`, `vec2/`, `vec4/`, `mat2/`,
|
|
6
|
+
`mat3/`, `packing/`, `random/`.
|
|
7
|
+
`AABB3`, `Ray`, `Plane`, `Sphere`, `Frustum`, `Transform`, `Matrix4` all live under
|
|
8
|
+
`core/geom/3d/` and were **not** reviewed (other reviewer's scope). `vec3/` likewise skipped
|
|
9
|
+
as 3D-specific.
|
|
10
|
+
|
|
11
|
+
Every finding below was reproduced by executing the real module in node
|
|
12
|
+
(the files are plain ESM and import cleanly with `file:///` URLs, no jest needed).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Files with no adjacent spec (prioritised, in review order)
|
|
17
|
+
|
|
18
|
+
High-risk (non-trivial logic, no spec):
|
|
19
|
+
`2d/line/ray2_ray2_compute_intersection_array_2d.js`,
|
|
20
|
+
`2d/line/segment2_intersects_segment2_2d.js`,
|
|
21
|
+
`2d/line/segment2_segment_compute_intersection_array_2d.js`,
|
|
22
|
+
`2d/line/line2_segment_intersection_fraction_2d.js`,
|
|
23
|
+
`2d/convex-hull/convex_hull_fixed_count_2d_bruteforce.js`,
|
|
24
|
+
`2d/convex-hull/convex_hull_fixed_count_2d_relaxation.js`,
|
|
25
|
+
`2d/quad-tree/qt_collect_by_box.js`, `qt_collect_by_circle.js`,
|
|
26
|
+
`qt_query_closest_to_point.js`, `qt_query_data_raycast.js`, `qt_collect_data_all.js`,
|
|
27
|
+
`2d/triangle/tri2_compute_barycentric.js`, `2d/triangle/tri2_contains_point.js`,
|
|
28
|
+
`2d/aabb/aabb2_intersects_ray.js`, `aabb2_intersects_circle.js`,
|
|
29
|
+
`aabb2_compute_intersection.js`, `aabb2_signed_distance_to_point.js`,
|
|
30
|
+
`2d/polygon/polygon2_triangulation_deviation.js`,
|
|
31
|
+
`packing/max-rect/*` (whole directory), `packing/miniball/*`,
|
|
32
|
+
`mat3/m3_invert.js`, `m3_make_rotation_TBN.js`, `m3_rm_compose_transform.js`,
|
|
33
|
+
`vec/vector_normalize.js` (spec exists but does not cover the zero vector).
|
|
34
|
+
|
|
35
|
+
Low-risk / trivially correct on read (verified, no findings):
|
|
36
|
+
`2d/aabb/aabb2_area.js`, `aabb2_array_set.js`, `aabb2_contains_point.js`,
|
|
37
|
+
`aabb2_distance_sqr_to_point.js`, `aabb2_union_array.js`,
|
|
38
|
+
`2d/circle/circle_*.js`, `2d/rectangle_to_aabb.js`, `2d/spline2_quadratic_bezier.js`,
|
|
39
|
+
`vec2/*`, `vec4/*`, `mat2/*`, `vec/vector_*`, `random/*`, all the `@deprecated`
|
|
40
|
+
rename shims.
|
|
41
|
+
|
|
42
|
+
Areas checked and found **clean** (no finding to report):
|
|
43
|
+
- `polygon2_triangulate.js` — faithful earcut port; the sign inversion at `orient()`
|
|
44
|
+
(line 202-204) is correct and consistently applied through every one of ~20 comparisons
|
|
45
|
+
(verified against earcut 3.x term by term). 400 random simple polygons + 300
|
|
46
|
+
hole cases: area deviation `0` in every case. Duplicated-vertex contours agree
|
|
47
|
+
above and below `HASH_THRESHOLD`. `ensure_node_capacity` bound (`3V + 2C`) is sound.
|
|
48
|
+
- `polygon2_is_counter_clockwise.js` — 20 000 randomised contours at offset (1000,1000),
|
|
49
|
+
zero disagreements with `polygon2_signed_area`'s sign. The exact-predicate-at-the-lex-min
|
|
50
|
+
vertex argument is valid (a simple polygon cannot be collinear at its lex-min vertex).
|
|
51
|
+
- `polygon2_signed_area.js` / `tri2_signed_area.js` — the "form differences first"
|
|
52
|
+
rewrites are algebraically identical to the shoelace/cross reference and correctly
|
|
53
|
+
handle the repeated-first-as-last point.
|
|
54
|
+
- `v2_morton_encode.js` — `split_by_2` masks to 16 bits, the `<< 1` + `>>> 0` handles
|
|
55
|
+
bit 31 correctly.
|
|
56
|
+
- `orient2d_indexed.js` — confirmed by execution that `robust-predicates@3.0.2`
|
|
57
|
+
`orient2d(0,0, 1,0, 0,1) === -1`, i.e. NEGATIVE for CCW. `convex_hull_monotone_2d`
|
|
58
|
+
(ported from `monotone-convex-hull-2d`, which used `robust-orientation` — same
|
|
59
|
+
inverted convention) and `convex_hull_jarvis_2d` are both consistent with it.
|
|
60
|
+
- `aabb2_intersects_ray.js` — matches Geometric Tools `IntrRay2Box2::Test` exactly.
|
|
61
|
+
- `aabb2_clip_line_cohen_sutherland.js` — matches the reference Cohen–Sutherland.
|
|
62
|
+
- `aabb2_signed_distance_sqr_to_point.js` — the `-dx * dx` in the penetration branch
|
|
63
|
+
reads as a precedence bug but is correct (`(-dx)*dx == -(dx*dx)` since `dx < 0` there).
|
|
64
|
+
- `circle_compute_intersection.js` — matches Bourke's circle-circle formula term for term.
|
|
65
|
+
- `tri2_rasterize_conservative.js` — SAT axis set is complete (2 box + 3 edge normals),
|
|
66
|
+
and touching counts as overlap, which is what "conservative" requires.
|
|
67
|
+
- `packing/skyline.js` — the `count + 2` scratch bound is provably sufficient (at most one
|
|
68
|
+
segment can straddle both cut edges); `find`'s dead `x < best_x` tie-break is harmless
|
|
69
|
+
because segments are x-ascending, so first-found is already leftmost.
|
|
70
|
+
- `tri2_contains_point.js` — the `Float32Array` scratch was suspected of losing the
|
|
71
|
+
`|1-u-v-w| < 1e-7` test; 400 000 fuzz cases (including coordinates at 1e5) produced
|
|
72
|
+
zero false negatives at triangle centroids. Not a defect.
|
|
73
|
+
- `Vector3.crossVectors` reads all six components before writing, so
|
|
74
|
+
`m3_make_rotation_TBN`'s `v3_scratch_1.crossVectors(v3_scratch_2, v3_scratch_1)`
|
|
75
|
+
aliasing is safe.
|
|
76
|
+
- `StaticR2Tree.build()` heap-layout arithmetic, `qt_collect_data_all` cursor arithmetic,
|
|
77
|
+
`QuadTreeNode` merge/split `treeDataCount` bookkeeping — all verified correct.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### GEOM2-1 `polygon2_clip_halfplane` extrapolates the crossing vertex far outside the segment
|
|
82
|
+
- **Severity**: MAJOR
|
|
83
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/polygon/polygon2_clip_halfplane.js:32-46`
|
|
84
|
+
- **Defect**: `in_a`/`in_b` classify vertices with an epsilon-slacked side test
|
|
85
|
+
(`sa >= -POINT_EPS`), but the emitted crossing uses the *exact* line parameter
|
|
86
|
+
`t = sa / (sa - sb)`. When `in_a !== in_b` while `sa` and `sb` have the **same** sign
|
|
87
|
+
(`sa` inside the slack band, `sb` just outside it) `t` is negative and unbounded as
|
|
88
|
+
`sa - sb → 0`, so the emitted vertex is extrapolated backwards off the segment.
|
|
89
|
+
The axis-aligned twin `polygon2_clip_axis_halfplane` has no epsilon and therefore
|
|
90
|
+
cannot produce this — its `in_a !== in_b` really does imply opposite sides, so
|
|
91
|
+
`t ∈ [0,1]`.
|
|
92
|
+
- **Failure scenario**:
|
|
93
|
+
```js
|
|
94
|
+
polygon2_clip_halfplane(
|
|
95
|
+
Float64Array.from([0, -1e-9, 1, -1.0000000001e-9, 1, 5, 0, 5]), 4,
|
|
96
|
+
out, /*ex*/0, /*ey*/0, /*dx*/1, /*dy*/0, /*keep_left*/true);
|
|
97
|
+
```
|
|
98
|
+
Here `s == y`, so `sa = -1e-9` (inside the slack, `in_a = true`) and
|
|
99
|
+
`sb = -1.0000000001e-9` (outside it, `in_b = false`), `sa - sb = 1e-19`,
|
|
100
|
+
`t = -1e10`.
|
|
101
|
+
Produced: 5 vertices, the second being `(-10000006779.697906, 0)`.
|
|
102
|
+
Expected: the crossing must lie **on** the segment `(0,-1e-9) → (1,-1e-9)`,
|
|
103
|
+
i.e. x ∈ [0, 1] (Sutherland–Hodgman, Foley & van Dam §3.14: the emitted point is
|
|
104
|
+
`A + t(B-A)` with `t ∈ [0,1]` by construction).
|
|
105
|
+
Every downstream consumer of the clipped polygon (area, containment, further clips)
|
|
106
|
+
is then wrong by ten orders of magnitude. The mirrored case
|
|
107
|
+
(`keep_left = false`, `sa ∈ (0, POINT_EPS]`) is identical.
|
|
108
|
+
- **Fix**: clamp the parameter — the crossing is on the segment by construction:
|
|
109
|
+
`const t = clamp01(sa / (sa - sb));` (`core/math/clamp01.js`). Alternatively decide
|
|
110
|
+
`in_a`/`in_b` on the exact sign (`sa >= 0`) and use `POINT_EPS` only to snap a
|
|
111
|
+
near-boundary *vertex* onto the kept side, which is what the comment claims the
|
|
112
|
+
epsilon is for.
|
|
113
|
+
- **Confidence**: Confirmed (executed).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### GEOM2-2 `shg_query_elements_line` skips cells the segment passes through, so it misses elements
|
|
118
|
+
- **Severity**: MAJOR
|
|
119
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/hash-grid/shg_query_elements_line.js:108-131`
|
|
120
|
+
- **Defect**: the grid walk is not a DDA. It steps to whichever of the two candidate
|
|
121
|
+
cells has the **cell centre** closest to the segment
|
|
122
|
+
(`line2_compute_segment_point_distance_sqr(... x+step_x+0.5, y+0.5)` vs
|
|
123
|
+
`... x+0.5, y+step_y+0.5`). That ordering is not equivalent to the order in which the
|
|
124
|
+
segment actually crosses cell boundaries, so the 4-connected path can step onto a cell
|
|
125
|
+
the segment never enters *instead of* one it does. Elements registered only in the
|
|
126
|
+
skipped cell are never visited — a silent false negative.
|
|
127
|
+
- **Failure scenario**: `new SpatialHashGrid(8, 8, 1)` holding a single element with
|
|
128
|
+
bounds `(3.0, 3.0)–(3.999, 3.999)` (entirely inside cell `(3,3)`), queried with
|
|
129
|
+
```js
|
|
130
|
+
shg_query_elements_line(result, 0, grid,
|
|
131
|
+
5.516462491825223, 3.617749162018299,
|
|
132
|
+
3.8124522920697927, 4.040001254528761);
|
|
133
|
+
```
|
|
134
|
+
Produced: `0` hits.
|
|
135
|
+
Expected: `1` hit — `aabb2_clip_line_cohen_sutherland` on the same segment and the same
|
|
136
|
+
box returns `true` (clip `(3.999, 3.9938)–(3.9779, 3.999)`). The segment crosses `y = 4`
|
|
137
|
+
at `x = 3.9739`, i.e. inside column 3, so the true cell sequence is
|
|
138
|
+
`(5,3) (4,3) (3,3) (3,4)`; the walk produces `(5,3) (4,3) (4,4) (3,4)`.
|
|
139
|
+
A 4000-segment fuzz over an 8×8 grid missed at least one truly-crossed cell in
|
|
140
|
+
**14 cases (0.35 %)**.
|
|
141
|
+
- **Fix**: replace the centre-distance heuristic with the standard Amanatides–Woo
|
|
142
|
+
voxel traversal (Amanatides & Woo 1987, *A Fast Voxel Traversal Algorithm for Ray
|
|
143
|
+
Tracing*): precompute `t_max_x`/`t_max_y` to the next cell boundary and
|
|
144
|
+
`t_delta_x`/`t_delta_y`, then step the axis with the smaller `t_max`. That is exact by
|
|
145
|
+
construction and no more expensive per step.
|
|
146
|
+
- **Confidence**: Confirmed (executed end-to-end against the real `SpatialHashGrid`).
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### GEOM2-3 `convex_hull_fixed_count_2d_bruteforce` minimises a SIGNED area, so clockwise input yields the *largest* polygon
|
|
151
|
+
- **Severity**: MAJOR
|
|
152
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_bruteforce.js:111-116`
|
|
153
|
+
- **Defect**: `const area = polygon2_signed_area(intersections, k); if (area < best_area)`.
|
|
154
|
+
`polygon2_signed_area` is **signed** — negative for a clockwise loop. The intersection
|
|
155
|
+
polygon inherits the input's winding, so for clockwise input every candidate area is
|
|
156
|
+
negative and `area < best_area` selects the *most negative*, i.e. the maximum-area
|
|
157
|
+
polygon. The function's contract ("such that the area would be minimal") and its input
|
|
158
|
+
contract ("must be an ordered loop… must form a convex hull") say nothing about winding,
|
|
159
|
+
and nothing normalises it.
|
|
160
|
+
- **Failure scenario**: regular octagon, circumradius 10, `output_point_count = 4`.
|
|
161
|
+
- CCW input → produced quad area **341.421**, which is the true minimum
|
|
162
|
+
(the square on four alternating edge lines, side `2·10·cos 22.5° = 18.478`).
|
|
163
|
+
- The *same octagon* wound CW → produced quad area **482.843**
|
|
164
|
+
(`(0,-24.14) (-10,0) (0,24.14) (10,0)`), 41 % larger than the achievable minimum.
|
|
165
|
+
- 12-gon: CCW → 373.205 (correct minimum, `= (2·10·cos15°)²`); CW → **746.410**, exactly
|
|
166
|
+
2× the minimum.
|
|
167
|
+
- **Fix**: compare `Math.abs(area)`, or normalise the input winding once at entry
|
|
168
|
+
(`polygon2_is_counter_clockwise` + reversed iteration) so the objective is unsigned.
|
|
169
|
+
- **Confidence**: Confirmed (executed).
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### GEOM2-4 `convex_hull_fixed_count_2d_relaxation` uses a signed collapse cost, so clockwise input diverges
|
|
174
|
+
- **Severity**: MAJOR
|
|
175
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/convex-hull/convex_hull_fixed_count_2d_relaxation.js:161-170`
|
|
176
|
+
- **Defect**: `compute_collapse_cost` returns `tri2_signed_area(v0, target, v1)`, fed
|
|
177
|
+
straight into `edge_heap` and consumed with `pop_min`. For a counter-clockwise loop the
|
|
178
|
+
collapse target lies to the right of `v0→v1`, so the signed area is positive and the
|
|
179
|
+
greedy correctly removes the cheapest (smallest added area) edge. For a clockwise loop
|
|
180
|
+
every cost is negative, so `pop_min` selects the collapse that adds the **most** area,
|
|
181
|
+
and the hull runs away. (The commented-out `if (area < 0 || Number.isNaN(area))
|
|
182
|
+
debugger;` at lines 166-168 shows the non-negative assumption.)
|
|
183
|
+
- **Failure scenario**: regular 12-gon, circumradius 10, `output_point_count = 4`.
|
|
184
|
+
- CCW input → 4 points, area 402.07 (greedy, documented as suboptimal vs the
|
|
185
|
+
exact 373.205 — acceptable).
|
|
186
|
+
- Same 12-gon wound CW → 4 points at
|
|
187
|
+
`(1790557056.00, 6682451968.00), (8.66,-5.00), (-5.00,-18.66), (-10.00,0)`,
|
|
188
|
+
area **6.68e10**. Every input point has |coordinate| ≤ 10; a bounding polygon
|
|
189
|
+
must too, and its area cannot exceed a few hundred.
|
|
190
|
+
- **Fix**: `return Math.abs(area)` in `compute_collapse_cost`, or normalise the input
|
|
191
|
+
winding at entry (same fix as GEOM2-3, and the two functions should share it).
|
|
192
|
+
- **Confidence**: Confirmed (executed).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### GEOM2-5 `ray2_ray2_compute_intersection_array_2d` rejects every intersection with `t < 0.5`
|
|
197
|
+
- **Severity**: MAJOR
|
|
198
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/line/ray2_ray2_compute_intersection_array_2d.js:33-34`
|
|
199
|
+
- **Defect**: `if (t < 0.5) return false; // Intersects on the wrong side`. For a ray,
|
|
200
|
+
"wrong side" is `t < 0`; `0.5` discards the entire first half of the parameter range.
|
|
201
|
+
The `t` derivation above it is correct (`t = (dir_b × (o_a - o_b)) / (dir_a × dir_b)`),
|
|
202
|
+
so the rejection threshold is the only defect. The adjacent spec
|
|
203
|
+
(`2d/intersect_ray_2d.spec.js`) only exercises `t = 2`, so it does not pin this down.
|
|
204
|
+
- **Failure scenario**: ray A origin `(0,0)` direction `(1,0)`; ray B origin `(0.25,-1)`
|
|
205
|
+
direction `(0,1)`. The rays cross at `(0.25, 0)` with `t = 0.25 ≥ 0`.
|
|
206
|
+
Produced: `false`, `result` untouched.
|
|
207
|
+
Expected: `true`, `result = [0.25, 0]`. (Same call with B at `x = 0.75` → `t = 0.75`
|
|
208
|
+
→ correctly returns `true, [0.75, 0]`, confirming the threshold is what rejects it.)
|
|
209
|
+
- **Fix**: `if (t < 0) return false;`.
|
|
210
|
+
- **Confidence**: Confirmed (executed).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### GEOM2-6 `ray2_ray2_compute_intersection_array_2d` uses an absolute parallelism epsilon on an unnormalised cross product
|
|
215
|
+
- **Severity**: MAJOR
|
|
216
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/line/ray2_ray2_compute_intersection_array_2d.js:21-26`
|
|
217
|
+
- **Defect**: `if (Math.abs(denominator) < 1e-7) return false;` where
|
|
218
|
+
`denominator = dir_a × dir_b`. The function's own doc requires the directions to be
|
|
219
|
+
**unnormalised**, "derived from line length", so the cross product scales as
|
|
220
|
+
`|dir_a|·|dir_b|·sin θ`. For short edges the magnitude collapses and perfectly
|
|
221
|
+
perpendicular rays are reported parallel. `convex_hull_fixed_count_2d_bruteforce` and
|
|
222
|
+
`..._relaxation` build their directions exactly this way from polygon edges, so a
|
|
223
|
+
small-scale polygon silently loses valid edge intersections.
|
|
224
|
+
- **Failure scenario**: ray A origin `(0,0)` direction `(1e-4, 0)`; ray B origin
|
|
225
|
+
`(0.5e-4, -1e-4)` direction `(0, 1e-4)`. `denominator = 1e-8 < 1e-7`.
|
|
226
|
+
Produced: `false` ("parallel").
|
|
227
|
+
Expected: `true`, `result = [0.5e-4, 0]` — the directions are exactly orthogonal
|
|
228
|
+
(`sin θ = 1`).
|
|
229
|
+
- **Fix**: make the test scale-relative, e.g. reject when
|
|
230
|
+
`Math.abs(denominator) <= EPS * Math.hypot(dir_a_x, dir_a_y) * Math.hypot(dir_b_x, dir_b_y)`,
|
|
231
|
+
or test `denominator === 0` and let the caller filter by the resulting magnitude.
|
|
232
|
+
- **Confidence**: Confirmed (executed).
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
### GEOM2-7 `segment2_intersects_segment2_2d` / `..._compute_intersection_array_2d` report collinear overlapping segments as disjoint
|
|
237
|
+
- **Severity**: MINOR
|
|
238
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/line/segment2_intersects_segment2_2d.js:23-28`
|
|
239
|
+
and `2d/line/segment2_segment_compute_intersection_array_2d.js:33-38`
|
|
240
|
+
- **Defect**: `inv_denom = 1 / (-s2_x*s1_y + s1_x*s2_y)`. For parallel segments the
|
|
241
|
+
denominator is `0`, so `inv_denom = Infinity`; for *collinear* segments the numerators
|
|
242
|
+
are also `0`, giving `s = t = 0 * Infinity = NaN`, and every comparison against `NaN`
|
|
243
|
+
is false. The in-file twin used by the triangulator
|
|
244
|
+
(`2d/polygon/polygon2_triangulate.js:425-452 segments_intersect`) explicitly handles the
|
|
245
|
+
collinear-touching case via `on_segment`, so the two "do these segments intersect"
|
|
246
|
+
predicates in the same package disagree.
|
|
247
|
+
- **Failure scenario**: `segment2_intersects_segment2_2d(0,0, 2,0, 1,0, 3,0)` — the two
|
|
248
|
+
segments overlap along `x ∈ [1,2]` on the line `y = 0`.
|
|
249
|
+
Produced: `false`. Expected: `true` (they share infinitely many points).
|
|
250
|
+
This matters for grid-aligned data, where collinear edges are the common case.
|
|
251
|
+
- **Fix**: branch on `denominator === 0` and, when the segments are also collinear
|
|
252
|
+
(`cross(b0 - a0, s1) === 0`), decide by 1D interval overlap along the dominant axis —
|
|
253
|
+
the same structure `polygon2_triangulate.segments_intersect` already uses. If the
|
|
254
|
+
exclusion is deliberate, document it on both functions.
|
|
255
|
+
- **Confidence**: Confirmed (executed).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
### GEOM2-8 `min_area_rect_2d` returns `area: Infinity` alongside `width: 0, height: 0`
|
|
260
|
+
- **Severity**: MINOR
|
|
261
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/oriented-box/min_area_rect_2d.js:26,38-40,68`
|
|
262
|
+
- **Defect**: `best_area` is seeded to `Infinity` and only ever lowered inside the loop.
|
|
263
|
+
When every hull edge is shorter than the `1e-12` guard the loop body is `continue`d for
|
|
264
|
+
all of them and the seed is returned, while `best_width`/`best_height` keep their `0`
|
|
265
|
+
seeds. The `h < 2` early-out at line 22-24 does not cover this: `convex_hull_monotone_2d`
|
|
266
|
+
returns two indices for three coincident points.
|
|
267
|
+
- **Failure scenario**: `min_area_rect_2d([1,1, 1,1, 1,1], 3)`
|
|
268
|
+
Produced: `{ angle: 0, width: 0, height: 0, area: Infinity }`.
|
|
269
|
+
Expected: `{ angle: 0, width: 0, height: 0, area: 0 }` — the object is internally
|
|
270
|
+
inconsistent as returned (`area !== width * height`), and a caller ranking candidate
|
|
271
|
+
boxes by `area` will rank a degenerate cluster as the worst possible instead of the best.
|
|
272
|
+
Also reachable with two near-coincident points: `min_area_rect_2d([0,0, 1e-13,0], 2)`.
|
|
273
|
+
- **Fix**: after the loop, `if (best_area === Infinity) { return { angle: 0, width: 0,
|
|
274
|
+
height: 0, area: 0 }; }` — or derive the returned `area` from `best_width * best_height`
|
|
275
|
+
so the three fields cannot disagree.
|
|
276
|
+
- **Confidence**: Confirmed (executed).
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### GEOM2-9 `convex_hull_monotone_2d` and `convex_hull_jarvis_2d` return opposite windings
|
|
281
|
+
- **Severity**: MINOR
|
|
282
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/convex-hull/convex_hull_monotone_2d.js:103-114`
|
|
283
|
+
vs `2d/convex-hull/convex_hull_jarvis_2d.js:64-87`
|
|
284
|
+
- **Defect**: both are correct hulls, but they traverse in opposite directions and neither
|
|
285
|
+
documents its winding. `convex_hull_jarvis_2d`'s doc says only "ordered to form a closed
|
|
286
|
+
loop around the hull"; `convex_hull_monotone_2d` says nothing. A caller that swaps one
|
|
287
|
+
for the other (they are drop-in compatible by signature and return type — flat xy in,
|
|
288
|
+
index array out) silently inverts every downstream orientation test.
|
|
289
|
+
- **Failure scenario**: `points = [0,0, 4,0, 4,4, 0,4, 2,2]`, `point_count = 5`.
|
|
290
|
+
- `convex_hull_jarvis_2d` → `[0, 1, 2, 3]` = `(0,0) (4,0) (4,4) (0,4)`,
|
|
291
|
+
signed area `+16` (counter-clockwise).
|
|
292
|
+
- `convex_hull_monotone_2d` → `[0, 3, 2, 1]` = `(0,0) (0,4) (4,4) (4,0)`,
|
|
293
|
+
signed area `−16` (clockwise).
|
|
294
|
+
Existing consumers happen to be winding-insensitive (`min_area_rect_2d` takes
|
|
295
|
+
min/max projections), so nothing is broken today — but the contract gap is a live trap.
|
|
296
|
+
- **Fix**: document the winding on both, and make them agree (reverse the merge order in
|
|
297
|
+
`convex_hull_monotone_2d`'s `result` assembly, lines 107-113, is the smaller change).
|
|
298
|
+
- **Confidence**: Confirmed (executed).
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### GEOM2-10 `qt_match_data_by_circle` / `qt_query_data_raycast` throw when the documented-optional `predicate` is omitted
|
|
303
|
+
- **Severity**: MINOR
|
|
304
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/quad-tree/qt_match_data_by_circle.js:17,56`
|
|
305
|
+
and `2d/quad-tree/qt_query_data_raycast.js:18,76`
|
|
306
|
+
- **Defect**: both declare `@param {function(...)} [predicate]` and
|
|
307
|
+
`@param {*} [predicateContext]` — square brackets, i.e. optional — but call
|
|
308
|
+
`predicate.call(predicateContext, ...)` unconditionally for every candidate datum.
|
|
309
|
+
- **Failure scenario**:
|
|
310
|
+
```js
|
|
311
|
+
const tree = new QuadTreeNode(0, 0, 100, 100);
|
|
312
|
+
tree.add("a", 1, 1, 2, 2);
|
|
313
|
+
qt_match_data_by_circle([], 0, tree, 1.5, 1.5, 5); // predicate omitted
|
|
314
|
+
```
|
|
315
|
+
Produced: `TypeError: Cannot read properties of undefined (reading 'call')`.
|
|
316
|
+
Expected: `1` (the datum is inside the circle), per the JSDoc.
|
|
317
|
+
Note the throw only happens once a candidate is found, so an empty tree passes and the
|
|
318
|
+
bug hides until there is data.
|
|
319
|
+
- **Fix**: either drop the brackets in the JSDoc (making `predicate` required — the
|
|
320
|
+
no-predicate case is already served by `qt_collect_by_circle`), or guard:
|
|
321
|
+
`if (d2_to_datum <= r2 && (predicate === undefined || predicate.call(...)))`.
|
|
322
|
+
Given the codebase preference for uniform control flow, the JSDoc fix is the better one.
|
|
323
|
+
- **Confidence**: Confirmed (executed).
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### GEOM2-11 `StaticR2Tree.sort_morton` seeds the shared scratch stack at absolute index 0, but reads from `stack.pointer`
|
|
328
|
+
- **Severity**: MINOR
|
|
329
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/r-tree/StaticR2Tree.js:219-245`
|
|
330
|
+
- **Defect**:
|
|
331
|
+
```js
|
|
332
|
+
const stack_top = stack.pointer;
|
|
333
|
+
let stackPointer = stack_top + 2;
|
|
334
|
+
stack[0] = 0; // <- absolute 0
|
|
335
|
+
stack[1] = this.__node_count_leaf - 1; // <- absolute 1
|
|
336
|
+
...
|
|
337
|
+
while (stackPointer > stack_top) {
|
|
338
|
+
stackPointer -= 2;
|
|
339
|
+
const right = stack[stackPointer + 1]; // <- reads stack_top+1
|
|
340
|
+
const left = stack[stackPointer]; // <- reads stack_top
|
|
341
|
+
```
|
|
342
|
+
The seed is written at absolute indices 0/1 while the first pop reads
|
|
343
|
+
`stack_top`/`stack_top + 1`. This is only correct when `stack.pointer === 0`.
|
|
344
|
+
`SCRATCH_UINT32_TRAVERSAL_STACK` is a module-level singleton shared with
|
|
345
|
+
`bvh2d_query_all_data_by_circle` and the other traversals.
|
|
346
|
+
- **Failure scenario**: with `SCRATCH_UINT32_TRAVERSAL_STACK.pointer === 4` on entry
|
|
347
|
+
(a BVH traversal in progress, e.g. `sort_morton` called from a query callback), the
|
|
348
|
+
first iteration reads `stack[4]`/`stack[5]` — stale node indices from that traversal —
|
|
349
|
+
as the quicksort range. With those being arbitrary node ids, `__swap_leaves` writes
|
|
350
|
+
outside the leaf block or the sort silently does nothing; the tree is then built from
|
|
351
|
+
unsorted leaves. Expected: the sort range is always `[0, leaf_count-1]`.
|
|
352
|
+
Also note `sort_morton` never claims or releases `stack.pointer`, so it corrupts the
|
|
353
|
+
in-flight traversal's stack regardless.
|
|
354
|
+
- **Fix**: write the seed relative to the claimed region —
|
|
355
|
+
`stack[stack_top] = 0; stack[stack_top + 1] = this.__node_count_leaf - 1;` — and
|
|
356
|
+
advance/restore `stack.pointer` around the sort the way the query functions do
|
|
357
|
+
(`stack.pointer = stackPointer` on entry, `stack.pointer = stack_top` on exit).
|
|
358
|
+
- **Confidence**: Likely (the index mismatch is unambiguous; the reachability depends on
|
|
359
|
+
a nested call, which the file's own "mostly untested" note does not exclude).
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### GEOM2-12 `vector_normalize` produces NaN for a zero vector while its 2D twin returns zero
|
|
364
|
+
- **Severity**: MINOR
|
|
365
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/vec/vector_normalize.js:19-23`
|
|
366
|
+
- **Defect**: `const magnitude_inv = 1 / Math.sqrt(magnitude2);` with no zero check.
|
|
367
|
+
Every sibling normalise in the package guards: `vec2/v2_array_normalize.js:24`
|
|
368
|
+
(`length_sqr !== 0 ? 1/sqrt(...) : 1`), `Vector2.normalize():621`,
|
|
369
|
+
`Vector3.normalize():560`. The N-dimensional one is the odd one out, and its spec
|
|
370
|
+
(`vec/vector_normalize.spec.js`) does not cover the case.
|
|
371
|
+
- **Failure scenario**: `const r = [0,0,0]; vector_normalize(r, [0,0,0], 3);`
|
|
372
|
+
Produced: `r === [NaN, NaN, NaN]`.
|
|
373
|
+
Expected (per the behaviour of every twin in this package): `[0, 0, 0]` — the result is
|
|
374
|
+
left unscaled rather than poisoned. A NaN written into a solver's basis vector then
|
|
375
|
+
propagates silently through every subsequent dot product.
|
|
376
|
+
- **Fix**: `const magnitude_inv = magnitude2 !== 0 ? 1 / Math.sqrt(magnitude2) : 1;`
|
|
377
|
+
(exactly the form `v2_array_normalize` already uses).
|
|
378
|
+
- **Confidence**: Confirmed (executed).
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
### GEOM2-13 `Vector2`/`Vector3`/`Quaternion` shadow the typed-array `length` property with a `length()` method
|
|
383
|
+
- **Severity**: MINOR
|
|
384
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/Vector2.js:605-607`
|
|
385
|
+
(same shape at `Vector3.js:543` and `Quaternion.js`)
|
|
386
|
+
- **Defect**: these classes `extend Float64Array` and also define a prototype method named
|
|
387
|
+
`length`. `Vector2.prototype.length` sits between the instance and
|
|
388
|
+
`%TypedArray%.prototype`'s `length` getter, so `instance.length` resolves to the
|
|
389
|
+
**function**, not the element count. The class doc at `Vector2.js:25` claims instances
|
|
390
|
+
are "usable directly as typed-array views"; that claim does not hold for any generic
|
|
391
|
+
consumer that reads `.length`. The `normalize()` comment at lines 614-618 documents that
|
|
392
|
+
V8 sometimes resolves `this.length` to the number instead — i.e. the property is not
|
|
393
|
+
merely wrong, it is *inconsistently* wrong depending on JIT state, which is why
|
|
394
|
+
`normalize()` had to be rewritten to call `v2_length` directly.
|
|
395
|
+
- **Failure scenario**: `const v = new Vector2(3, 4);`
|
|
396
|
+
- `typeof v.length` → `"function"` (expected `"number"`, value `2`).
|
|
397
|
+
- `Array.prototype.slice.call(v)` → `[]` (`ToLength(function) === 0`);
|
|
398
|
+
expected `[3, 4]`.
|
|
399
|
+
- `Array.prototype.indexOf.call(v, 3)` → `-1`; expected `0`.
|
|
400
|
+
(Internal typed-array methods and `Array.from(v)` still work, because they use the
|
|
401
|
+
internal `[[ArrayLength]]` slot / the iterator rather than the property.)
|
|
402
|
+
- **Fix**: rename the method (`magnitude()`), keeping `length()` as a deprecated alias
|
|
403
|
+
on a differently-named prototype slot is not possible — the name is the problem. If the
|
|
404
|
+
method name must be preserved for compatibility, stop extending `Float64Array` and hold
|
|
405
|
+
the buffer as a field instead.
|
|
406
|
+
- **Confidence**: Confirmed (executed).
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
### GEOM2-14 `AABB2.computeArea()` on the canonical empty box returns `+Infinity`
|
|
411
|
+
- **Severity**: MINOR
|
|
412
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/2d/aabb/AABB2.js:392-399, 405-415, 360-386, 675-677`
|
|
413
|
+
- **Defect**: `setNegativelyInfiniteBounds()` is the package's "empty box" representation
|
|
414
|
+
(`x0=y0=+∞, x1=y1=−∞`) and is used as an accumulator seed in ~15 call sites. None of the
|
|
415
|
+
size/cost accessors handle it: `aabb2_area(∞,∞,−∞,−∞)` computes
|
|
416
|
+
`(−∞ − ∞) * (−∞ − ∞) = (−∞)·(−∞) = +∞`.
|
|
417
|
+
- **Failure scenario**: `const b = new AABB2(); b.setNegativelyInfiniteBounds();`
|
|
418
|
+
- `b.computeArea()` → `Infinity`; expected `0` (an empty box encloses nothing).
|
|
419
|
+
- `b.computeSurfaceArea()` → `-Infinity`; expected `0`.
|
|
420
|
+
- `b.costForInclusion(new AABB2(0,0,1,1))` → `-Infinity`; expected a finite
|
|
421
|
+
non-negative growth cost. A BVH/packing insertion loop ranking candidates by
|
|
422
|
+
`costForInclusion` will always pick a not-yet-initialised (empty) node, because
|
|
423
|
+
`-Infinity` beats every real candidate.
|
|
424
|
+
This is reachable whenever an accumulator loop turns out to be empty — e.g.
|
|
425
|
+
`entity_node_compute_bounding_box.js:37` or `View.js:505` seed an `AABB2` this way and
|
|
426
|
+
then only `_expandToFit` inside a loop over children.
|
|
427
|
+
- **Fix**: give the three accessors an emptiness early-out — `if (this.x1 < this.x0 ||
|
|
428
|
+
this.y1 < this.y0) return 0;` in `computeArea`/`computeSurfaceArea`, and make
|
|
429
|
+
`_costForInclusion` fall back to the area of the *other* box when `this` is empty.
|
|
430
|
+
Note `_costForInclusion` also drops the second-order term of the true area growth
|
|
431
|
+
(`x*dy + y*dx`, missing `x*y`), so `AABB2(0,0,1,1).costForInclusion(AABB2(2,2,3,3))`
|
|
432
|
+
returns `4` where the actual area increase is `8`; worth deciding whether the function
|
|
433
|
+
is meant to be an exact growth or a first-order heuristic, and documenting it.
|
|
434
|
+
- **Confidence**: Confirmed (executed).
|