@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,117 @@
1
+ # Correctness review — `core/geom/3d/`
2
+
3
+ Scope: everything under `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/`.
4
+ Excluded per brief: `core/geom/2d/`, loose files in `core/geom/`, `editor/`, `prototype*.js`,
5
+ `*.spec.js` (evidence only), and the already-fixed/known-good items listed in the brief.
6
+
7
+ ## Tree survey (what was covered)
8
+
9
+ ~800 `.js` files. Roughly half are thin `@deprecated` re-export shims for the camelCase→snake_case
10
+ rename; those were checked and dismissed.
11
+
12
+ Directories read in depth (priority order from the brief):
13
+
14
+ | area | dir | files w/o adjacent spec (sampled/read) |
15
+ |---|---|---|
16
+ | half-edge / topology | `topology/**` (233 files) | `bt_loop_kill`, `bt_mesh_fill_small_holes`, `bt_mesh_from_unindexed_geometry`, `edge/bt_edge_get_or_create`, `edge/bt_edge_kill`, `edge/bt_edge_swap_vertex_slots`, `edge/bt_kill_only_edge`, `face/bt_face_kill`, `face/bt_kill_only_face`, `vertex/bt_vertex_kill`, `vertex/bt_vertex_kill_only`, `query/bt_collect_boundary_segments`, `query/bt_edge_is_boundary`, `query/bt_face_get_edge_adjacent_faces`, `query/bt_face_get_shared_loop`, `query/bt_face_get_vertex_adjacent_faces`, `query/bt_mesh_count_edge_faces`, `query/bt_mesh_is_closed`, `query/bt_mesh_is_manifold`, `query/bt_mesh_build_boundary_euclidean_distance_field`, all of `topology/simplify/**` except 4, all of `topology/query/**`, `topology/three/**`, `topology/util/**` |
17
+ | UV atlas / LSCM / packing | `atlas/**` (39 files) | `atlas_generate`, `atlas_extract_chart`, `Chart`, `atlas_write_chart_uvs`(spec'd), `data/atlas_build_edge_dihedrals`, `data/atlas_build_edge_lengths`, `data/atlas_build_face_areas`, `param/atlas_chart_param_validity`, `param/atlas_chart_parameterize_ortho`, `param/atlas_chart_stretch_metric`, `pack/ChartPlacement`, `segment/RegionGraph`, `segment/atlas_build_region_graph` |
18
+ | triangulation | `polygon/**`, `topology/.../face/bt_face_triangulate`, `bt_mesh_triangulate` | `polygon3_compute_normal`, `bt_mesh_triangulate` |
19
+ | convex hull | no 3D hull constructor exists in this tree — only `shape/ConvexHullShape3D` (already-fixed item) | — |
20
+ | mesh queries / sampling | `triangle/**`, `query/bt_mesh_sample_interior_grid_points`, `shape/util/**` | `tri3_area`, `tri3_compute_normal`, `tri3_compute_plane_side`, `tri3_incenter`, `tri3_intersects_clipping_volume`, `tri3_mesh_compute_signed_volume`, `tri3_ray_intersection_barycentric_edge`, `tri3_ray_intersection_indexed`, `shape/util/shape3_voxelize_to_grid`, `shape/util/compute_signed_distance_gradient_by_sampling` |
21
+ | simplification / welding | `topology/simplify/**`, `io/vertex/bt_merge_vertices_by_distance`, `io/edge/bt_mesh_fuse_duplicate_edges` | `compute_edge_collapse_cost`, `compute_face_normal_change_dot_product`, `build_vertex_quadrics`, `apply_border_penalty_to_vertex_quadrics`, `compute_edge_collapse_cost_quadric`, `deep_copy_vertex_quadrics`, `partial_edge_collapse`, `collapse_edge`, `edge_collapse_quadratic`, `EdgeCollapseCandidate`, `tm_edge_collapse_is_degenerate_flip`, `simplifyTopoMesh`, `simplifyTopoMesh2` |
22
+ | GJK / MPR | `gjk/**` | `minkowski_support` (mpr, gjk, gjk_epa_penetration have specs) |
23
+ | encodings / misc | `normal/**`, `morton/**`, `equirectangular/**`, `SurfacePoint3`, `tetrahedra/delaunay/**` | spot-checked |
24
+
25
+ Checked and found clean (no finding): `bt_edge_split` (radial + disk rewiring verified end-to-end,
26
+ both halves get one loop per original face), `bt_edge_create`/`bt_disk_edge_add`/`bt_disk_edge_remove`
27
+ self-loop convention, `bt_mesh_compact` (remap/patch/compact/truncate ordering), `bt_face_poke`
28
+ fan wiring, `bt_face_triangulate` ear replay (`_ring_prev` ↔ `loop_read_prev` correspondence holds
29
+ for both the forward and backward ear orderings, and the emitted mesh winding matches the source
30
+ face), `bt_mesh_walk_boundary_loops` + `order_cycle_from` direction seeding (the `edges_at_v[0]`
31
+ tiebreak provably resolves to `cycle[0]`, so the new hole face winds opposite the existing loop),
32
+ `polygon3_triangulate` axis-drop table (all six ±x/±y/±z cases preserve CCW-from-+normal),
33
+ `polygon3_compute_normal`, `tri3_closest_point_barycentric` (all seven Voronoi regions and the
34
+ collinear-degenerate paths traced numerically), `mpr.js` portal discovery (both plane tests
35
+ algebraically identical to libccd's `det[v0,v1,v3] < 0` / `det[v0,v2,v3] > 0` after expanding the
36
+ relative-to-V0 form) and `expandPortal` branch table, `matrix_symmetric_3x3_eigen` column/ascending
37
+ convention used by `fit_plane_basis`, `min_area_rect_2d` angle convention vs
38
+ `rotate_uv_to_min_area_box`, `atlas_pack_charts` rot-1 UV remap (transpose ↔ `(x+tv, y+tu)`
39
+ verified), the skyline/bitmap growth loops, `atlas_segment_charts` vs `atlas_segment_charts_atomic`
40
+ boundary/seam arithmetic (both reduce to `chart_boundary + perimeter − 2·shared`),
41
+ `encode_unit_to_octahedron`/`decode_octahedron_to_unit` fold (pre-fold abs values used on both
42
+ sides), `topo_mesh_to_three_buffer_geometry` index-width thresholds (no Uint16 overflow),
43
+ `Quadric3.optimize` aliasing and symmetric-inverse index layout, `v3_normalize_array` zero guard.
44
+
45
+ ---
46
+
47
+ ## Findings
48
+
49
+ ### GEOM3-1 Edge-collapse flip rejection degenerates into "is the face normal pointing down?"
50
+ - **Severity**: CRITICAL
51
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/topology/simplify/compute_edge_collapse_cost.js:43-54` (with `compute_face_normal_change_dot_product.js:29-47` and `tri3_compute_normal.js:40-46`)
52
+ - **Defect**: The flip test loops over **every** face of the victim, including the 1–2 faces that contain *both* endpoints of the collapsing edge — the faces the collapse deletes. Substituting `victim → target` in those faces produces a triangle with two identical vertices, so `tri3_compute_normal` takes its zero-area branch and writes the documented "arbitrary invalid result" `(0, 1, 0)`. The dot product against the real face normal is therefore just `original_normal.y`, and the whole test collapses to `original_normal.y >= 0`. The sibling helper `tm_edge_collapse_is_degenerate_flip.js:26-29` skips exactly these faces, confirming the intent.
53
+ - **Failure scenario**: A `TopoMesh` quad in the XZ plane facing **down** — A(0,0,0), B(1,0,0), C(1,0,1), D(0,0,1), triangles (A,B,C) and (A,C,D) wound so both normals are `(0,-1,0)`. Call `compute_edge_collapse_cost(A, C, edge_AC, quadrics)`. Face ABC contains both A and C, so substitution gives (C,B,C): `vCB = C−B`, `vAB = C−B`, cross `= 0`, `l_sqr === 0` → normal `(0,1,0)`. `dot((0,1,0), (0,-1,0)) = -1 < 0` → **produced `Number.MAX_VALUE`**; expected the ordinary quadric cost (0 here, plus the border constants). The reverse direction `compute_edge_collapse_cost(C, A, …)` hits the same faces and also returns `MAX_VALUE`, so `EdgeCollapseCandidate.update` (EdgeCollapseCandidate.js:117-140) records `MAX_VALUE` for the edge in both orientations. Flip the same quad to face up (normals `(0,1,0)`) and the dot becomes `+1` and the edge is accepted. Net effect on a real model: `simplifyTopoMesh` refuses to collapse any edge whose adjacent face normal has a negative Y component — on a closed mesh (cube, sphere, rock) that is the entire downward-facing half, which is simply never decimated while the upward-facing half is.
54
+ - **Fix**: In the loop at `compute_edge_collapse_cost.js:43`, skip faces that contain `target` as well as `victim` (they are deleted by the collapse), mirroring `tm_edge_collapse_is_degenerate_flip.js:26-29`:
55
+ ```js
56
+ if (face.containsVertex(target)) { continue; }
57
+ ```
58
+ - **Confidence**: Confirmed
59
+
60
+ ### GEOM3-2 Zero-area faces inject a fabricated `(0,1,0)` plane into vertex quadrics
61
+ - **Severity**: MAJOR
62
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js:70-91`; same defect in `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/topology/simplify/quadratic/build_vertex_quadrics.js:12-30`
63
+ - **Defect**: Both quadric builders derive the supporting plane from `tri3_compute_normal`, which for a zero-area triangle writes the sentinel `(0, 1, 0)` (`tri3_compute_normal.js:40-46`, "degenerate triangle, provide arbitrary invalid result"). Neither builder tests for that case, so the sentinel is treated as a genuine plane and accumulated onto all three vertices at full weight. A degenerate face carries no geometric constraint and must contribute a zero quadric. (`build_vertex_quadrics` would be saved by area weighting, but `use_triangle_area` defaults to `false`; `bt_mesh_compute_vertex_quadrics` has no area weighting at all.)
64
+ - **Failure scenario**: A sheet lying in the plane `z = 0` that also contains one collinear triangle `T = (0,0,0), (1,0,0), (2,0,0)` — a routine artifact after `bt_merge_vertices_by_distance` welds colocal points, or from an imported model. For `T`: `vCB = (1,0,0)`, `vAB = (-1,0,0)`, cross `= 0` → normal `(0,1,0)`, `d = -dot((0,1,0),(0,0,0)) = 0`, i.e. the plane `y = 0`, added to the quadrics of `(0,0,0)`, `(1,0,0)`, `(2,0,0)`. Now score the collapse of edge `B=(1,0,0) — E=(1,1,0)` at target `(1, 0.5, 0)` (`evaluate_edge_collapse`, `bt_mesh_simplify.js:110-115`): every real supporting plane is `z = 0` and the target lies in it, so the true cost is **0**; the fabricated plane contributes `0.5² = 0.25`, so the **produced cost is 0.25**. The bogus plane also raises the rank of the combined tensor, so `Quadric3.optimize` succeeds where it should have reported singular and pulls the survivor off the sheet toward `y = 0`.
65
+ - **Fix**: Detect the degeneracy where the plane is built rather than relying on `tri3_compute_normal`'s sentinel — compute the raw cross product and `continue` when its squared length is 0 (`bt_mesh_compute_vertex_quadrics.js` before line 87; `quadric_set_from_face` in `build_vertex_quadrics.js` before line 29).
66
+ - **Confidence**: Confirmed
67
+
68
+ ### GEOM3-3 `bt_vertex_replace` silently rewrites edge 0 when a vertex has more than 1024 incident edges
69
+ - **Severity**: MAJOR
70
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.js:6,40-42,57-58`
71
+ - **Defect**: The victim's disk cycle is snapshotted into the module-static `const scratch_edges = new Uint32Array(1024)`. The bound is only checked by an `assert` (compiled out in production). Past 1024, `scratch_edges[edges_to_move_count++] = curr_e` is a silently-dropped out-of-range typed-array store while the counter keeps incrementing, so stage 3 reads `scratch_edges[i] === undefined` for `i >= 1024`. That `undefined` then flows into `mesh.edge_read_loop(undefined)` → `element_address(undefined)` = `28 * undefined` = `NaN` → `data_view.getUint32(NaN)`, and `DataView` coerces the index with `ToIndex`, which maps `NaN` to **0**. So every over-budget iteration reads *and rewrites* **edge 0**.
72
+ - **Failure scenario**: Take a vertex `v` with 1200 incident edges and call `bt_vertex_replace(mesh, v, r)` (directly, or via `bt_edge_collapse` → `bt_mesh_simplify`; survivor valence grows monotonically with each collapse, so decimating a ≥200k-triangle mesh down to a few hundred faces reaches this routinely). Produced: edges 0..1023 are correctly re-pointed; for the remaining 176 iterations the code instead sets `edge 0`'s `vertex1`/`vertex2` to `r` and splices edge 0 into `r`'s disk cycle repeatedly, while edges 1024..1199 keep pointing at `v` — which `bt_edge_collapse` then releases via `bt_vertex_kill`, leaving 176 edges referencing a freed vertex. Expected: all 1200 edges re-pointed and edge 0 untouched.
73
+ - **Fix**: Grow `scratch_edges` on demand before the write (`if (edges_to_move_count >= scratch_edges.length) { scratch_edges = grow(...) }` with `let` instead of `const`), or count the disk cycle first and size the buffer to the valence.
74
+ - **Confidence**: Likely (the `NaN → index 0` mechanism is Confirmed; reaching valence > 1024 depends on the input mesh / decimation ratio)
75
+
76
+ ### GEOM3-4 `atlas_extract_chart` marks chart-interior edges as boundary on non-manifold edges
77
+ - **Severity**: MINOR
78
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/atlas/atlas_extract_chart.js:84-91`
79
+ - **Defect**: The boundary test consults only the single `loop_read_radial_next` neighbour: `is_boundary = opp === li || face_chart[loop_read_face(opp)] !== chart_id`. On an edge with three or more faces the answer depends on where the loop sits in the radial cycle, so an edge that is interior to the chart can be recorded as a chart boundary. The correct predicate is "no *other* loop on the radial cycle belongs to this chart".
80
+ - **Failure scenario**: Edge `e` shared by faces A, B (both in chart 0) and C (chart 1), radial cycle A→B→C→A. For A's loop, `radial_next` is B (chart 0) → not boundary. For B's loop, `radial_next` is C (chart 1) → **boundary**, so `chart.boundary_edges` gains the pair `(vB0, vB1)` for an edge that is interior to chart 0. Downstream: the extra edge gives one of its endpoints two outgoing boundary edges, so `atlas_chart_parameterize_bff` hits `if (succ[a] !== -1) return false` (`atlas_chart_parameterize_bff.js:102`) and rejects a chart that really is a disk; and `atlas_chart_param_validity` tests that interior segment for crossings against the true outline, so a bijective LSCM layout can be reported `self_intersects: true` and be needlessly exploded by `atlas_piecewise_resegment`. Expected: `boundary_edges` contains only edges with no in-chart face across them.
81
+ - **Fix**: Walk the full radial cycle from `li` and treat the edge as boundary only if no other loop's face has `face_chart === chart_id`.
82
+ - **Confidence**: Confirmed (for non-manifold input)
83
+
84
+ ### GEOM3-5 `build_triangle_adjacency` keeps the *last* pairing, not the first, on a non-manifold edge
85
+ - **Severity**: MINOR
86
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/atlas/param/atlas_piecewise_resegment.js:26-33`
87
+ - **Defect**: `edge_map` retains the first slot forever, so the third and later triangles on an edge each overwrite `tri_neighbor[first_slot]`. The result is an asymmetric adjacency graph, contradicting the function's own doc ("a non-manifold edge keeps the first pairing").
88
+ - **Failure scenario**: Triangles `t1, t2, t3` share an edge, visited in that order. After `t2`: `tri_neighbor[slot1] = t2`, `tri_neighbor[slot2] = t1`. After `t3`: `tri_neighbor[slot3] = t1` and `tri_neighbor[slot1] = t3` — overwriting `t2`. Now grow a sub-chart containing `{t1, t2}`: `build_subchart` reads `tri_neighbor[slot1] = t3`, which is not in `member`, so it pushes that edge into `sub.boundary_edges` even though `t2` sits across it. Produced: `is_ortho_valid` runs the O(b²) crossing test on an interior edge and can reject a legitimate growth step, splitting the chart into more pieces than necessary; and `t2 → t1` while `t1 → t3` makes the traversal non-symmetric. Expected: `tri_neighbor[slot1] === t2` and `tri_neighbor[slot2] === t1`, with `t3`'s slot left at `-1`.
89
+ - **Fix**: `edge_map.delete(key)` after the first pairing is consumed, so a third triangle on the same edge starts a fresh (or no) pairing instead of clobbering `slot1`.
90
+ - **Confidence**: Confirmed (for non-manifold input)
91
+
92
+ ### GEOM3-6 Mesh edits allocate through the raw pools, bypassing the data-pool capacity sync
93
+ - **Severity**: MINOR
94
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/topology/struct/binary/io/face/bt_face_poke.js:53,83`; also `edge/bt_edge_split.js:73,146`, `face/bt_face_triangulate.js:61`, `bt_mesh_close_boundary_holes.js:112`, `bt_mesh_fill_small_holes.js:80`, `bt_mesh_face_decouple.js:85`, `edge/bt_edge_create.js:71`
95
+ - **Defect**: These call `mesh.vertices.allocate()` / `mesh.faces.allocate()` / `mesh.edges.allocate()` directly instead of `mesh.vertex_create()` / `face_create()` / `edge_create()`. The `*_create()` wrappers exist precisely to run `__*_data_pool?.ensure_capacity(id + 1)` (`BinaryTopology.js:635-680`). The same functions *do* use `mesh.loop_create()` for their loops, so the omission is inconsistent within a single function body — this is not the documented "caller keeps `__size` in sync" contract, it is a missing capacity grow that leaves the data buffer physically too short.
96
+ - **Failure scenario**: `topo.create_face_data(4)` on a 100-face mesh → the data pool is sized by `allocate_continuous(100)`, giving `capacity = 128` and a 512-byte buffer (128 words). Call `bt_face_poke` on a 40-gon: it allocates 39 new faces, ids 100…138. `face_data.element_word(138)` is 138, but `face_data.data_uint32.length` is 128. Produced: writes at index 138 are silently discarded and reads return `undefined` (and `NaN` once arithmetic touches them). Expected: the data pool grows so face 138 is addressable, exactly as `mesh.loop_create()` already guarantees for the loops created in the same call.
97
+ - **Fix**: Use `mesh.vertex_create()` / `mesh.face_create()` / `mesh.edge_create()` in these seven sites.
98
+ - **Confidence**: Confirmed
99
+
100
+ ### GEOM3-7 `atlas_generate` reports a `chart_count` that undercounts the charts it returns
101
+ - **Severity**: MINOR
102
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/core/geom/3d/atlas/atlas_generate.js:36-38` (value originates at `atlas_compute_charts.js:187`)
103
+ - **Defect**: `atlas_compute_charts` returns the *segmentation* chart count alongside `face_chart` — self-consistent there, since `face_chart` holds segmentation ids. `atlas_generate` forwards that number as `chart_count` but does **not** return `face_chart`, so the only array the caller has is `charts`, whose length differs whenever `atlas_piecewise_resegment` fires (each failed chart becomes N sub-charts, all appended).
104
+ - **Failure scenario**: A mesh whose segmentation produces 4 charts, one of which fails ortho, LSCM and BFF and is re-segmented into 7 pieces. `atlas_generate` returns `charts.length === 10` but `chart_count === 4`. A caller sizing a per-chart array from `chart_count` (the obvious reading of the field) under-allocates by 6 and drops six charts' metadata. Expected: `chart_count === charts.length` for the value returned next to `charts`.
105
+ - **Fix**: In `atlas_generate`, return `chart_count: computed.charts.length` (or return `face_chart` too and document the field as the segmentation count).
106
+ - **Confidence**: Confirmed
107
+
108
+ ---
109
+
110
+ ## Notable non-findings (checked, deliberately not reported)
111
+
112
+ - `tri3_ray_intersection_barycentric_edge` returns `true` for negative `t` (hit behind the origin). Both in-tree BVH callers (`ebvh_geometry_query_nearest_triangle_ray.js:127`, `ebvh_geometry_query_any_triangle_ray.js:129`) and `BufferedGeometryBVH` gate on `t > min_distance`, and the function's contract is "returns the barycentric solution", so this is by design, not a defect.
113
+ - `tri3_ray_intersection` writes an **un-normalised** normal into `result.normal`. Its only in-tree consumer (`bt_mesh_sample_interior_grid_points`) uses the boolean only. Contract ambiguity, no wrong value.
114
+ - `bt_face_is_degenerate` uses an absolute `area² <= 1e-17` threshold, so a valid triangle with ~3e-5 edges is called degenerate. Scale-dependent tolerance rather than a logic error.
115
+ - `compute_edge_collapse_cost.js:29` declares `normal_cost` and never assigns it, so the curvature term of the Melax cost is dead. Incomplete, but it produces no wrong value on its own (the quadric term carries the geometric error) — GEOM3-1 is the actual defect in that file.
116
+ - `BinaryElementPool.copy` leaves `__capacity` stale when `item_size` shrinks and the existing buffer is reused (`BinaryElementPool.js:471-489`). Self-heals on the next `allocate()`; no reachable wrong value found.
117
+ - `tetrahedral_mesh_compute_cavity`'s facet-vertex table (`k0/k1/k2`) yields the correct vertex *sets* for all four facets but the opposite *orientation* to the table in `tetrahedral_mesh_walk_towards_containing_tetrahedron`. The flip is uniform across all four facets, so it is self-consistent; without running `compute_delaunay_tetrahedral_mesh.spec.js` against the mesh's sign convention I could not construct a produced-vs-expected, so it is not reported — but it is the one place in this tree I would re-check first.
@@ -0,0 +1,143 @@
1
+ # Correctness review — `engine/graphics/` (excluding ecs/, render/, texture/, particular/, geometry/)
2
+
3
+ three.js r136. Verified against `H:/git/moh/node_modules/three/src/renderers/**` where three semantics were load-bearing.
4
+
5
+ ---
6
+
7
+ ### GFXG-1 LPV GI vertex injection reads `worldPosition`, which three only declares conditionally
8
+ - **Severity**: CRITICAL
9
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/sh3/gi/material/space_fragment/build_vertex_shader.js:21` (also `.../sh3/gi/material/space_vertex/build_vertex_shader.js:12`, `:14`, `:23`)
10
+ - **Defect**: Both LPV vertex-shader builders insert code that reads `worldPosition` after `#include <fog_vertex>`. In three r136 `worldpos_vertex.glsl.js` declares `vec4 worldPosition` only inside `#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION )`. With none of those defines the identifier does not exist.
11
+ - **Failure scenario**: A `MeshStandardMaterial` mesh (no envMap, no transmission) in a scene whose only lights are ambient + a non-shadow-casting directional light, registered with `graphics/sh3/gi/material/MaterialTransformer`. `USE_SHADOWMAP` is not defined, so the injected `vWorldPosition = worldPosition.xyz;` (fragment-space variant) / `lpv_guess_initial_tet( worldPosition.xyz )` (vertex-space variant) hits `'worldPosition' : undeclared identifier`. three logs a shader-compile error and the mesh renders with the fallback program — i.e. the object disappears/renders black. Turning shadows on for one light makes it compile again, which is exactly the kind of "works on my scene" trap this hides behind.
12
+ - **Fix**: Do what the newer LPG builders already do (`sh3/lpg/material/space_fragment/build_vertex_shader.js:20-32` documents this exact hazard): compute the world position locally from the `position` attribute —
13
+ ```glsl
14
+ vec4 lpv_world_position = vec4( position, 1.0 );
15
+ #ifdef USE_INSTANCING
16
+ lpv_world_position = instanceMatrix * lpv_world_position;
17
+ #endif
18
+ lpv_world_position = modelMatrix * lpv_world_position;
19
+ ```
20
+ and use that instead of `worldPosition`.
21
+ - **Confidence**: Confirmed
22
+
23
+ ---
24
+
25
+ ### GFXG-2 Impostor bake leaves `tNormal` unbound when the source material has no normal map
26
+ - **Severity**: CRITICAL
27
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/impostors/octahedral/bake/prepare_bake_material.js:100`
28
+ - **Defect**: Every other texture slot gets a 1×1 fallback when the source material lacks it (`tDiffuse` white, `tRoughness` white, `tMetalness` black, `tOcclusion` white, `tEmissive` black), but `texture_normal` is left `null` and assigned straight through. A `null` `sampler2D` uniform binds three's default empty texture, which `WebGLState.createTexture` uploads from `new Uint8Array(4)` — i.e. **black**, not (0.5, 0.5, 1).
29
+ - **Failure scenario**: Bake an octahedral impostor of any `MeshStandardMaterial` without a `normalMap` — the common case for props. In `BakeShaderStandard` the fragment does `normal_sample = texture(tNormal, uv).rgb * 2.0 - 1.0` → `(-1,-1,-1)`, then `normal = normalize(TBN * (-1,-1,-1))` = `normalize(-T - B - N)`. The `gNormal` atlas channel therefore stores a normal ~125° away from the true surface normal, pointing *into* the surface. `ImpostorShaderLitV0` and `ImpostorShaderNormalsV0` then shade the impostor with that normal: a tree impostor lit from above renders dark/inverted, and the whole card shades as if lit from behind.
30
+ - **Fix**: Add the missing fallback next to the others, e.g.
31
+ ```js
32
+ if (texture_normal === null) {
33
+ texture_normal = makeOnePixelTexture([128, 128, 255, 255], cleanup_signal);
34
+ }
35
+ ```
36
+ (before the anisotropy loop, so the `null` skip in that loop becomes unnecessary).
37
+ - **Confidence**: Confirmed
38
+
39
+ ---
40
+
41
+ ### GFXG-3 `TerrainShader` uses the pre-r135 fog varying name `fogDepth`
42
+ - **Severity**: MAJOR
43
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/shaders/TerrainShader.js:361` and `:363`
44
+ - **Defect**: The hand-rolled fog block reads `fogDepth`. three r136 renamed the varying to `vFogDepth` (`fog_pars_fragment.glsl.js:5`, `fog_vertex.glsl.js:4`); `fogDepth` is declared nowhere. The block is guarded by `#ifdef USE_FOG`, so it only bites when fog is actually enabled.
45
+ - **Failure scenario**: `scene.fog = new Fog(0x808080, 10, 200)` on a scene containing terrain. `USE_FOG` becomes defined (ShaderMaterial's `fog` defaults to `true` and `SplatMaterial` does not turn it off), the fragment shader fails to compile with `'fogDepth' : undeclared identifier`, and the entire terrain stops rendering. Removing the fog makes it work again.
46
+ - **Fix**: Rename both uses to `vFogDepth`. Better: replace the whole hand-copied block with `#include <fog_fragment>` (it is byte-identical apart from the name) so the next three bump cannot desync it again.
47
+ - **Confidence**: Confirmed
48
+
49
+ ---
50
+
51
+ ### GFXG-4 `MaterialManager` de-duplicates `ShaderMaterial`s without comparing shader source, defines or uniforms
52
+ - **Severity**: MAJOR
53
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/material/manager/MaterialManager.js:181` (`__library` lookup; hash/equality supplied at `:14-18`)
54
+ - **Defect**: `__library` is a `HashMap` keyed with `computeMaterialHash` / `computeMaterialEquality`. For `isShaderMaterial` those two only fold in the generic `Material` flags plus `material.lights` — `vertexShader`, `fragmentShader`, `defines` and `uniforms` are never inspected. Two structurally different custom shader materials with matching blend/side/depth/transparent flags therefore compare *equal*, and `obtain()` hands the second caller the first material's managed instance.
55
+ - **Failure scenario**: Two `Trail3D` entities with `TubeXMaterialSpec` `{diffuse: "smoke.png", lit:false, depthWrite:false}` and `{diffuse: "spark.png", lit:false, depthWrite:false}`. `TubeXPlugin` correctly builds two distinct `TubeMaterialX` (its own cache is keyed on the spec), then calls `graphics.getMaterialManager().obtain(material)` for each (`TubeXPlugin.js:84`). The second `obtain` hits the `__library` entry of the first — identical `type` `'ShaderMaterial'`, identical flags, both still carrying the default prototype `onBeforeCompile` — and returns the first material. Both trails render with `smoke.png`. Same collision hits `{diffuse: null}` vs `{diffuse: "x.png"}` (the second trail renders untextured, since `USE_TEXTURE` lives in `defines`), and any two `SplatMaterial`s (two terrains would share one splat/diffuse atlas set).
56
+ - **Fix**: Extend the key for shader materials. In `computeMaterialHash` / `computeMaterialEquality`, when `material.isShaderMaterial`, also fold in `vertexShader`, `fragmentShader` and the `defines` entries (a stable `JSON.stringify` of `defines` plus `computeStringHash` of the two sources is enough), and treat materials whose `uniforms` objects are not identity-equal as distinct.
57
+ - **Confidence**: Confirmed
58
+
59
+ ---
60
+
61
+ ### GFXG-5 SH probe visualiser uses the signed real-SH basis while every probe in the engine is projected with the unsigned one
62
+ - **Severity**: MAJOR
63
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/sh3/shader/visualize.frag.js:19`, `:21`, `:25`, `:27`
64
+ - **Defect**: `shGetIrradianceAt` in the visualiser subtracts bands 1 (`y`, `x`), 2·(`yz`), 2·(`xz`). Every projection path in the engine writes coefficients with `sh3_basis_at` (`core/geom/3d/sphere/harmonics/sh3_basis_at.js`), which is all-positive, and the two runtime GI evaluators (`sh3/gi/material/chunk_lpv_get_irradiance_at.js`, `sh3/lpg/material/chunk_lpg_get_irradiance_at.js`) and three's own `shGetIrradianceAt` (`lights_pars_begin.glsl.js:18-27`) are all-positive too. The visualiser is the only evaluator using the opposite sign convention.
65
+ - **Failure scenario**: Bake an LPV/LPG probe in a room lit by a single light directly overhead, then call `visualise_spherical_harmonic_sphere({ sh, position })`. Band 1 dominates; flipping its sign mirrors the directional lobe through the origin, so the debug sphere renders **bright on the bottom and dark on the top** — the exact opposite of the probe the renderer is actually using. Anyone using this to debug a GI bake gets sent the wrong way.
66
+ - **Fix**: Change the four `-=` back to `+=` so the visualiser matches `chunk_lpv_get_irradiance_at` / three. (Better still: emit `chunk_lpv_get_irradiance_at` here instead of a third hand-written copy of the same nine lines.)
67
+ - **Confidence**: Confirmed
68
+
69
+ ---
70
+
71
+ ### GFXG-6 Lit impostor materials break the Forward+ contract — no `#include <worldpos_vertex>` marker
72
+ - **Severity**: MAJOR
73
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/impostors/octahedral/shader/ImpostorShaderLitV0.js:225`; `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/impostors/voxel/shader/VoxelImpostorShaderLitV0.js:97`
74
+ - **Defect**: Both are custom **lit** (`lights: true`) `ShaderMaterial`s. Both declare `vec4 worldPosition = ...` inline instead of going through `#include <worldpos_vertex>`. `fp_build_vertex_shader` (`render/forward_plus/materials/fp_build_vertex_shader.js:20`) keys its only vertex-side injection on that exact literal and `insert_after` merely `console.warn`s and returns the source unchanged when the marker is missing. The fragment side still gets `varying vec3 v_world_position;` injected, so the varying exists but is never written.
75
+ - **Failure scenario**: Put a baked tree impostor at world (100, 0, 100) under Forward+ and drop a decal (blood splat, scorch mark) on it. `FP_SHADER_CHUNK_APPLY_DECALS` computes `decal_transform_matrix * vec4(v_world_position, 1.0)` with `v_world_position` never assigned (reads as 0), so the test is evaluated at the world origin: `max3(abs(local_position.xyz)) < 0.5` fails for every decal that is not at the origin, and the impostor never receives any decal. Conversely a decal placed near the origin uniformly floods the whole impostor card. Console shows `Term '#include <worldpos_vertex>' not found in the source`.
76
+ - **Fix**: In both vertex shaders, replace the inline `vec4 worldPosition = ...` with the marker, computing the intermediate through `transformed` so the chunk produces the right value:
77
+ ```glsl
78
+ vec3 transformed = pos_OS; // octahedral variant; centre_world basis for the voxel one
79
+ #include <worldpos_vertex>
80
+ #if !( defined( USE_ENVMAP ) || defined( DISTANCE ) || defined( USE_SHADOWMAP ) || defined( USE_TRANSMISSION ) )
81
+ vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );
82
+ #endif
83
+ ```
84
+ (the `#if` mirrors what `TerrainShader.js:66-68` already does).
85
+ - **Confidence**: Confirmed
86
+
87
+ ---
88
+
89
+ ### GFXG-7 Impostor bake builds the tangent with `tangent.w` as the homogeneous coordinate
90
+ - **Severity**: MAJOR
91
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/impostors/octahedral/shader/BakeShaderStandard.js:38`
92
+ - **Defect**: `vec3 T = normalize(vec3(modelViewMatrix * tangent));` — `tangent` is a `vec4` whose `w` is the glTF handedness sign (±1), not a homogeneous coordinate. Multiplying the full `vec4` by `modelViewMatrix` adds `w ×` the model-view **translation** into the tangent. The default attribute value set at `:192` is `[1, 0, 0, 1]`, so `w = 1` even for meshes with no tangents.
93
+ - **Failure scenario**: Bake an impostor of a mesh that *does* have a normal map. The bake camera sits `distance` units from the bounding-sphere centre (`ImpostorBaker.js:216-222`), so `modelViewMatrix[3].xyz ≈ (0, 0, -distance)`. For a 2 m prop, `T = normalize((tangent.xyz) + (0,0,-2))` ≈ `(0,0,-1)`, i.e. almost exactly the view direction and therefore almost anti-parallel to the surface normal `N` for front-facing texels. `B = cross(N, T)` collapses toward zero and `normalize(TBN * normal_sample)` produces a near-random perturbed normal. The impostor's `gNormal` channel is wrong across the whole silhouette, and the lit impostor shades with visible per-frame normal popping as the camera orbits. With a handedness of `w = -1` (mirrored UV islands) the tangent additionally points the wrong way.
94
+ - **Fix**: `vec3 T = normalize( mat3(modelViewMatrix) * tangent.xyz );` and apply the handedness to the bitangent: `vec3 B = cross(N, T) * tangent.w;`. Set `defaultAttributeValues.tangent = [1, 0, 0, 1]` unchanged — with `.xyz` extraction it now means "object-space +X, right-handed", which is a sane default.
95
+ - **Confidence**: Confirmed
96
+
97
+ ---
98
+
99
+ ### GFXG-8 `DenoiseShader` never passes `filterSmoothness` to the shader — the bilateral term is inert
100
+ - **Severity**: MAJOR
101
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/shaders/DenoiseShader.js:96-98`
102
+ - **Defect**: The shader reads the spatial sigma from `sigmas.x` and the **range** sigma (`bSigma`, the edge-preserving term) from `sigmas.y` (`:31`, `:34`). JS initialises the uniform as `new Vector2(sigma, sigma)`, so `bSigma` gets the spatial sigma. The `filterSmoothness` argument only survives into `bZnorm` (`:71`), which is a constant factor that cancels out against `accumulatedFactor` in the final `accumulatedColor / accumulatedFactor` normalisation. `filterSmoothness` therefore has literally no effect on the output.
103
+ - **Failure scenario**: `heightMap2AOMap` calls `new DenoiseShader(2)` (`engine/ecs/grid/HeightMap2AOMap.js:76`), intending sigma = 2 spatial, 0.2 range. The shader gets `bSigma = 2`, so `normpdf3(rgba.rgb - pixel.rgb, 2.0)` ≈ `0.199 · exp(-Δ²/8)` — for AO values in [0,1] the exponent never drops below `exp(-0.375) ≈ 0.69`, i.e. the range weight is nearly constant. The "edge-preserving bilateral filter" degrades into a plain 15×15 Gaussian blur, and the terrain AO map smears occlusion across cliff bases and ridge lines instead of holding those edges.
104
+ - **Fix**: `sigmas: { type: "v2", value: new Vector2(sigma, filterSmoothness) }`.
105
+ - **Confidence**: Confirmed
106
+
107
+ ---
108
+
109
+ ### GFXG-9 Uninitialised accumulator in the impostor bake supersampler
110
+ - **Severity**: MAJOR
111
+ - **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/impostors/octahedral/shader/BakeShaderStandard.js:95`
112
+ - **Defect**: `vec4 col;` is declared without an initialiser and then accumulated into with `col += texture(...)` four times. GLSL ES 3.00 leaves uninitialised local variables undefined; there is no guarantee of zero-fill and drivers differ (desktop NVIDIA/AMD commonly zero, several mobile/Adreno and ANGLE-on-D3D configurations do not).
113
+ - **Failure scenario**: Run `ImpostorBaker.bake()` on a driver that does not zero-initialise locals. Every call to `texture_supersample_4` returns `garbage + Σ(4 samples) · 0.25`, so the baked atlas's colour, normal+depth and ORM channels are all offset by a per-invocation constant. The impostor comes out with a uniform colour cast, wrong depths (the `.a` channel feeds the parallax and the shadow-caster height) and a shifted normal — and the bake is cached to disk, so the corruption persists.
114
+ - **Fix**: `vec4 col = vec4(0.0);`
115
+ - **Confidence**: Likely (unconditionally undefined per spec; the visible symptom is driver-dependent)
116
+
117
+ ---
118
+
119
+ ## Status of the "already known" list
120
+
121
+ | Item | Status |
122
+ |---|---|
123
+ | LPV GI vertex shader walks the tetrahedral mesh in object space (`gi/material/space_vertex/build_vertex_shader.js:18`) | Still open — `lpv_walk_to_tetrahedron(position, …)` unchanged; note GFXG-1 sits on top of it in the same block |
124
+ | Path-tracer material conversion drops texture wrap mode | Not re-examined (excluded from write-up per brief) |
125
+ | Path-tracer tint linearized twice | Not re-examined (excluded per brief) |
126
+ | Path-tracer emissive colour/intensity dropped | Not re-examined (excluded per brief) |
127
+ | `WaterShader` references r136-removed `ShaderChunk` symbols | Still open — `lights_pars` (×2, `:22`/`:78`) and `linear_to_gamma_fragment` (`:142`) resolve to `undefined` and are `join`ed into the source as the literal text `undefined`; `BRDF_Diffuse_Lambert` (`:112`, `:124`) also no longer exists |
128
+ | Water never disposes its DataTexture/geometry/material | Not re-examined (no `water/` directory in scope; `material/WaterMaterial.js` has no dispose path) |
129
+ | AO `ShaderMaterial` leak on shutdown | Not re-examined (lives under `render/`, out of scope) |
130
+ | Stale `projectionMatrixInverse` after a projection modifier | Not re-examined (out of scope) |
131
+ | `DepthLimitedBlur` dropping `format` | Not re-examined (`render/`, out of scope) |
132
+ | `NormalFrameBuffer` shared-material wrong normals | Not re-examined (`render/`, out of scope) |
133
+ | Lightmap T-junction shading-normal carry | Not re-examined (excluded per brief) |
134
+ | `set_environment_texture` leaking PMREM targets | Still open — `GraphicsEngine.js:401/405/415` dispose the PMREM generator and the previous target, so the specific leak may already be addressed; not investigated further |
135
+
136
+ ## Notes on things deliberately NOT reported
137
+
138
+ - `sh3/gi/material/MaterialTransformer.js:311` `result.clone();` discards its return value. It is a dead statement, not a behaviour change — the transformer is *supposed* to mutate the source in place (the newer `LightProbeGridMaterialTransformer.transform` does exactly that without the stray clone).
139
+ - `VecToSphereOct` in the impostor shaders uses GLSL `sign()` (returns 0 at 0) where the CPU baker uses `sign_not_zero`. The divergence only manifests for camera directions with an exactly-zero X or Z component *and* negative Y — measure-zero and, for the hemisphere capture mode, unreachable.
140
+ - `HemiOctahedralUvEncoder` does not override `unit_to_uv`, so the inherited `throw new Error('Not implemented')` fires. No caller reaches it and there is no scene that produces a wrong render from it.
141
+ - Uninitialised `LayerCompositer`/`CompositLayer` material disposal and the `DataTexture` allocated in `HeightMap2AOMap` are small one-shot leaks with no visible rendering consequence.
142
+ - `ImageFilter.processTexture` calls `renderer.setSize()` and never restores it, but its only caller drives it through `WebGLRendererPool.global`, i.e. an offscreen renderer, so the main canvas is unaffected.
143
+ - `TubeMaterialX`'s lit branch feeds the `color/255` vertex colour to the light chain as a linear value and then runs `<encodings_fragment>`, whereas the unlit branch writes it raw. This matches three's own vertex-colour convention, so it is a convention choice rather than a defect.