@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.
Files changed (295) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/package.json +1 -1
  4. package/src/REVIEW_2026_08_06/adv-core.md +511 -0
  5. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
  6. package/src/REVIEW_2026_08_06/adv-view.md +446 -0
  7. package/src/REVIEW_2026_08_06/api-core.md +296 -0
  8. package/src/REVIEW_2026_08_06/api-engine.md +195 -0
  9. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
  10. package/src/REVIEW_2026_08_06/core-binary.md +348 -0
  11. package/src/REVIEW_2026_08_06/core-collection.md +376 -0
  12. package/src/REVIEW_2026_08_06/core-math.md +221 -0
  13. package/src/REVIEW_2026_08_06/core-process.md +266 -0
  14. package/src/REVIEW_2026_08_06/core-science.md +223 -0
  15. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
  16. package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
  17. package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
  18. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
  19. package/src/REVIEW_2026_08_06/engine-input.md +197 -0
  20. package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
  21. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
  22. package/src/REVIEW_2026_08_06/generation.md +176 -0
  23. package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
  24. package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
  25. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
  26. package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
  27. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
  28. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
  29. package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
  30. package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
  31. package/src/REVIEW_2026_08_06/view.md +667 -0
  32. package/src/REVIEW_2026_08_06.md +571 -0
  33. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  34. package/src/core/binary/BinaryBuffer.js +12 -1
  35. package/src/core/binary/BitImage2.d.ts +5 -1
  36. package/src/core/binary/BitImage2.d.ts.map +1 -1
  37. package/src/core/binary/BitImage2.js +5 -1
  38. package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
  39. package/src/core/bvh8/build/NodeProxy.js +314 -308
  40. package/src/core/collection/list/List.d.ts +15 -0
  41. package/src/core/collection/list/List.d.ts.map +1 -1
  42. package/src/core/collection/list/List.js +34 -0
  43. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  44. package/src/core/collection/list/SortedListProjection.js +4 -1
  45. package/src/core/collection/map/BiMap.d.ts +25 -2
  46. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  47. package/src/core/collection/map/BiMap.js +57 -3
  48. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  49. package/src/core/debug/matchers/IsAnything.js +5 -1
  50. package/src/core/events/signal/Signal.d.ts.map +1 -1
  51. package/src/core/events/signal/Signal.js +3 -1
  52. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
  53. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
  54. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
  55. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
  56. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
  57. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
  58. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
  59. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
  60. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
  61. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
  62. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
  63. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
  64. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
  65. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
  66. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
  67. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
  68. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
  69. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  70. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
  71. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
  72. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
  73. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
  74. package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
  75. package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
  76. package/src/core/geom/2d/v2_morton_encode.js +23 -0
  77. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  78. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
  81. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
  82. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
  83. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
  84. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
  85. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
  86. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
  87. package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
  88. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
  90. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
  91. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
  92. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
  93. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
  94. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
  95. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
  96. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
  97. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
  98. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
  99. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
  100. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
  101. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
  102. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
  103. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
  104. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
  105. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
  106. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
  108. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
  109. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
  110. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
  111. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
  112. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
  113. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
  114. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
  115. package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
  116. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
  117. package/src/core/geom/vec3/v3_cotangent.js +6 -2
  118. package/src/core/geom/vec3/v3_cross.d.ts +6 -2
  119. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
  120. package/src/core/geom/vec3/v3_cross.js +6 -2
  121. package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
  122. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
  123. package/src/core/geom/vec3/v3_multiply.js +6 -2
  124. package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
  125. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_subtract.js +6 -2
  127. package/src/core/localization/Localization.js +1 -1
  128. package/src/core/math/random/randomSeed.d.ts +13 -0
  129. package/src/core/math/random/randomSeed.d.ts.map +1 -0
  130. package/src/core/math/random/randomSeed.js +16 -0
  131. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
  132. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
  133. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
  134. package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
  135. package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
  136. package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
  137. package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
  138. package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
  139. package/src/core/math/statistics/hammersley_sequence.js +25 -16
  140. package/src/core/process/Future.d.ts.map +1 -1
  141. package/src/core/process/Future.js +12 -0
  142. package/src/core/process/PromiseWatcher.d.ts.map +1 -1
  143. package/src/core/process/PromiseWatcher.js +9 -2
  144. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  145. package/src/core/process/executor/ConcurrentExecutor.js +12 -2
  146. package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
  147. package/src/core/process/worker/WorkerProxy.js +5 -2
  148. package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
  149. package/src/core/process/worker/extractTransferables.js +4 -1
  150. package/src/engine/Engine.d.ts +12 -12
  151. package/src/engine/Engine.d.ts.map +1 -1
  152. package/src/engine/Engine.js +149 -10
  153. package/src/engine/EngineHarness.d.ts.map +1 -1
  154. package/src/engine/EngineHarness.js +4 -1
  155. package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
  156. package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
  157. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
  158. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
  159. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  160. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
  161. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
  162. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  163. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
  164. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
  165. package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
  166. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  167. package/src/engine/graphics/GraphicsEngine.js +196 -24
  168. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
  169. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
  170. package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
  171. package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
  172. package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
  173. package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
  174. package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
  175. package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
  176. package/src/engine/graphics/context/WebGLContextState.js +24 -0
  177. package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
  178. package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
  179. package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
  180. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
  181. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
  182. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
  183. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
  184. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
  185. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
  186. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  187. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
  188. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
  189. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  190. package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
  191. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
  192. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  193. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
  194. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
  195. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  196. package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
  197. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
  198. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  199. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
  200. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  201. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
  202. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  203. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
  204. package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
  205. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
  206. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
  207. package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
  208. package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
  209. package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
  210. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
  211. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
  212. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
  213. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  214. package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
  215. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
  216. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
  217. package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
  218. package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
  219. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
  220. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
  221. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
  222. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
  223. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
  224. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
  225. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
  226. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
  227. package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
  228. package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
  229. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
  230. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
  231. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
  232. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
  233. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
  234. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  235. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
  236. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  237. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  238. package/src/engine/physics/constraint/solve_constraints.js +38 -13
  239. package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
  240. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  241. package/src/engine/physics/contact/ManifoldStore.js +15 -3
  242. package/src/engine/physics/ecs/Joint.d.ts +7 -4
  243. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  244. package/src/engine/physics/ecs/Joint.js +7 -4
  245. package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
  246. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  247. package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
  248. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  249. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  250. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
  251. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  252. package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
  253. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
  254. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  255. package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
  256. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
  257. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  258. package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
  259. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  260. package/src/engine/physics/queries/raycast.js +11 -4
  261. package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
  262. package/src/engine/save/storage/IndexedDBStorage.js +21 -3
  263. package/src/engine/simulation/Ticker.d.ts +12 -0
  264. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  265. package/src/engine/simulation/Ticker.js +18 -0
  266. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  267. package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
  268. package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
  269. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  270. package/src/engine/sound/simulation/core/VolumeField.js +6 -2
  271. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
  272. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  273. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
  274. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
  275. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
  276. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
  277. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
  278. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
  279. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
  280. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  281. package/src/engine/ui/GUIEngine.js +8 -1
  282. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  283. package/src/generation/theme/TerrainTheme.js +2 -1
  284. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  285. package/src/generation/theme/ThemeEngine.js +6 -5
  286. package/src/view/ViewGroup.d.ts.map +1 -1
  287. package/src/view/ViewGroup.js +9 -7
  288. package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
  289. package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
  290. package/src/view/graphics/WebGLContextFailureView.js +76 -0
  291. package/src/view/minimap/Minimap.d.ts +8 -0
  292. package/src/view/minimap/Minimap.d.ts.map +1 -1
  293. package/src/view/minimap/Minimap.js +16 -3
  294. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  295. 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).