@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
@@ -468,6 +468,40 @@ class List {
468
468
  return this.data.slice(start, end);
469
469
  }
470
470
 
471
+ /**
472
+ * Whether every element satisfies the condition. Vacuously true when empty,
473
+ * matching Array.prototype.every.
474
+ *
475
+ * @param {function(element:T):boolean} condition
476
+ * @returns {boolean}
477
+ */
478
+ every(condition) {
479
+ const l = this.length;
480
+ const data = this.data;
481
+
482
+ for (let i = 0; i < l; i++) {
483
+ if (!condition(data[i])) {
484
+ return false;
485
+ }
486
+ }
487
+
488
+ return true;
489
+ }
490
+
491
+ /**
492
+ * Iterate the list's elements, so `for...of` and spread work on it the way
493
+ * they already do on HashMap, HashSet and Deque.
494
+ *
495
+ * @returns {Iterator<T>}
496
+ */
497
+ * [Symbol.iterator]() {
498
+ const data = this.data;
499
+
500
+ for (let i = 0; i < this.length; i++) {
501
+ yield data[i];
502
+ }
503
+ }
504
+
471
505
  /**
472
506
  *
473
507
  * @param {function(element:T):boolean} condition
@@ -1 +1 @@
1
- {"version":3,"file":"SortedListProjection.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/list/SortedListProjection.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;IACI;;;;OAIG;IACH,mBAHW,KAAK,CAAC,CAAC,qBACE,CAAC,QAAC,CAAC,KAAE,MAAM,EAoB9B;IAjBG;;;OAGG;IACH,OAFU,KAAK,CAAC,CAAC,CAEC;IAElB;;;OAGG;IACH,QAFU,KAAK,CAAC,CAAC,CAEO;IAExB;;;OAGG;IACH,mBAFmB,CAAC,QAAE,CAAC,KAAG,MAAM,CAEJ;IAGhC,aAIC;IAED,eAEC;IAED;;;OAGG;IACH,mBAFW,CAAC,QAMX;IAGD,cAUC;CAEJ;iBAhEgB,WAAW"}
1
+ {"version":3,"file":"SortedListProjection.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/list/SortedListProjection.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;IACI;;;;OAIG;IACH,mBAHW,KAAK,CAAC,CAAC,qBACE,CAAC,QAAC,CAAC,KAAE,MAAM,EAoB9B;IAjBG;;;OAGG;IACH,OAFU,KAAK,CAAC,CAAC,CAEC;IAElB;;;OAGG;IACH,QAFU,KAAK,CAAC,CAAC,CAEO;IAExB;;;OAGG;IACH,mBAFmB,CAAC,QAAE,CAAC,KAAG,MAAM,CAEJ;IAGhC,aAIC;IAED,eAEC;IAED;;;OAGG;IACH,mBAFW,CAAC,QASX;IAGD,cAUC;CAEJ;iBAnEgB,WAAW"}
@@ -45,9 +45,12 @@ export class SortedListProjection {
45
45
  * @param {T} el
46
46
  */
47
47
  handleAddition(el) {
48
+ // binarySearchLowIndex returns the index of the largest element <= el,
49
+ // or -1 when el sorts below everything. The insertion point is one past
50
+ // that in both cases.
48
51
  const where = binarySearchLowIndex(this.output.data, el, this.comparator);
49
52
 
50
- this.output.insert(where, el);
53
+ this.output.insert(where + 1, el);
51
54
  }
52
55
 
53
56
 
@@ -14,11 +14,34 @@ export class BiMap<K, V> {
14
14
  */
15
15
  backward: Map<V, K>;
16
16
  /**
17
+ * Note the argument order: the pair is stored as value-then-key, which is
18
+ * how the only in-tree consumer (EncodingBinaryBuffer) calls it. The
19
+ * parameter names used to claim the opposite, which made every accessor
20
+ * below look inverted.
17
21
  *
22
+ * @param {V} value
18
23
  * @param {K} key
24
+ */
25
+ add(value: V, key: K): void;
26
+ /**
27
+ * Remove a pair by its value.
28
+ *
19
29
  * @param {V} value
30
+ * @returns {boolean} whether a pair was removed
31
+ */
32
+ deleteByValue(value: V): boolean;
33
+ /**
34
+ * Remove a pair by its key.
35
+ *
36
+ * @param {K} key
37
+ * @returns {boolean} whether a pair was removed
38
+ */
39
+ deleteByKey(key: K): boolean;
40
+ /**
41
+ * @returns {number}
20
42
  */
21
- add(key: K, value: V): void;
43
+ get size(): number;
44
+ clear(): void;
22
45
  /**
23
46
  *
24
47
  * @param {V} value
@@ -32,6 +55,6 @@ export class BiMap<K, V> {
32
55
  */
33
56
  getValueByKey(address: K): V | undefined;
34
57
  get: (address: any) => any;
35
- set: (key: any, value: any) => void;
58
+ set: (value: any, key: any) => void;
36
59
  }
37
60
  //# sourceMappingURL=BiMap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BiMap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/map/BiMap.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH;IAEI;;;OAGG;IACH,SAFU,IAAI,CAAC,EAAE,CAAC,CAAC,CAEC;IACpB;;;OAGG;IACH,UAFU,IAAI,CAAC,EAAE,CAAC,CAAC,CAEE;IAGrB;;;;OAIG;IACH,SAHW,CAAC,SACD,CAAC,QAKX;IAED;;;;OAIG;IACH,qBAHW,CAAC,GACC,CAAC,GAAC,SAAS,CAIvB;IAED;;;;OAIG;IACH,uBAHW,CAAC,GACC,CAAC,GAAC,SAAS,CAIvB;IAGL,2BAAmB;IACnB,oCAAmB;CAHlB"}
1
+ {"version":3,"file":"BiMap.d.ts","sourceRoot":"","sources":["../../../../../src/core/collection/map/BiMap.js"],"names":[],"mappings":"AAAA;;;GAGG;AACH;IAEI;;;OAGG;IACH,SAFU,IAAI,CAAC,EAAE,CAAC,CAAC,CAEC;IACpB;;;OAGG;IACH,UAFU,IAAI,CAAC,EAAE,CAAC,CAAC,CAEE;IAGrB;;;;;;;;OAQG;IACH,WAHW,CAAC,OACD,CAAC,QAKX;IAED;;;;;OAKG;IACH,qBAHW,CAAC,GACC,OAAO,CAanB;IAED;;;;;OAKG;IACH,iBAHW,CAAC,GACC,OAAO,CAanB;IAED;;OAEG;IACH,mBAEC;IAED,cAGC;IAED;;;;OAIG;IACH,qBAHW,CAAC,GACC,CAAC,GAAC,SAAS,CAIvB;IAED;;;;OAIG;IACH,uBAHW,CAAC,GACC,CAAC,GAAC,SAAS,CAIvB;IAGL,2BAAmB;IACnB,oCAAmB;CAHlB"}
@@ -17,13 +17,67 @@ export class BiMap {
17
17
 
18
18
 
19
19
  /**
20
+ * Note the argument order: the pair is stored as value-then-key, which is
21
+ * how the only in-tree consumer (EncodingBinaryBuffer) calls it. The
22
+ * parameter names used to claim the opposite, which made every accessor
23
+ * below look inverted.
20
24
  *
25
+ * @param {V} value
21
26
  * @param {K} key
27
+ */
28
+ add(value, key) {
29
+ this.forward.set(key, value);
30
+ this.backward.set(value, key);
31
+ }
32
+
33
+ /**
34
+ * Remove a pair by its value.
35
+ *
22
36
  * @param {V} value
37
+ * @returns {boolean} whether a pair was removed
38
+ */
39
+ deleteByValue(value) {
40
+ const key = this.backward.get(value);
41
+
42
+ if (key === undefined) {
43
+ return false;
44
+ }
45
+
46
+ this.backward.delete(value);
47
+ this.forward.delete(key);
48
+
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Remove a pair by its key.
54
+ *
55
+ * @param {K} key
56
+ * @returns {boolean} whether a pair was removed
57
+ */
58
+ deleteByKey(key) {
59
+ const value = this.forward.get(key);
60
+
61
+ if (value === undefined) {
62
+ return false;
63
+ }
64
+
65
+ this.forward.delete(key);
66
+ this.backward.delete(value);
67
+
68
+ return true;
69
+ }
70
+
71
+ /**
72
+ * @returns {number}
23
73
  */
24
- add(key, value) {
25
- this.forward.set(value, key);
26
- this.backward.set(key, value);
74
+ get size() {
75
+ return this.forward.size;
76
+ }
77
+
78
+ clear() {
79
+ this.forward.clear();
80
+ this.backward.clear();
27
81
  }
28
82
 
29
83
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"IsAnything.d.ts","sourceRoot":"","sources":["../../../../../src/core/debug/matchers/IsAnything.js"],"names":[],"mappings":"AAEA;IAGI,8BAIC;IAND,gBAAO;IAQP,uDAEC;IAED,mCAEC;CACJ;4BAlB2B,mBAAmB"}
1
+ {"version":3,"file":"IsAnything.d.ts","sourceRoot":"","sources":["../../../../../src/core/debug/matchers/IsAnything.js"],"names":[],"mappings":"AAEA;IAGI,8BAIC;IAND,gBAAO;IAQP,uDAMC;IAED,mCAEC;CACJ;4BAtB2B,mBAAmB"}
@@ -10,7 +10,11 @@ export class IsAnything extends BaseMatcher {
10
10
  }
11
11
 
12
12
  matches(item, mismatch_description) {
13
- return false;
13
+ // "anything" accepts everything; it is the identity of a conjunction and
14
+ // the placeholder for an argument whose value does not matter. Returning
15
+ // false made it match nothing at all -- and, since nothing() is defined
16
+ // as not(anything()), made that match everything.
17
+ return true;
14
18
  }
15
19
 
16
20
  describeTo(description) {
@@ -1 +1 @@
1
- {"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../../../../../src/core/events/signal/Signal.js"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH;IACI;;;;OAIG;IACH,iBAAqB;IAErB;;;;OAIG;IACH,cAAU;IAEV;;;OAGG;IACH,YAFU,MAAM,CAED;IAWf;;;OAGG;IACH,yBAEC;IAfD;;;OAGG;IACH,sBAEC;IAYD;;;;OAIG;IACH,cAHW,MAAM;;KAAY,GAChB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM;;KAAY,GAChB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM;;KAAY,SAClB,OAAO,QAQjB;IAED;;;;OAIG;IACH,cAHW,MAAM;;KAAY,GAChB,OAAO,CAInB;IAGD;;;;;OAKG;IACH,4CAFa,OAAO,CAcnB;IAED,aAEC;IAED,eAEC;IAED;;;OAGG;IACH,eAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,yCAQC;IAED;;;;OAIG;IACH,sCAOC;IA8BD;;;;;OAKG;IACH,oCAFa,OAAO,CA2CnB;IAED;;;;;;;;OAQG;IACH,kBAEC;IAqDD;;;;;;;;;;;;;;OAcG;IACH,WAZY,QAAQ,EAAE,CAAC,CAkBtB;IAED;;;OAGG;IACH,+BA2CC;IAGD;;;OAGG;IACH,4BA6CC;IAED;;;;OAIG;IACH,sCA4CC;IAED;;;;OAIG;IACH,4CA2CC;IAED;;;;;OAKG;IACH,oDA2CC;IAED;;;;;;OAMG;IACH,4DA2CC;IAED;;;;;;;OAOG;IACH,oEA2CC;IAED;;;;;;;;OAQG;IACH,4EA2CC;IAED;;;;;;;;;;OAUG;IACH,4FA2CC;IAED;;;;OAIG;IACH,aAHW,MAAM,GACJ,MAAM,CAalB;IAIL;;;OAGG;IACH,mBAFU,OAAO,CAEQ;;CAPxB"}
1
+ {"version":3,"file":"Signal.d.ts","sourceRoot":"","sources":["../../../../../src/core/events/signal/Signal.js"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH;IACI;;;;OAIG;IACH,iBAAqB;IAErB;;;;OAIG;IACH,cAAU;IAEV;;;OAGG;IACH,YAFU,MAAM,CAED;IAWf;;;OAGG;IACH,yBAEC;IAfD;;;OAGG;IACH,sBAEC;IAYD;;;;OAIG;IACH,cAHW,MAAM;;KAAY,GAChB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM;;KAAY,GAChB,IAAI,CAIhB;IAED;;;;OAIG;IACH,gBAHW,MAAM;;KAAY,SAClB,OAAO,QAQjB;IAED;;;;OAIG;IACH,cAHW,MAAM;;KAAY,GAChB,OAAO,CAInB;IAGD;;;;;OAKG;IACH,4CAFa,OAAO,CAcnB;IAED,aAEC;IAED,eAEC;IAED;;;OAGG;IACH,eAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,yCAQC;IAED;;;;OAIG;IACH,sCAOC;IA8BD;;;;;OAKG;IACH,oCAFa,OAAO,CA2CnB;IAED;;;;;;;;OAQG;IACH,kBAEC;IAqDD;;;;;;;;;;;;;;OAcG;IACH,WAZY,QAAQ,EAAE,CAAC,CAkBtB;IAED;;;OAGG;IACH,+BA2CC;IAGD;;;OAGG;IACH,4BA6CC;IAED;;;;OAIG;IACH,sCA4CC;IAED;;;;OAIG;IACH,4CA2CC;IAED;;;;;OAKG;IACH,oDA2CC;IAED;;;;;;OAMG;IACH,4DA2CC;IAED;;;;;;;OAOG;IACH,oEA2CC;IAED;;;;;;;;OAQG;IACH,4EA2CC;IAED;;;;;;;;;;OAUG;IACH,4FA2CC;IAED;;;;OAIG;IACH,aAHW,MAAM,GACJ,MAAM,CAelB;IAIL;;;OAGG;IACH,mBAFU,OAAO,CAEQ;;CAPxB"}
@@ -798,7 +798,9 @@ export class Signal {
798
798
  const result = new Signal();
799
799
 
800
800
  function handler() {
801
- result.dispatch(arguments);
801
+ // spread: dispatch takes varargs, so passing the arguments object
802
+ // forwards it as a single argument to every subscriber
803
+ result.dispatch(...arguments);
802
804
  }
803
805
 
804
806
  this.add(handler);
@@ -0,0 +1,433 @@
1
+ # Polygon triangulation — design note
2
+
3
+ Status: **LANDED** · 2026-08-01
4
+ Follows [`../../MANIFOLD_COMPARISON_2026_08_01.md`](../../MANIFOLD_COMPARISON_2026_08_01.md) §4.1.
5
+
6
+ > **Outcome.** Built as designed. What it cost, and what it turned up, is in §7 —
7
+ > including two defects in *existing* meep primitives that the work exposed and
8
+ > that are now fixed, and one place where the plan above was simply wrong.
9
+
10
+ meep has no general polygon triangulator. This note records the research behind
11
+ the one being added: the algorithm choice, and — for every component it needs —
12
+ the answer to *"does meep already have this, and is this how meep does it?"*
13
+
14
+ ---
15
+
16
+ ## 1. What the callers actually need
17
+
18
+ Surveyed every site in meep + app that triangulates or wants to:
19
+
20
+ | Site | Today | n | Holes | Needs |
21
+ | --- | --- | --- | --- | --- |
22
+ | [`bt_face_triangulate`](../../3d/topology/struct/binary/io/face/bt_face_triangulate.js) | fan from the first loop vertex, documented as convex-only | 4–12 | no | correct output for concave faces |
23
+ | [`bt_mesh_triangulate`](../../3d/topology/struct/binary/io/bt_mesh_triangulate.js) | applies the above to every face unconditionally | — | no | same |
24
+ | [`bt_mesh_close_boundary_holes`](../../3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.js) | Steiner vertex at the loop centroid + fan | 3–500 | no | a fill without a Steiner point, for non-convex loops |
25
+ | [`navmesh_build_topology`](../../../../engine/navigation/mesh/build/navmesh_build_topology.js):97,108 | *explicit TODO*: "identify flat areas and re-triangulate them to remove unnecessary triangles" | 10³–10⁴ | **yes** (obstacle footprints) | the general case |
26
+ | [`clip_soup_against_overhangs`](../../../../engine/navigation/mesh/build/clip_soup_against_overhangs.js):378 | fan, over convex pieces from `polygon2_clip_halfplane` | ≤ 8 | no | **correct as-is** — every piece is a half-plane intersection, hence convex. Listed only so nobody "fixes" it. |
27
+ | future: `Extrude` / `Revolve` / `Slice` / `Project`, glyph outlines (`opentype.js` is already a dependency), cross-sections | — | any | yes | the general case |
28
+
29
+ Two conclusions:
30
+
31
+ 1. The dominant call is **tiny n, no holes** (`bt_face_triangulate`). The
32
+ asymptotics of the algorithm barely matter there; correctness and a small
33
+ constant do.
34
+ 2. The demanding call is **large n with holes** (navmesh flat regions). That one
35
+ is offline.
36
+
37
+ Nothing in the survey needs an incremental/dynamic triangulation, and nothing
38
+ needs Steiner points. That rules a lot of design space out immediately.
39
+
40
+ ---
41
+
42
+ ## 2. Algorithm choice
43
+
44
+ ### Candidates
45
+
46
+ | | complexity | status structure needed | predicate set | output |
47
+ | --- | --- | --- | --- | --- |
48
+ | **Monotone decomposition sweep** (Lee–Preparata; what manifold uses) | O(n log n) | ordered dictionary keyed on "edge x at sweep y" | orient2d **+ inexact edge/horizontal-line comparisons** | arbitrary order |
49
+ | **Ear clipping** (earcut-family) | O(n²), ~O(n log n) with z-order | none — a doubly-linked ring | **orient2d only** | ears, in cut order |
50
+ | **Constrained Delaunay** | O(n log n) | ordered dictionary + flip machinery | orient2d + incircle | best quality |
51
+ | **Seidel trapezoidation** | O(n log* n) | trapezoid DAG | orient2d | arbitrary order |
52
+
53
+ ### Decision: ear clipping, with exact `orient2d`
54
+
55
+ Three reasons, in order of weight.
56
+
57
+ **(a) meep already ships the one predicate ear clipping needs, and nothing
58
+ else's predicate set is satisfiable.** `robust-predicates@3.0.2` is a
59
+ first-class dependency (`package.json`), already whitelisted in the root
60
+ `jest.conf.json` `transformIgnorePatterns`, and already used in four places —
61
+ [`orient2d_indexed.js`](../convex-hull/orient2d_indexed.js) for the 2D hull,
62
+ [`orient3d_robust.js`](../../3d/plane/orient3d_robust.js),
63
+ [`in_sphere3d_robust.js`](../../3d/sphere/in_sphere3d_robust.js),
64
+ [`TetrahedralMesh.js`](../../3d/tetrahedra/TetrahedralMesh.js).
65
+
66
+ Every decision an ear clipper makes reduces to `orient2d`:
67
+
68
+ - *is vertex v convex?* → `orient2d(prev, v, next)`
69
+ - *is point p inside triangle (a,b,c)?* → three `orient2d` calls
70
+ - *is the candidate diagonal locally valid?* → two `orient2d` calls
71
+
72
+ So the whole algorithm can be made **exact**, with no epsilon anywhere in the
73
+ combinatorial decisions. That is not true of the sweep: its status structure
74
+ compares edges by their x-coordinate at the current sweep y, which is a
75
+ division. You can reformulate it into orient2d tests, but the reformulation is
76
+ subtle and a comparator that is merely *nearly* consistent corrupts an ordered
77
+ dictionary silently.
78
+
79
+ **(b) meep has no ordered dictionary, and adding one is the wrong first move.**
80
+ `core/collection` has `BinaryHeap`, `FibonacciHeap`, `IndexedFloatMinHeap`,
81
+ `Uint32Heap`, `Uint32Map`, `PairUint32Map`, `HashMap`, `HashSet`, `RingBuffer`,
82
+ `Deque`, `union_find`, `RowFirstTable`, `binarySearchLowIndex/HighIndex` — no
83
+ balanced BST, no skip list, no ordered set. The sweep needs one. Building a
84
+ red-black tree *and* a sweep triangulator on top of it, in one change, means the
85
+ first bug is ambiguous between the two. Ear clipping needs no new collection at
86
+ all: its ring is two `Uint32Array`s.
87
+
88
+ **(c) Ear clipping's output is directly consumable by meep's topology surgery.**
89
+ `bt_face_triangulate`'s existing primitive is "cut triangle (l_prev, l_curr,
90
+ l_next) off the face by inserting one edge" — that *is* an ear cut. Today it
91
+ always picks the first ear whether or not it is one. The fix is to pick a real
92
+ ear. A sweep would emit an arbitrary triangle set that then has to be
93
+ re-sequenced into diagonal insertions.
94
+
95
+ ### What we give up, and the upgrade path
96
+
97
+ Ear clipping is O(n²) in the worst case and produces sliver-heavy
98
+ triangulations. Both are accepted, explicitly:
99
+
100
+ - **Time.** The z-order acceleration (§3.6) makes the point-in-ear test local
101
+ rather than global, which is what turns the practical cost into ~O(n log n).
102
+ Per [`feedback_reuse_over_micro_opt`], the right response if this ever shows
103
+ up in a profile is a measurement, not a pre-emptive rewrite. The bench hook is
104
+ the navmesh flat-region path, which is the only large-n caller and is offline.
105
+ - **Quality.** Deliberately *not* solved inside the triangulator. Adding a
106
+ "quality mode" flag would be exactly the `X = null means auto` sentinel
107
+ pattern that is rejected in this codebase ([`feedback_uniform_flow`]). The
108
+ triangulator has one behaviour: produce a valid triangulation. A caller that
109
+ wants Delaunay quality gets a **separate** `tri2_mesh_flip_to_delaunay` pass
110
+ (§6) built on `incircle` from the same `robust-predicates` package — the exact
111
+ 2D analogue of the existing `in_sphere3d_robust.js`, and independently
112
+ testable.
113
+
114
+ ---
115
+
116
+ ## 3. Component-by-component meep audit
117
+
118
+ For each piece: *does meep already have it? is this how meep does it?*
119
+
120
+ ### 3.1 Exact orientation predicate — **reuse, verbatim**
121
+
122
+ `orient2d` from `robust-predicates`, wrapped the way meep already wraps it.
123
+ [`orient2d_indexed.js`](../convex-hull/orient2d_indexed.js) is the existing
124
+ precedent: it takes a flat `points` array plus three *indices*, which is exactly
125
+ the call shape the triangulator wants (the ring stores point indices). **Reused
126
+ unchanged.** No new vendor import site — per [`feedback_confine_vendor_imports`]
127
+ the `robust-predicates` import stays inside the two existing wrapper files.
128
+
129
+ ### 3.2 The vertex ring — **BinaryTopology-flavoured, not object nodes**
130
+
131
+ earcut uses a linked list of JS objects (`{i, x, y, prev, next, z, prevZ,
132
+ nextZ, steiner}`). That is not how meep does it. Every comparable meep
133
+ structure is flat typed arrays with integer handles and a `NULL_POINTER`
134
+ sentinel — `BinaryElementPool`, `BinaryTopology`, `RowFirstTable`, `bvh8`.
135
+
136
+ So the ring is:
137
+
138
+ ```
139
+ ring_next : Uint32Array // node -> node
140
+ ring_prev : Uint32Array // node -> node
141
+ ring_point : Uint32Array // node -> index into `points`
142
+ ring_z : Uint32Array // node -> 2D Morton code (§3.6)
143
+ ring_zprev : Uint32Array
144
+ ring_znext : Uint32Array
145
+ ```
146
+
147
+ Node ids are dense `[0, total_vertex_count)`; removal is unlinking, never
148
+ compaction. `NULL_POINTER = 0xFFFFFFFF` is imported from
149
+ [`BinaryTopology.js`](../../3d/topology/struct/binary/BinaryTopology.js) rather
150
+ than redeclared — it is already the codebase-wide sentinel.
151
+
152
+ Buffers are module-static and grown on demand, matching the
153
+ `_cur` / `_tmp` / `_side` + `ensure_capacity` pattern in
154
+ [`clip_soup_against_overhangs.js`](../../../../engine/navigation/mesh/build/clip_soup_against_overhangs.js).
155
+ The triangulator is therefore **not re-entrant**, same as
156
+ [`bt_mesh_simplify_by_error`](../../3d/topology/struct/binary/io/bt_mesh_simplify_by_error.js)
157
+ (whose `_degree` / `_vboundary` caches are module-level and documented as such).
158
+ That is stated in the JSDoc, not guarded against.
159
+
160
+ ### 3.3 Signed area / winding — **reuse**
161
+
162
+ [`polygon2_signed_area`](polygon2_signed_area.js) already exists and already
163
+ handles the "does the contour repeat its first point" case. Contour orientation
164
+ (outer CCW, holes CW) is decided with it. [`tri2_signed_area`](../triangle/tri2_signed_area.js)
165
+ is reused by the tests for area conservation.
166
+
167
+ ### 3.4 Hole elimination — **bridging, no new structure**
168
+
169
+ Holes are eliminated the standard way: for each hole, find the outer-contour
170
+ vertex that is mutually visible from the hole's rightmost vertex, and splice the
171
+ hole into the outer ring through a doubled bridge edge. Requires only:
172
+
173
+ - a ray cast to the right from the hole's rightmost point against outer edges —
174
+ plain arithmetic, no structure;
175
+ - a visibility tie-break — `orient2d` again;
176
+ - sorting the holes by their rightmost x so that bridges never cross.
177
+
178
+ **Sorting**: meep has `array_sort_quick`, `array_quick_sort_by_comparator`,
179
+ `array_quick_sort_by_lookup_map`, `array_quick_sort_by_lookup_uint`. Hole count
180
+ is small (typically < 100); the comparator variant is used rather than a
181
+ hand-rolled sort, per [`feedback_reuse_over_micro_opt`].
182
+
183
+ ### 3.5 Degenerate-input policy — **sentinel return, never throw, never swallow**
184
+
185
+ Real input is not ε-valid: welding produces self-touching contours, imported
186
+ geometry has duplicate and collinear points, navmesh footprints can be zero-area.
187
+ [`feedback_no_silent_catch`] says a function with a legitimate "can be expected
188
+ to fail" mode returns a sentinel; [`feedback_no_defensive_guards`] says do not
189
+ guard can't-happen conditions.
190
+
191
+ Applied:
192
+
193
+ - **Preconditions** (finite coordinates, `contour_count >= 1`, each contour
194
+ `>= 3` vertices, `contour_ends` non-decreasing) are `assert.*` — free in
195
+ production ([`feedback_assert_stripped`]) and documented in JSDoc.
196
+ - **Expected failure** (the polygon is self-intersecting / partially
197
+ degenerate and cannot be fully triangulated) is reported by the **return
198
+ value**: the number of triangles actually written. A caller that cares
199
+ compares it against the exact expected count `V + 2H − 2`, or measures area
200
+ conservation with the companion `polygon2_triangulation_deviation`.
201
+ - **No throw, no try/catch, no silent zero.** A partially-triangulated result is
202
+ still the best available answer and the caller is told how much it got.
203
+
204
+ Three recovery tiers are implemented, in this order, because without them the
205
+ behaviour on real welded meshes is "returns almost nothing":
206
+
207
+ 1. plain ear clipping;
208
+ 2. *cure local intersections* — when no ear is found, remove a vertex whose
209
+ incident edges cross a neighbour's, then retry;
210
+ 3. *split* — find a pair of non-adjacent ring vertices whose diagonal is valid,
211
+ cut the ring in two, and triangulate each half.
212
+
213
+ These are not bandaids ([`feedback_correctness_no_bandaid`]): they are the
214
+ defined behaviour for input outside the ε-valid contract, each has its own
215
+ tests, and none of them weakens an assertion about valid input.
216
+
217
+ ### 3.6 Z-order acceleration — **reuse `split_by_2`**
218
+
219
+ The point-in-ear test is the hot loop. earcut restricts it to points whose 2D
220
+ Morton code lies in the ear's bounding-box code range. meep already has
221
+ [`split_by_2`](../../../binary/split_by_2.js) (bit interleave, libmorton-derived)
222
+ and already uses it for 2D Morton in `SpatialHashGrid` and `StaticR2Tree`, plus
223
+ a full `v3_morton_encode*` family under [`../../3d/morton/`](../../3d/morton/).
224
+ There is no `v2_morton_encode` file, so one is added next to `split_by_2` rather
225
+ than inlining the interleave — filling the gap in the existing family instead of
226
+ growing a private copy.
227
+
228
+ Acceleration is enabled above a vertex-count threshold. This is *not* a mode
229
+ flag on the public API — it is an internal branch on `n`, invisible to callers,
230
+ and both paths are asserted to produce the same triangle count in the tests.
231
+
232
+ ### 3.7 3D → 2D projection — **axis-aligned drop, never a rotation**
233
+
234
+ Faces are 3D. The projection to 2D must not perturb coordinates, or the exact
235
+ predicates stop being exact for the *original* geometry. So: drop the axis with
236
+ the largest |normal| component and keep the other two verbatim, flipping the
237
+ winding when that component is negative. Same choice manifold makes
238
+ (`GetAxisAlignedProjection`).
239
+
240
+ meep has [`v3_orthonormal_matrix_from_normal`](../../vec3/v3_orthonormal_matrix_from_normal.js)
241
+ — used by `clip_soup_against_overhangs` — but that is a *rotation*, and rotating
242
+ introduces rounding into every coordinate. It is deliberately **not** used here.
243
+ The axis-drop helper is new (`v3_project_to_dominant_plane`), which is the
244
+ honest answer to "does meep already have this": it has a related thing that is
245
+ wrong for this job.
246
+
247
+ ### 3.8 Output buffer convention — **`(result, result_offset, …)` first**
248
+
249
+ Matches the bvh3 query convention recorded in [`project_bvh3_review_2026_06`]
250
+ and the `support(result, result_offset, …)` / `vertex_read_coordinate(out, offset, v)`
251
+ shape used throughout `AbstractShape3D` and `BinaryTopology`.
252
+
253
+ ---
254
+
255
+ ## 4. API
256
+
257
+ One general function. A single-contour caller passes `contour_count = 1`; there
258
+ is no convenience overload, because two entry points for one operation is the
259
+ kind of API surface [`feedback_uniform_flow`] rejects.
260
+
261
+ ```js
262
+ /**
263
+ * @param {Uint32Array} result 3 point-indices per triangle
264
+ * @param {number} result_offset
265
+ * @param {ArrayLike<number>} points flat (x, y), stride 2
266
+ * @param {ArrayLike<number>} contour_ends exclusive end vertex index per contour
267
+ * @param {number} contour_count
268
+ * @returns {number} triangles written; < V + 2H - 2 means the input could not be
269
+ * fully triangulated
270
+ */
271
+ polygon2_triangulate(result, result_offset, points, contour_ends, contour_count)
272
+ ```
273
+
274
+ `contour_ends` rather than earcut's `holeIndices` (contour starts) so that
275
+ contour `i` is uniformly `[i === 0 ? 0 : ends[i - 1], ends[i])` — no special case
276
+ for the first contour.
277
+
278
+ Contour 0 is the outer boundary; 1..n are holes. Winding is **not** a
279
+ precondition: the implementation normalises it from `polygon2_signed_area`, so
280
+ callers cannot get it subtly wrong. (This is the one place where inferring
281
+ rather than demanding is right: winding is derivable from the data, so demanding
282
+ it would be redundant state the caller can contradict.)
283
+
284
+ Companions:
285
+
286
+ ```js
287
+ polygon2_triangulation_deviation(result, result_offset, triangle_count, points, contour_ends, contour_count)
288
+ ```
289
+ → relative area error `|Σ|tri| − |polygon||/|polygon|`; `0` for a perfect
290
+ triangulation. Used by the tests as the primary invariant and available to
291
+ callers that need to validate untrusted input.
292
+
293
+ ```js
294
+ polygon3_triangulate(result, result_offset, points3, contour_ends, contour_count, nx, ny, nz)
295
+ ```
296
+ → projects with §3.7 and delegates. This is what `bt_face_triangulate` calls.
297
+
298
+ ---
299
+
300
+ ## 5. Testing plan
301
+
302
+ Per house rules: black-box, no mocks or spies, no assertions on call counts or
303
+ internal paths, verbose and explicit over test frameworks, reproducible, nothing
304
+ vacuous.
305
+
306
+ The triangulator is unusually well suited to **invariant** testing, which is the
307
+ only kind that is honest here — asserting a specific triangle list would bake in
308
+ one valid answer out of many and break on any legitimate change.
309
+
310
+ Invariants asserted for every case:
311
+
312
+ 1. **Triangle count** is exactly `V + 2H − 2`.
313
+ 2. **Area conservation** — `Σ|signed area of each output triangle|` equals
314
+ `|polygon area − Σ hole areas|` to a tight relative tolerance.
315
+ 3. **Consistent orientation** — every output triangle is CCW.
316
+ 4. **No overlap** — pairwise, for small cases: triangle interiors are disjoint
317
+ (sampled at each triangle's incenter, which must lie in exactly one).
318
+ 5. **Index validity** — every emitted index is in `[0, V)`.
319
+ 6. **Coverage** — a grid of sample points inside the polygon and outside every
320
+ hole is covered by exactly one triangle.
321
+
322
+ Case families:
323
+
324
+ - squares, L-shapes, combs, spirals, stars (concave), zero-area spikes;
325
+ - squares with 1 / 2 / nested holes; a hole touching the boundary at a vertex;
326
+ - collinear runs; duplicate consecutive points; a contour that repeats its first
327
+ point; a hole wound the "wrong" way (must still work — §4);
328
+ - the two accelerated/non-accelerated paths agreeing on a large input;
329
+ - **corpus** — `polygon2_triangulate.corpus.js`, a checked-in list of inputs
330
+ that have ever failed, replayed as tests. Manifold's most transferable
331
+ practice: `test/polygons/polygon_corpus.txt` is 2.9 MB of exactly this.
332
+ - **fuzz** — a *seeded* generator (`seededRandom` from `core/math/random`, so
333
+ every failure is reproducible from its seed) producing random contours,
334
+ asserting the invariants that survive on invalid input: never throws, never
335
+ hangs, never emits an out-of-range index, and the deviation is reported
336
+ honestly.
337
+
338
+ ---
339
+
340
+ ## 6. What landed
341
+
342
+ | File | What |
343
+ | --- | --- |
344
+ | [`polygon2_triangulate.js`](polygon2_triangulate.js) | the triangulator |
345
+ | [`polygon2_triangulation_deviation.js`](polygon2_triangulation_deviation.js) | relative area error of a result |
346
+ | [`polygon2_is_counter_clockwise.js`](polygon2_is_counter_clockwise.js) | exact winding test (see §7.1) |
347
+ | [`../v2_morton_encode.js`](../v2_morton_encode.js) | 2D Morton code, filling the gap next to `v3_morton_encode*` |
348
+ | [`../../3d/polygon/polygon3_triangulate.js`](../../3d/polygon/polygon3_triangulate.js) | planar 3D adapter, axis-aligned projection |
349
+ | [`../../3d/polygon/polygon3_compute_normal.js`](../../3d/polygon/polygon3_compute_normal.js) | Newell normal for an arbitrary polygon |
350
+ | [`polygon2_triangulate.corpus.js`](polygon2_triangulate.corpus.js) | recorded failures, replayed as tests |
351
+ | [`bt_face_triangulate.js`](../../3d/topology/struct/binary/io/face/bt_face_triangulate.js) | rewritten onto the triangulator |
352
+
353
+ Also fixed, because the work depended on them and they were wrong: §7.1 and §7.2.
354
+
355
+ ## 7. What the work turned up
356
+
357
+ ### 7.1 The plan's winding test was not exact — `polygon2_signed_area` cannot be
358
+
359
+ The design above says every combinatorial decision is exact. The first
360
+ implementation broke that promise in its very first step: it picked each
361
+ contour's winding from `polygon2_signed_area`, a shoelace sum. A shoelace sum
362
+ adds terms of magnitude `O(|p|²)` to reach an answer that is the area, so its
363
+ *sign* is rounding noise whenever the polygon is small relative to its distance
364
+ from the origin. A 1e-9-sided square at (1, 1) has a true area of 1e-18 against
365
+ per-term rounding of ~1e-16. The ring came out backwards, every corner looked
366
+ reflex, and the triangulator returned nothing.
367
+
368
+ Fixed by [`polygon2_is_counter_clockwise`](polygon2_is_counter_clockwise.js):
369
+ one exact `orient2d` at the contour's lexicographically smallest vertex, which
370
+ is necessarily on the convex hull, so the turn there is the contour's own. No
371
+ summation, no cancellation, magnitude-independent.
372
+
373
+ The lesson generalises: **use the signed area when you want the area, and the
374
+ predicate when you want the winding.** They are not the same question.
375
+
376
+ ### 7.2 Two existing meep primitives had the same cancellation defect
377
+
378
+ Both were on the path of the new code, so both had to be right.
379
+
380
+ - **`tri2_signed_area`** computed the expanded determinant of absolute
381
+ coordinates. Its consumers are mostly *sign* tests — the Polyanya funnel
382
+ (`bt_mesh_face_find_path_polyanya`), the atlas parameterisation validity check
383
+ — which is the worst possible exposure: a cancelled sign is a wrong path or a
384
+ wrongly-rejected chart, not a slightly wrong number. Now differences-first.
385
+ - **`polygon2_signed_area`** had the same shape and now subtracts the first
386
+ vertex before accumulating. As a bonus the special-case closing term
387
+ disappeared: both edges at the reference vertex contribute zero, so the
388
+ open-vs-closed contour branch is gone.
389
+
390
+ Neither changes any API. Both are strictly better conditioned, and both now have
391
+ specs that fail on the old implementation.
392
+
393
+ This is the same defect class as `tri3_compute_signed_volume` in
394
+ [the manifold comparison](../../MANIFOLD_COMPARISON_2026_08_01.md) §3.1, which
395
+ remains open.
396
+
397
+ ### 7.3 `V + 2H − 2` is an upper bound, not an equality
398
+
399
+ The first spec asserted equality and failed on two hole cases. The triangulator
400
+ was right: bridging a hole can leave three collinear corners, and absorbing them
401
+ is better output than emitting a zero-area triangle. Area conservation plus
402
+ "every input vertex is used" is the honest invariant; the exact count is asserted
403
+ only for hole-free shapes whose vertices are all genuine corners.
404
+
405
+ ### 7.4 The sentinel had to be propagated, not assumed away
406
+
407
+ `filter_points` returns a collapsed-ring sentinel. The recovery pass took it as a
408
+ node id and walked it — an infinite loop on a 20-point coarse-grid soup (corpus
409
+ entry 2). Fixed by making the recovery pass accept and propagate the sentinel,
410
+ and stop once the ring holds fewer than three nodes. That second condition is a
411
+ genuine termination condition, not a guard: with fewer than three nodes there is
412
+ no triangle to cure and nothing left to walk.
413
+
414
+ ### 7.5 Replaying an ear sequence as topology surgery needs the ring, not the handles
415
+
416
+ `bt_face_triangulate` cuts each ear by moving two existing corners onto the new
417
+ face and giving the shrunken polygon a *fresh* corner in the ear's predecessor's
418
+ place. The first version replayed the triangulator's output against the corner
419
+ handles collected up front, which go stale the moment a cut happens — producing
420
+ 2-corner faces. It now walks the same index ring the dry run uses and swaps each
421
+ replaced handle as it goes.
422
+
423
+ ## 8. Deliberately out of scope
424
+
425
+ - **Delaunay quality** (`tri2_mesh_flip_to_delaunay` via `incircle`). Separate
426
+ pass, separate change; the note above explains why it must not be a flag.
427
+ - **Steiner points / refinement.** No caller needs them.
428
+ - **2D polygon boolean and offsetting.** Different problem, much larger; §4.2 of
429
+ the comparison doc.
430
+ - **`bt_mesh_close_boundary_holes` rework.** It currently adds a centroid
431
+ Steiner vertex, which is a *different* output (n triangles instead of n−2) and
432
+ is what its tetrahedralisation consumer wants. Switching it is a behaviour
433
+ change with its own consumer study, tracked separately.