@woosh/meep-engine 2.138.20 → 2.140.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 (584) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
  3. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
  4. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
  5. package/src/core/collection/PairUint32Map.d.ts +100 -0
  6. package/src/core/collection/PairUint32Map.d.ts.map +1 -0
  7. package/src/core/collection/PairUint32Map.js +321 -0
  8. package/src/core/collection/Uint32Map.d.ts +119 -0
  9. package/src/core/collection/Uint32Map.d.ts.map +1 -0
  10. package/src/core/collection/Uint32Map.js +345 -0
  11. package/src/core/collection/array/array_shuffle.d.ts +10 -3
  12. package/src/core/collection/array/array_shuffle.d.ts.map +1 -1
  13. package/src/core/collection/array/array_shuffle.js +27 -22
  14. package/src/core/collection/heap/FibonacciHeap.d.ts +195 -0
  15. package/src/core/collection/heap/FibonacciHeap.d.ts.map +1 -0
  16. package/src/core/collection/heap/FibonacciHeap.js +586 -0
  17. package/src/core/collection/heap/Uint32Heap.js +1 -1
  18. package/src/core/collection/heap/Uint32Heap4.d.ts +169 -0
  19. package/src/core/collection/heap/Uint32Heap4.d.ts.map +1 -0
  20. package/src/core/collection/heap/Uint32Heap4.js +490 -0
  21. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts +30 -0
  22. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
  23. package/src/core/geom/3d/aabb/aabb3_transform_oriented.js +93 -0
  24. package/src/core/geom/3d/line/line3_closest_points_segment_segment.d.ts +27 -0
  25. package/src/core/geom/3d/line/line3_closest_points_segment_segment.d.ts.map +1 -0
  26. package/src/core/geom/3d/line/line3_closest_points_segment_segment.js +88 -0
  27. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
  28. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  29. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  30. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  31. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  32. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  33. package/src/core/geom/3d/shape/BoxShape3D.d.ts +61 -0
  34. package/src/core/geom/3d/shape/BoxShape3D.d.ts.map +1 -0
  35. package/src/core/geom/3d/shape/BoxShape3D.js +158 -0
  36. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts +11 -0
  37. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -1
  38. package/src/core/geom/3d/shape/CapsuleShape3D.js +12 -0
  39. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  40. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  41. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  42. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  43. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  44. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  45. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  46. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  47. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  48. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  49. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  50. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  51. package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts +37 -9
  52. package/src/core/geom/3d/shape/UnitCubeShape3D.d.ts.map +1 -1
  53. package/src/core/geom/3d/shape/UnitCubeShape3D.js +45 -98
  54. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts +10 -0
  55. package/src/core/geom/3d/shape/UnitSphereShape3D.d.ts.map +1 -1
  56. package/src/core/geom/3d/shape/UnitSphereShape3D.js +11 -0
  57. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  58. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  59. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  60. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.d.ts +61 -0
  61. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.d.ts.map +1 -0
  62. package/src/core/geom/3d/shape/util/shape3d_voxelize_to_grid.js +148 -0
  63. package/src/core/geom/3d/tetrahedra/compute_tetrahedral_mesh_from_surface.d.ts +39 -0
  64. package/src/core/geom/3d/tetrahedra/compute_tetrahedral_mesh_from_surface.d.ts.map +1 -0
  65. package/src/core/geom/3d/tetrahedra/compute_tetrahedral_mesh_from_surface.js +147 -0
  66. package/src/core/geom/3d/tetrahedra/compute_tetrahedron_quality.d.ts +15 -0
  67. package/src/core/geom/3d/tetrahedra/compute_tetrahedron_quality.d.ts.map +1 -0
  68. package/src/core/geom/3d/tetrahedra/compute_tetrahedron_quality.js +22 -0
  69. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.d.ts +2 -0
  70. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.d.ts.map +1 -0
  71. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +671 -0
  72. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  73. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  74. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  75. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.d.ts +26 -0
  76. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.d.ts.map +1 -0
  77. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.js +222 -0
  78. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_find_tets_around_edge.d.ts +34 -0
  79. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_find_tets_around_edge.d.ts.map +1 -0
  80. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_find_tets_around_edge.js +146 -0
  81. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.d.ts +36 -0
  82. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.d.ts.map +1 -0
  83. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_23.js +232 -0
  84. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.d.ts +33 -0
  85. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.d.ts.map +1 -0
  86. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_flip_32.js +255 -0
  87. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts +68 -0
  88. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -0
  89. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +387 -0
  90. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +35 -0
  91. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -0
  92. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +140 -0
  93. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +31 -0
  94. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -0
  95. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +97 -0
  96. package/src/core/geom/3d/tetrahedra/tetrahedron_compute_quality.d.ts +32 -0
  97. package/src/core/geom/3d/tetrahedra/tetrahedron_compute_quality.d.ts.map +1 -0
  98. package/src/core/geom/3d/tetrahedra/tetrahedron_compute_quality.js +66 -0
  99. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +41 -0
  100. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  101. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +124 -13
  102. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.d.ts +134 -0
  103. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.d.ts.map +1 -1
  104. package/src/core/geom/3d/topology/struct/binary/BinaryTopology.js +276 -3
  105. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.d.ts +17 -0
  106. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_close_boundary_holes.js +135 -0
  108. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compact.d.ts +14 -0
  109. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compact.d.ts.map +1 -0
  110. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compact.js +177 -0
  111. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_decouple.d.ts.map +1 -1
  112. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_face_decouple.js +20 -4
  113. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  114. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +5 -3
  115. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_create.d.ts.map +1 -1
  116. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_create.js +9 -0
  117. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_get_or_create.d.ts.map +1 -1
  118. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_get_or_create.js +21 -45
  119. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill.d.ts.map +1 -1
  120. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill.js +7 -1
  121. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.d.ts +8 -6
  122. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.d.ts.map +1 -1
  123. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_edge_kill_parallels.js +8 -6
  124. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_kill_short_edges.d.ts +22 -0
  125. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_kill_short_edges.d.ts.map +1 -0
  126. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_kill_short_edges.js +73 -0
  127. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.d.ts.map +1 -1
  128. package/src/core/geom/3d/topology/struct/binary/io/vertex/bt_vertex_replace.js +51 -1
  129. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.d.ts +10 -0
  130. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.d.ts.map +1 -0
  131. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_get.js +42 -0
  132. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.d.ts +28 -0
  133. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.d.ts.map +1 -0
  134. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_sample_interior_grid_points.js +227 -0
  135. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_walk_boundary_loops.d.ts +13 -0
  136. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_walk_boundary_loops.d.ts.map +1 -0
  137. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_walk_boundary_loops.js +108 -0
  138. package/src/core/geom/3d/topology/struct/binary/query/bt_query_edge_is_boundary.d.ts +11 -0
  139. package/src/core/geom/3d/topology/struct/binary/query/bt_query_edge_is_boundary.d.ts.map +1 -0
  140. package/src/core/geom/3d/topology/struct/binary/query/bt_query_edge_is_boundary.js +20 -0
  141. package/src/core/geom/3d/triangle/triangle_mesh_compute_signed_volume.d.ts +20 -0
  142. package/src/core/geom/3d/triangle/triangle_mesh_compute_signed_volume.d.ts.map +1 -0
  143. package/src/core/geom/3d/triangle/triangle_mesh_compute_signed_volume.js +38 -0
  144. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  145. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  146. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  147. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  148. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  149. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  150. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  151. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  152. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  153. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  154. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  155. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  156. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  157. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  158. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  159. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  160. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  161. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  162. package/src/core/graph/csr/CSRGraph.d.ts +168 -0
  163. package/src/core/graph/csr/CSRGraph.d.ts.map +1 -0
  164. package/src/core/graph/csr/CSRGraph.js +319 -0
  165. package/src/core/graph/metis/cluster_mesh_metis.d.ts +12 -0
  166. package/src/core/graph/metis/cluster_mesh_metis.d.ts.map +1 -1
  167. package/src/core/graph/metis/cluster_mesh_metis.js +12 -0
  168. package/src/core/graph/metis/metis.d.ts +19 -0
  169. package/src/core/graph/metis/metis.d.ts.map +1 -1
  170. package/src/core/graph/metis/metis.js +20 -0
  171. package/src/core/graph/metis/metis_cluster_bs.d.ts +11 -0
  172. package/src/core/graph/metis/metis_cluster_bs.d.ts.map +1 -1
  173. package/src/core/graph/metis/metis_cluster_bs.js +11 -0
  174. package/src/core/graph/metis/metis_options.d.ts +17 -2
  175. package/src/core/graph/metis/metis_options.d.ts.map +1 -1
  176. package/src/core/graph/metis/metis_options.js +17 -2
  177. package/src/core/graph/metis/native/MetisGraph.d.ts +144 -0
  178. package/src/core/graph/metis/native/MetisGraph.d.ts.map +1 -0
  179. package/src/core/graph/metis/native/MetisGraph.js +212 -0
  180. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts +72 -0
  181. package/src/core/graph/metis/native/bisection/BisectionScratch.d.ts.map +1 -0
  182. package/src/core/graph/metis/native/bisection/BisectionScratch.js +101 -0
  183. package/src/core/graph/metis/native/bisection/bisect_graph.d.ts +37 -0
  184. package/src/core/graph/metis/native/bisection/bisect_graph.d.ts.map +1 -0
  185. package/src/core/graph/metis/native/bisection/bisect_graph.js +100 -0
  186. package/src/core/graph/metis/native/bisection/compute_2way_params.d.ts +15 -0
  187. package/src/core/graph/metis/native/bisection/compute_2way_params.d.ts.map +1 -0
  188. package/src/core/graph/metis/native/bisection/compute_2way_params.js +84 -0
  189. package/src/core/graph/metis/native/bisection/fm_2way.d.ts +30 -0
  190. package/src/core/graph/metis/native/bisection/fm_2way.d.ts.map +1 -0
  191. package/src/core/graph/metis/native/bisection/fm_2way.js +290 -0
  192. package/src/core/graph/metis/native/bisection/grow_bisection.d.ts +23 -0
  193. package/src/core/graph/metis/native/bisection/grow_bisection.d.ts.map +1 -0
  194. package/src/core/graph/metis/native/bisection/grow_bisection.js +137 -0
  195. package/src/core/graph/metis/native/bisection/split_graph_two_way.d.ts +28 -0
  196. package/src/core/graph/metis/native/bisection/split_graph_two_way.d.ts.map +1 -0
  197. package/src/core/graph/metis/native/bisection/split_graph_two_way.js +119 -0
  198. package/src/core/graph/metis/native/coarsen/coarsen_graph.d.ts +20 -0
  199. package/src/core/graph/metis/native/coarsen/coarsen_graph.d.ts.map +1 -0
  200. package/src/core/graph/metis/native/coarsen/coarsen_graph.js +94 -0
  201. package/src/core/graph/metis/native/coarsen/create_coarse_graph.d.ts +24 -0
  202. package/src/core/graph/metis/native/coarsen/create_coarse_graph.d.ts.map +1 -0
  203. package/src/core/graph/metis/native/coarsen/create_coarse_graph.js +158 -0
  204. package/src/core/graph/metis/native/coarsen/match_shem.d.ts +41 -0
  205. package/src/core/graph/metis/native/coarsen/match_shem.d.ts.map +1 -0
  206. package/src/core/graph/metis/native/coarsen/match_shem.js +175 -0
  207. package/src/core/graph/metis/native/initial/initial_kway_bfs.d.ts +24 -0
  208. package/src/core/graph/metis/native/initial/initial_kway_bfs.d.ts.map +1 -0
  209. package/src/core/graph/metis/native/initial/initial_kway_bfs.js +122 -0
  210. package/src/core/graph/metis/native/initial/initial_kway_recursive_bisection.d.ts +29 -0
  211. package/src/core/graph/metis/native/initial/initial_kway_recursive_bisection.d.ts.map +1 -0
  212. package/src/core/graph/metis/native/initial/initial_kway_recursive_bisection.js +170 -0
  213. package/src/core/graph/metis/native/metis_partition_kway.d.ts +41 -0
  214. package/src/core/graph/metis/native/metis_partition_kway.d.ts.map +1 -0
  215. package/src/core/graph/metis/native/metis_partition_kway.js +126 -0
  216. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.d.ts +62 -0
  217. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.d.ts.map +1 -0
  218. package/src/core/graph/metis/native/refine/IndexedFloatMaxHeap.js +261 -0
  219. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts +45 -0
  220. package/src/core/graph/metis/native/refine/RefinementScratch.d.ts.map +1 -0
  221. package/src/core/graph/metis/native/refine/RefinementScratch.js +53 -0
  222. package/src/core/graph/metis/native/refine/compute_kway_params.d.ts +18 -0
  223. package/src/core/graph/metis/native/refine/compute_kway_params.d.ts.map +1 -0
  224. package/src/core/graph/metis/native/refine/compute_kway_params.js +138 -0
  225. package/src/core/graph/metis/native/refine/fm_kway.d.ts +63 -0
  226. package/src/core/graph/metis/native/refine/fm_kway.d.ts.map +1 -0
  227. package/src/core/graph/metis/native/refine/fm_kway.js +462 -0
  228. package/src/core/graph/metis/native/refine/project_kway.d.ts +22 -0
  229. package/src/core/graph/metis/native/refine/project_kway.d.ts.map +1 -0
  230. package/src/core/graph/metis/native/refine/project_kway.js +43 -0
  231. package/src/core/graph/metis/native/refine/refine_kway.d.ts +34 -0
  232. package/src/core/graph/metis/native/refine/refine_kway.d.ts.map +1 -0
  233. package/src/core/graph/metis/native/refine/refine_kway.js +43 -0
  234. package/src/core/math/linalg/eigen/matrix_householder_in_place.d.ts +2 -2
  235. package/src/core/math/linalg/eigen/matrix_householder_in_place.js +2 -2
  236. package/src/core/math/linalg/eigen/matrix_qr_in_place.d.ts +6 -4
  237. package/src/core/math/linalg/eigen/matrix_qr_in_place.d.ts.map +1 -1
  238. package/src/core/math/linalg/eigen/matrix_qr_in_place.js +69 -23
  239. package/src/engine/EngineHarness.d.ts +3 -1
  240. package/src/engine/EngineHarness.d.ts.map +1 -1
  241. package/src/engine/EngineHarness.js +3 -0
  242. package/src/engine/control/first-person/DESIGN.md +30 -6
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +563 -0
  244. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +115 -9
  245. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  246. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -176
  247. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +601 -8
  248. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  249. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +349 -8
  250. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +319 -23
  251. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  252. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -799
  253. package/src/engine/control/first-person/TODO.md +173 -0
  254. package/src/engine/control/first-person/abilities/Ability.d.ts +101 -0
  255. package/src/engine/control/first-person/abilities/Ability.d.ts.map +1 -0
  256. package/src/engine/control/first-person/abilities/Ability.js +119 -0
  257. package/src/engine/control/first-person/abilities/AbilitySet.d.ts +86 -0
  258. package/src/engine/control/first-person/abilities/AbilitySet.d.ts.map +1 -0
  259. package/src/engine/control/first-person/abilities/AbilitySet.js +185 -0
  260. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +62 -0
  261. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -0
  262. package/src/engine/control/first-person/abilities/LedgeGrab.js +199 -0
  263. package/src/engine/control/first-person/abilities/Mantle.d.ts +45 -0
  264. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -0
  265. package/src/engine/control/first-person/abilities/Mantle.js +188 -0
  266. package/src/engine/control/first-person/abilities/Slide.d.ts +33 -0
  267. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -0
  268. package/src/engine/control/first-person/abilities/Slide.js +166 -0
  269. package/src/engine/control/first-person/abilities/WallJump.d.ts +45 -0
  270. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -0
  271. package/src/engine/control/first-person/abilities/WallJump.js +131 -0
  272. package/src/engine/control/first-person/abilities/WallRun.d.ts +44 -0
  273. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -0
  274. package/src/engine/control/first-person/abilities/WallRun.js +180 -0
  275. package/src/engine/control/first-person/composer/EyeOffsetStack.d.ts +49 -0
  276. package/src/engine/control/first-person/composer/EyeOffsetStack.d.ts.map +1 -0
  277. package/src/engine/control/first-person/composer/EyeOffsetStack.js +60 -0
  278. package/src/engine/control/first-person/mastery/BreathRhythmEvaluator.d.ts +100 -0
  279. package/src/engine/control/first-person/mastery/BreathRhythmEvaluator.d.ts.map +1 -0
  280. package/src/engine/control/first-person/mastery/BreathRhythmEvaluator.js +133 -0
  281. package/src/engine/control/first-person/mastery/DecisionPoint.d.ts +10 -0
  282. package/src/engine/control/first-person/mastery/DecisionPoint.d.ts.map +1 -0
  283. package/src/engine/control/first-person/mastery/DecisionPoint.js +30 -0
  284. package/src/engine/control/first-person/mastery/FootAsymmetryTurnEvaluator.d.ts +61 -0
  285. package/src/engine/control/first-person/mastery/FootAsymmetryTurnEvaluator.d.ts.map +1 -0
  286. package/src/engine/control/first-person/mastery/FootAsymmetryTurnEvaluator.js +109 -0
  287. package/src/engine/control/first-person/mastery/MasteryEvaluator.d.ts +40 -0
  288. package/src/engine/control/first-person/mastery/MasteryEvaluator.d.ts.map +1 -0
  289. package/src/engine/control/first-person/mastery/MasteryEvaluator.js +45 -0
  290. package/src/engine/control/first-person/mastery/MasteryScore.d.ts +68 -0
  291. package/src/engine/control/first-person/mastery/MasteryScore.d.ts.map +1 -0
  292. package/src/engine/control/first-person/mastery/MasteryScore.js +100 -0
  293. package/src/engine/control/first-person/mastery/MasterySet.d.ts +60 -0
  294. package/src/engine/control/first-person/mastery/MasterySet.d.ts.map +1 -0
  295. package/src/engine/control/first-person/mastery/MasterySet.js +86 -0
  296. package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.d.ts +58 -0
  297. package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.d.ts.map +1 -0
  298. package/src/engine/control/first-person/mastery/SlideInitiationTimingEvaluator.js +83 -0
  299. package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.d.ts +69 -0
  300. package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.d.ts.map +1 -0
  301. package/src/engine/control/first-person/mastery/StrideTimingJumpEvaluator.js +109 -0
  302. package/src/engine/control/first-person/math/Spring.d.ts +56 -0
  303. package/src/engine/control/first-person/math/Spring.d.ts.map +1 -0
  304. package/src/engine/control/first-person/math/Spring.js +71 -0
  305. package/src/engine/control/first-person/math/computeLRCBreathRate.d.ts +26 -0
  306. package/src/engine/control/first-person/math/computeLRCBreathRate.d.ts.map +1 -0
  307. package/src/engine/control/first-person/math/computeLRCBreathRate.js +41 -0
  308. package/src/engine/control/first-person/math/computeMassRatios.d.ts +35 -0
  309. package/src/engine/control/first-person/math/computeMassRatios.d.ts.map +1 -0
  310. package/src/engine/control/first-person/math/computeMassRatios.js +44 -0
  311. package/src/engine/control/first-person/pose/FirstPersonPose.d.ts +31 -1
  312. package/src/engine/control/first-person/pose/FirstPersonPose.d.ts.map +1 -1
  313. package/src/engine/control/first-person/pose/FirstPersonPose.js +49 -3
  314. package/src/engine/control/first-person/pose/FirstPersonPosture.d.ts +7 -0
  315. package/src/engine/control/first-person/pose/FirstPersonPosture.d.ts.map +1 -0
  316. package/src/engine/control/first-person/pose/FirstPersonPosture.js +27 -0
  317. package/src/engine/control/first-person/prototype_first_person_controller.js +637 -120
  318. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +58 -0
  319. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -0
  320. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +77 -0
  321. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +80 -0
  322. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -0
  323. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +196 -0
  324. package/src/engine/control/first-person/test/buildTestPlayer.d.ts +20 -0
  325. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -0
  326. package/src/engine/control/first-person/test/buildTestPlayer.js +36 -0
  327. package/src/engine/graphics/camera/testClippingPlaneComputation.js +0 -2
  328. package/src/engine/graphics/ecs/light/Light.d.ts.map +1 -1
  329. package/src/engine/graphics/ecs/light/Light.js +27 -0
  330. package/src/engine/graphics/ecs/light/LightSystem.js +1 -1
  331. package/src/engine/graphics/ecs/path/PathDisplaySystem.d.ts.map +1 -1
  332. package/src/engine/graphics/ecs/path/testPathDisplaySystem.js +0 -2
  333. package/src/engine/graphics/ecs/path/tube/prototypeAnimatedPathMask.js +0 -2
  334. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  335. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  336. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  337. package/src/engine/graphics/render/buffer/buffers/prototypeNormalFrameBuffer.js +0 -2
  338. package/src/engine/graphics/render/forward_plus/plugin/ptototypeFPPlugin.js +0 -2
  339. package/src/engine/graphics/render/visibility/hiz/prototypeHiZ.js +0 -2
  340. package/src/engine/navigation/grid/find_path_on_grid_astar.d.ts.map +1 -1
  341. package/src/engine/navigation/grid/find_path_on_grid_astar.js +11 -2
  342. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  343. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +11 -1
  344. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  345. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  346. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  347. package/src/engine/physics/PLAN.md +461 -0
  348. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  349. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  350. package/src/engine/physics/body/BodyStorage.d.ts +187 -0
  351. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -0
  352. package/src/engine/physics/body/BodyStorage.js +427 -0
  353. package/src/engine/physics/broadphase/PairList.d.ts +62 -0
  354. package/src/engine/physics/broadphase/PairList.d.ts.map +1 -0
  355. package/src/engine/physics/broadphase/PairList.js +97 -0
  356. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts +16 -0
  357. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts.map +1 -0
  358. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +61 -0
  359. package/src/engine/physics/broadphase/generate_pairs.d.ts +38 -0
  360. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -0
  361. package/src/engine/physics/broadphase/generate_pairs.js +101 -0
  362. package/src/engine/physics/contact/ManifoldStore.d.ts +299 -0
  363. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -0
  364. package/src/engine/physics/contact/ManifoldStore.js +608 -0
  365. package/src/engine/physics/ecs/BodyKind.d.ts +23 -0
  366. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -0
  367. package/src/engine/physics/ecs/BodyKind.js +24 -0
  368. package/src/engine/physics/ecs/Collider.d.ts +98 -0
  369. package/src/engine/physics/ecs/Collider.d.ts.map +1 -0
  370. package/src/engine/physics/ecs/Collider.js +136 -0
  371. package/src/engine/physics/ecs/ColliderFlags.d.ts +14 -0
  372. package/src/engine/physics/ecs/ColliderFlags.d.ts.map +1 -0
  373. package/src/engine/physics/ecs/ColliderFlags.js +15 -0
  374. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +58 -0
  375. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -0
  376. package/src/engine/physics/ecs/ColliderObserverSystem.js +103 -0
  377. package/src/engine/physics/ecs/ColliderSerializationAdapter.d.ts +25 -0
  378. package/src/engine/physics/ecs/ColliderSerializationAdapter.d.ts.map +1 -0
  379. package/src/engine/physics/ecs/ColliderSerializationAdapter.js +37 -0
  380. package/src/engine/physics/ecs/PhysicsEvents.d.ts +15 -0
  381. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -0
  382. package/src/engine/physics/ecs/PhysicsEvents.js +16 -0
  383. package/src/engine/physics/ecs/PhysicsSystem.d.ts +628 -0
  384. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -0
  385. package/src/engine/physics/ecs/PhysicsSystem.js +1301 -0
  386. package/src/engine/physics/ecs/RigidBody.d.ts +197 -0
  387. package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -0
  388. package/src/engine/physics/ecs/RigidBody.js +240 -0
  389. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +21 -0
  390. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -0
  391. package/src/engine/physics/ecs/RigidBodyFlags.js +22 -0
  392. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts +28 -0
  393. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -0
  394. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +81 -0
  395. package/src/engine/physics/ecs/SleepState.d.ts +11 -0
  396. package/src/engine/physics/ecs/SleepState.d.ts.map +1 -0
  397. package/src/engine/physics/ecs/SleepState.js +12 -0
  398. package/src/engine/physics/events/ContactEventBuffer.d.ts +46 -0
  399. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -0
  400. package/src/engine/physics/events/ContactEventBuffer.js +83 -0
  401. package/src/engine/physics/events/diff_manifolds.d.ts +25 -0
  402. package/src/engine/physics/events/diff_manifolds.d.ts.map +1 -0
  403. package/src/engine/physics/events/diff_manifolds.js +50 -0
  404. package/src/engine/physics/fluid/FluidField.d.ts +294 -16
  405. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  406. package/src/engine/physics/fluid/FluidField.js +510 -66
  407. package/src/engine/physics/fluid/FluidSimulator.d.ts +188 -5
  408. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  409. package/src/engine/physics/fluid/FluidSimulator.js +456 -95
  410. package/src/engine/physics/fluid/SliceVisualiser.d.ts +29 -6
  411. package/src/engine/physics/fluid/SliceVisualiser.d.ts.map +1 -1
  412. package/src/engine/physics/fluid/SliceVisualiser.js +190 -165
  413. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts +154 -0
  414. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts.map +1 -0
  415. package/src/engine/physics/fluid/ecs/FluidComponent.js +238 -0
  416. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.d.ts +45 -0
  417. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.d.ts.map +1 -0
  418. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.js +89 -0
  419. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +107 -0
  420. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts.map +1 -0
  421. package/src/engine/physics/fluid/ecs/FluidSystem.js +278 -0
  422. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +62 -1
  423. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts.map +1 -1
  424. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +81 -6
  425. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts +17 -4
  426. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  427. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +105 -12
  428. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts +43 -0
  429. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -0
  430. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +210 -0
  431. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts +62 -1
  432. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  433. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +302 -8
  434. package/src/engine/physics/fluid/prototype.js +102 -91
  435. package/src/engine/physics/fluid/solver/optimal_sor_omega.d.ts +33 -0
  436. package/src/engine/physics/fluid/solver/optimal_sor_omega.d.ts.map +1 -0
  437. package/src/engine/physics/fluid/solver/optimal_sor_omega.js +41 -0
  438. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.d.ts +20 -5
  439. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.d.ts.map +1 -1
  440. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_forward.js +60 -38
  441. package/src/engine/physics/fluid/solver/v3_grid_apply_diffusion.d.ts +25 -4
  442. package/src/engine/physics/fluid/solver/v3_grid_apply_diffusion.d.ts.map +1 -1
  443. package/src/engine/physics/fluid/solver/v3_grid_apply_diffusion.js +93 -73
  444. package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.d.ts +23 -0
  445. package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.d.ts.map +1 -0
  446. package/src/engine/physics/fluid/solver/v3_grid_apply_scalar_advection.js +60 -0
  447. package/src/engine/physics/fluid/solver/v3_grid_compute_divergence.d.ts +23 -0
  448. package/src/engine/physics/fluid/solver/v3_grid_compute_divergence.d.ts.map +1 -0
  449. package/src/engine/physics/fluid/solver/v3_grid_compute_divergence.js +68 -0
  450. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +30 -0
  451. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -0
  452. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +66 -0
  453. package/src/engine/physics/fluid/solver/v3_grid_patch_edges_uniform.d.ts +26 -0
  454. package/src/engine/physics/fluid/solver/v3_grid_patch_edges_uniform.d.ts.map +1 -0
  455. package/src/engine/physics/fluid/solver/v3_grid_patch_edges_uniform.js +113 -0
  456. package/src/engine/physics/fluid/solver/v3_grid_shift_in_place.d.ts +30 -0
  457. package/src/engine/physics/fluid/solver/v3_grid_shift_in_place.d.ts.map +1 -0
  458. package/src/engine/physics/fluid/solver/v3_grid_shift_in_place.js +107 -0
  459. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +49 -0
  460. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -0
  461. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +126 -0
  462. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +93 -0
  463. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -0
  464. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +424 -0
  465. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +48 -0
  466. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -0
  467. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +92 -0
  468. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  469. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  470. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +76 -32
  471. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  472. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  473. package/src/engine/physics/gjk/gjk.js +421 -378
  474. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  475. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  476. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  477. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  478. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  479. package/src/engine/physics/gjk/mpr.js +344 -0
  480. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +44 -0
  481. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -0
  482. package/src/engine/physics/inertia/world_inverse_inertia.js +77 -0
  483. package/src/engine/physics/integration/integrate_position.d.ts +34 -0
  484. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -0
  485. package/src/engine/physics/integration/integrate_position.js +79 -0
  486. package/src/engine/physics/integration/integrate_velocity.d.ts +55 -0
  487. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -0
  488. package/src/engine/physics/integration/integrate_velocity.js +160 -0
  489. package/src/engine/physics/integration/quat_integrate.d.ts +27 -0
  490. package/src/engine/physics/integration/quat_integrate.d.ts.map +1 -0
  491. package/src/engine/physics/integration/quat_integrate.js +62 -0
  492. package/src/engine/physics/island/IslandBuilder.d.ts +167 -0
  493. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -0
  494. package/src/engine/physics/island/IslandBuilder.js +411 -0
  495. package/src/engine/physics/island/union_find.d.ts +51 -0
  496. package/src/engine/physics/island/union_find.d.ts.map +1 -0
  497. package/src/engine/physics/island/union_find.js +76 -0
  498. package/src/engine/physics/narrowphase/PosedShape.d.ts +51 -0
  499. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -0
  500. package/src/engine/physics/narrowphase/PosedShape.js +108 -0
  501. package/src/engine/physics/narrowphase/box_box_manifold.d.ts +32 -0
  502. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -0
  503. package/src/engine/physics/narrowphase/box_box_manifold.js +639 -0
  504. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  505. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  506. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  507. package/src/engine/physics/narrowphase/capsule_contacts.d.ts +122 -0
  508. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -0
  509. package/src/engine/physics/narrowphase/capsule_contacts.js +462 -0
  510. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  511. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  512. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  513. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  514. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  515. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  516. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  517. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  518. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  519. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  520. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  521. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  522. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  523. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  524. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  525. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  526. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  527. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  528. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  529. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  530. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  531. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +17 -0
  532. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -0
  533. package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -0
  534. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts +38 -0
  535. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -0
  536. package/src/engine/physics/narrowphase/sphere_box_contact.js +123 -0
  537. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts +26 -0
  538. package/src/engine/physics/narrowphase/sphere_sphere_contact.d.ts.map +1 -0
  539. package/src/engine/physics/narrowphase/sphere_sphere_contact.js +51 -0
  540. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  541. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  542. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  543. package/src/engine/physics/queries/PhysicsSurfacePoint.d.ts +83 -0
  544. package/src/engine/physics/queries/PhysicsSurfacePoint.d.ts.map +1 -0
  545. package/src/engine/physics/queries/PhysicsSurfacePoint.js +100 -0
  546. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  547. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  548. package/src/engine/physics/queries/overlap_shape.js +183 -0
  549. package/src/engine/physics/queries/raycast.d.ts +20 -0
  550. package/src/engine/physics/queries/raycast.d.ts.map +1 -0
  551. package/src/engine/physics/queries/raycast.js +249 -0
  552. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  553. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  554. package/src/engine/physics/queries/shape_cast.js +387 -0
  555. package/src/engine/physics/solver/friction_cone.d.ts +16 -0
  556. package/src/engine/physics/solver/friction_cone.d.ts.map +1 -0
  557. package/src/engine/physics/solver/friction_cone.js +37 -0
  558. package/src/engine/physics/solver/solve_contacts.d.ts +122 -0
  559. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -0
  560. package/src/engine/physics/solver/solve_contacts.js +1016 -0
  561. package/src/core/geom/3d/topology/struct/binary/io/edge/OrderedEdge.d.ts +0 -34
  562. package/src/core/geom/3d/topology/struct/binary/io/edge/OrderedEdge.d.ts.map +0 -1
  563. package/src/core/geom/3d/topology/struct/binary/io/edge/OrderedEdge.js +0 -66
  564. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_calc_edges.d.ts +0 -2
  565. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_calc_edges.d.ts.map +0 -1
  566. package/src/core/geom/3d/topology/struct/binary/io/edge/bt_mesh_calc_edges.js +0 -54
  567. package/src/core/geom/3d/topology/struct/binary/io/edge/get_or_create_edge_map.d.ts +0 -2
  568. package/src/core/geom/3d/topology/struct/binary/io/edge/get_or_create_edge_map.d.ts.map +0 -1
  569. package/src/core/geom/3d/topology/struct/binary/io/edge/get_or_create_edge_map.js +0 -26
  570. package/src/engine/ecs/components/Motion.d.ts +0 -21
  571. package/src/engine/ecs/components/Motion.d.ts.map +0 -1
  572. package/src/engine/ecs/components/Motion.js +0 -27
  573. package/src/engine/ecs/components/MotionSerializationAdapter.d.ts +0 -20
  574. package/src/engine/ecs/components/MotionSerializationAdapter.d.ts.map +0 -1
  575. package/src/engine/ecs/components/MotionSerializationAdapter.js +0 -26
  576. package/src/engine/ecs/systems/MotionSystem.d.ts +0 -9
  577. package/src/engine/ecs/systems/MotionSystem.d.ts.map +0 -1
  578. package/src/engine/ecs/systems/MotionSystem.js +0 -29
  579. package/src/engine/physics/fluid/Fluid.d.ts +0 -26
  580. package/src/engine/physics/fluid/Fluid.d.ts.map +0 -1
  581. package/src/engine/physics/fluid/Fluid.js +0 -221
  582. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_reverse.d.ts +0 -7
  583. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_reverse.d.ts.map +0 -1
  584. package/src/engine/physics/fluid/solver/v3_grid_apply_advection_reverse.js +0 -8
@@ -0,0 +1,1300 @@
1
+ # Physics Engine Review: meep vs. cannon-es
2
+
3
+ A deep technical comparison of the meep in-house rigid-body engine
4
+ (`H:/git/moh/app/src/mir-engine/meep/src/engine/physics/`) against
5
+ **cannon-es** (https://github.com/pmndrs/cannon-es), the
6
+ community-maintained fork of Stefan Hedman's **cannon.js**
7
+ (https://github.com/schteppe/cannon.js).
8
+
9
+ Cannon is uniquely interesting as a reference because it shares every
10
+ hard constraint we work under: **pure JavaScript, single-threaded, no
11
+ SIMD, no SharedArrayBuffer, GC-sensitive hot paths**. Anywhere Cannon
12
+ discovered a JS-specific workaround, it translates directly to us.
13
+ Anywhere we beat Cannon, it's a fair fight on engineering merit — not
14
+ "they had SIMD intrinsics and we didn't".
15
+
16
+ This review respects PLAN.md's documented out-of-scope decisions (pure
17
+ JS, no SAB, no multi-threaded solver, action-log netcode). Cannon
18
+ citations use `src/<dir>/<File>.ts` paths; our citations use
19
+ `path:line` relative to the package root.
20
+
21
+ ---
22
+
23
+ ## 1. Overall Architecture
24
+
25
+ ### Pipeline shape
26
+
27
+ | Stage | meep `PhysicsSystem.fixedUpdate` (`ecs/PhysicsSystem.js:1075`) | cannon-es `World.internalStep` (`src/world/World.ts`) |
28
+ |---|---|---|
29
+ | Apply gravity | Folded into Stage 1 `integrate_velocity` (`integration/integrate_velocity.js`) | Standalone "gravity" loop over all bodies |
30
+ | Forces from user constraints | n/a (no joints in v1) | `for (const c of constraints) c.update()` adds equations |
31
+ | Subsystem callbacks | n/a | `subsystems` user-extension hook |
32
+ | Velocity integration | Stage 1, awake-only | Last (after solver); leap-frog |
33
+ | Broadphase refit | Stage 2, awake-only, fat-AABB `compute_fat_world_aabb.js` | Per-body `updateAABB()` inside `broadphase.collisionPairs` |
34
+ | Pair generation | Stage 3, `generate_pairs.js` — leaf query into both BVHs, dedup via touched bit | `broadphase.collisionPairs(this, p1, p2)` |
35
+ | Wake propagation | Stage 4, explicit `__wake_pairs` over the pair list | Implicit — narrowphase sets `wakeUpAfterNarrowphase = true` when relative speed exceeds threshold |
36
+ | Narrowphase | Stage 5, `narrowphase_step.js` cascade `if (a_kind && b_kind) ...` | `Narrowphase.getContacts(p1, p2, ...)` giant switch dispatch |
37
+ | Island build | Stage 6, `IslandBuilder` union-find CSR | Lazy in `SplitSolver` (BFS over equation graph), or no partitioning if using bare `GSSolver` |
38
+ | Solver | Stage 7, `solve_contacts.js` per island, 10 iters | `solver.solve(dt, this)` over the flat equation list |
39
+ | Position integration | Stage 8 | Same as velocity (leap-frog `pos += velo * dt`) |
40
+ | Sleep | Stage 9, per-island atomic | Stage 14, per-body `body.sleepTick(time)` |
41
+ | Damping | Folded into integrators | Stage 11, explicit multiplicative decay |
42
+ | Event diff | Stage 10, `diff_manifolds` → Begin/Stay/End | `collisionMatrix` flip + `collide`/`endContact` events from narrowphase |
43
+ | End-of-step | Stage 11, `manifolds.advance_frame()` (roll touched, evict grace-expired) | `clearForces()` — accumulators wiped |
44
+
45
+ The single most consequential structural divergence is **what each step
46
+ iterates over**. Cannon's documented `internalStep` walks **every body**
47
+ on every pass (`for (let i = 0; i !== N; i++) { const bi = bodies[i] }`
48
+ appears repeatedly in `World.ts`), with sleep state being a property
49
+ checked inline rather than a filter. We instead iterate
50
+ `storage.awake_at(i)` for `i in [0, awake_count)` — the awake set is a
51
+ dense `Uint32Array` (`body/BodyStorage.js:101-110`) and grows / shrinks
52
+ via swap-with-last (`body/BodyStorage.js:330-337`). At million-body
53
+ scale where 99% of bodies are mostly sleeping (the design target in
54
+ PLAN.md), we pay O(awake) per stage while Cannon pays O(total). This
55
+ is **the** scalability story between the two engines.
56
+
57
+ ### Data layout — body pool
58
+
59
+ | Aspect | meep `BodyStorage` | cannon-es `Body` |
60
+ |---|---|---|
61
+ | ID encoding | 24-bit index ǁ 8-bit generation (`body/BodyStorage.js:11-19`) | Bare object reference (`Body` instance) + monotonically increasing `id` |
62
+ | Slot reuse policy | Min-heap of free indices for deterministic reuse (`body/BodyStorage.js:154-176`, `383-426`) | No reuse — destroyed bodies are GC'd |
63
+ | Active list | Dense `Uint32Array __awake_list` + reverse map `__awake_pos` (`body/BodyStorage.js:101-103`) | None — bodies live in `World.bodies: Body[]` regardless of sleep state |
64
+ | Body state storage | SoA — `__entities`, `__generations`, `__kinds`, `__flags` as parallel typed arrays. Hot mutable state (velocity / accumulators) on `RigidBody` instances | AoS — every field on the `Body` class instance (`position: Vec3`, `velocity: Vec3`, `force: Vec3`, `torque: Vec3`, `sleepState: number`, etc.) |
65
+ | Iteration | `for (let i = 0; i < awake_count; i++) { const idx = awake_at(i); const rb = __bodies[idx]; ... }` | `for (let i = 0; i !== N; i++) { const bi = bodies[i]; if (bi.sleepState === Body.SLEEPING) continue; ... }` |
66
+ | Generation safety | Stale packed id detected via `is_valid()` mask + generation check (`body/BodyStorage.js:210-219`) | Stale references possible (caller is responsible) |
67
+
68
+ **Where we win:** SoA identity tables + awake-list filtering. Hot
69
+ streaming reads of cold flags / kinds / generations don't pull
70
+ unrelated body fields into cache, and disconnected sleeping bodies
71
+ contribute literally zero work per step. Cannon's per-body
72
+ `sleepState` check is cheap-but-not-free, and on a scene of 1M static
73
+ chairs in a town the difference is dramatic.
74
+
75
+ **Where Cannon wins (or rather, "is more idiomatic"):** Cannon's
76
+ `Body` is a single class with every field on it. V8 monomorphises
77
+ this beautifully — every `body.velocity` access is the same hidden
78
+ class lookup. There's no parallel-array index arithmetic, no
79
+ `body_id_index(packed)` mask-and-shift. Debugging is plain
80
+ object inspection. Our split (RigidBody for the JS-side hot state,
81
+ BodyStorage for identity SoA, Collider list as a separate per-body
82
+ array, transforms on a Transform component) costs a few extra
83
+ indirections per body in the solver inner loop — but those are array
84
+ indexed loads, not pointer-chases, so still cache-friendly.
85
+
86
+ The crucial nuance: **even Cannon's `Body` AoS is "AoS per body but
87
+ cache-unfriendly per pass"**. When the integrator wants only
88
+ `position`, `velocity`, and `force`, it still pulls all 25-ish other
89
+ fields of `Body` into cache. Pure SoA (one `Float64Array` per
90
+ attribute) would be better, but we don't go fully SoA on the hot
91
+ fields either — `RigidBody.linearVelocity` is a `Vector3` extending
92
+ `Float64Array`, accessed as `lv[0]/[1]/[2]` (`solver/solve_contacts.js:427`).
93
+ Our SoA win is therefore narrower than the layout suggests; the
94
+ honest comparison is "we got the identity / scheduling pieces into
95
+ SoA, the per-body kinematic state is still AoS like Cannon's, just
96
+ with typed-array storage". The Jolt/Bullet style of fully SoA kinematic
97
+ state remains a future optimisation.
98
+
99
+ ### Broadphase
100
+
101
+ | Engine | Strategies | Default |
102
+ |---|---|---|
103
+ | meep | One: BVH × 2 (`staticBvh` + `dynamicBvh`), with fat-AABB refit (`broadphase/compute_fat_world_aabb.js`) | The only choice |
104
+ | cannon-es | Three: `NaiveBroadphase` (O(N²)), `GridBroadphase` (uniform grid, spheres+planes only), `SAPBroadphase` (single-axis sweep & prune) | `NaiveBroadphase` |
105
+
106
+ `NaiveBroadphase.collisionPairs` is literally:
107
+ ```js
108
+ for (let i = 0; i !== n; i++) {
109
+ for (let j = 0; j !== i; j++) {
110
+ if (!this.needBroadphaseCollision(bi, bj)) continue;
111
+ this.intersectionTest(bi, bj, pairs1, pairs2);
112
+ }
113
+ }
114
+ ```
115
+ The class docstring even concedes the algorithm's complexity is "N²
116
+ (which is bad)". Cannon's expectation is that users will swap in
117
+ `SAPBroadphase` for scenes with more than a few hundred bodies. SAP
118
+ uses insertion-sort on one axis (chosen via `autoDetectAxis()` —
119
+ variance maximisation), which handles small frame-to-frame motion in
120
+ ~O(N) but degrades to O(N²) on sudden mass motion. Cannon makes no
121
+ attempt at a tree.
122
+
123
+ **Our BVH split with awake-list-driven pair generation is
124
+ unambiguously better at our target scale.** At 1M bodies the
125
+ NaiveBroadphase is 10¹² pair checks per frame — completely
126
+ unusable. Our `generate_pairs.js:50-99` does one BVH query per
127
+ awake-leaf into each of two BVHs and dedups via the manifold
128
+ touched flag, scaling O(awake · log(total)) at worst. This is a
129
+ *million-fold* difference at the design-target scale.
130
+
131
+ ### Island structure
132
+
133
+ Cannon ships **`SplitSolver`** as an opt-in wrapper around `GSSolver`.
134
+ On each `solve()` call it builds a graph (every body is a node, every
135
+ equation an edge), runs BFS to find connected components, then calls
136
+ the inner solver per island. Each rebuild allocates fresh `nodes`,
137
+ `children`, and intermediate arrays — there's an internal pool but
138
+ graph-shape changes between frames defeat it. Cannon's island
139
+ construction is also coupled to the solver, not to the contact
140
+ generator, so it's purely a solver-side optimisation; sleep and
141
+ events use unrelated machinery (per-body sleep tick, per-pair
142
+ narrowphase-driven events).
143
+
144
+ We pull islands up to a first-class pipeline stage with `IslandBuilder`
145
+ (`island/IslandBuilder.js`) producing CSR-shaped output that the
146
+ solver and sleep test *both* consume. Bodies are sorted ascending
147
+ within an island and islands ascending by root index, giving us
148
+ **determinism by construction** without sorting at the solver
149
+ boundary. The PLAN.md atomic-sleep + atomic-wake story (sleep groups
150
+ threaded via `sleep_group_next/prev` so waking any member walks the
151
+ chain) is structurally impossible in Cannon's per-body model — see §3.
152
+
153
+ ### Solver
154
+
155
+ Cannon's `GSSolver` is a beautifully general sequential-impulse
156
+ solver over a flat equation list (contacts, friction, joints all the
157
+ same shape: an `Equation` with `computeB`, `computeC`, lambda
158
+ bounds). It implements the **SPOOK formulation** (Lacoursière /
159
+ Erleben) of constraint regularisation: `a = 4/(h*(1+4*d))`, `b =
160
+ 4d/(1+4d)`, `eps = 4/(h*h*k*(1+4*d))` parameterised by stiffness `k`
161
+ and relaxation `d` (`src/equations/Equation.ts`). Per-iteration:
162
+
163
+ ```js
164
+ deltalambda = invC * (B - GWlambda - c.eps * lambdaj);
165
+ ```
166
+ The `c.eps * lambdaj` term is what makes SPOOK different from naive
167
+ Baumgarte — it's **continuous regularisation**, applied at every
168
+ iteration to soften the constraint, rather than a one-shot velocity
169
+ bias. Defaults: `stiffness = 1e7, relaxation = 4, iterations = 10`.
170
+
171
+ Our `solve_contacts.js` is a more specialised contacts-only sequential
172
+ impulse solver: warm-start replay (`solver/solve_contacts.js:466-477`),
173
+ Baumgarte position correction folded into the velocity bias
174
+ (`solver/solve_contacts.js:442-450`), Coulomb friction with disk-clamp
175
+ (`solver/solve_contacts.js:566-571`), per-island iteration
176
+ (`solver/solve_contacts.js:308-313`). 10 iterations default
177
+ (matches Cannon). The split-impulse / split-position-pass story is
178
+ discussed in §3.
179
+
180
+ ### Sleep system
181
+
182
+ Cannon's per-body sleep is the chatter-prone classic algorithm.
183
+ `Body.sleepTick(time)` (verbatim from `src/objects/Body.ts`):
184
+ ```js
185
+ if (this.allowSleep) {
186
+ const sleepState = this.sleepState;
187
+ const speedSquared = velocity² + angularVelocity²;
188
+ const speedLimitSquared = this.sleepSpeedLimit ** 2;
189
+ if (sleepState === Body.AWAKE && speedSquared < speedLimitSquared) {
190
+ this.sleepState = Body.SLEEPY;
191
+ this.timeLastSleepy = time;
192
+ } else if (sleepState === Body.SLEEPY && speedSquared > speedLimitSquared) {
193
+ this.wakeUp();
194
+ } else if (sleepState === Body.SLEEPY && time - this.timeLastSleepy > this.sleepTimeLimit) {
195
+ this.sleep();
196
+ }
197
+ }
198
+ ```
199
+ This is the failure mode that **specifically motivates** our atomic
200
+ sleep: in a stack of 100 blocks, frame N the bottom block falls
201
+ asleep, frame N+1 a top block's micro-jitter wakes a middle block via
202
+ broadphase pair propagation, frame N+2 the wake cascades down,
203
+ frame N+3 the bottom re-wakes. The whole stack chatters indefinitely.
204
+ PLAN.md "Sleep + events" calls this out explicitly:
205
+
206
+ > "Per-island atomic sleep: an island sleeps when `max(|v|² + |ω|²)`
207
+ > across all members stays below the threshold long enough; the whole
208
+ > island sleeps in the same frame. Replaces the per-body chatter on
209
+ > weakly-connected piles."
210
+
211
+ Our `__sleep_test` (`ecs/PhysicsSystem.js:942-1006`) walks each
212
+ island, takes `max(v² + ω²)` over all members, increments every
213
+ member's timer if-and-only-if the **island as a whole** is below
214
+ threshold, and atomically sleeps all members the same frame
215
+ (`__atomic_sleep_island_range` at `:754-786`). The dual atomic-wake
216
+ walks the `sleep_group_next` circular DLL (`:705-737`).
217
+
218
+ Cannon has no equivalent. The `wakeUpAfterNarrowphase` flag is a
219
+ one-frame deferral, not an island-aware operation.
220
+
221
+ ### Threading
222
+
223
+ Both single-threaded by deliberate design.
224
+
225
+ ---
226
+
227
+ ## 2. Specific Algorithms and Tradeoffs
228
+
229
+ ### Body storage — class instances vs SoA + stable IDs
230
+
231
+ Cannon's `Body` declares ~30 mutable public fields including
232
+ `position`, `velocity`, `quaternion`, `angularVelocity`, `force`,
233
+ `torque`, `mass`, `linearDamping`, `angularDamping`, `linearFactor`,
234
+ `angularFactor`, `sleepState`, `allowSleep`, `collisionFilterGroup`,
235
+ `collisionFilterMask`, `collisionResponse`, `isTrigger`, `shapes`,
236
+ `shapeOffsets`, `shapeOrientations`, `aabb`, `boundingRadius`,
237
+ `inertia`, `invInertia`, `invInertiaWorld`. They're set in the
238
+ constructor with `||` default coalescing — typical Cannon idiom is
239
+ `this.mass = typeof options.mass === 'number' ? options.mass : 0`.
240
+ V8 monomorphises `Body` instances aggressively because the
241
+ initialisation order is fixed and every field gets a typed default.
242
+
243
+ Our split:
244
+ - `RigidBody` (`ecs/RigidBody.js`) — JS-side hot state (velocity,
245
+ accumulators, damping, mass, gravity scale, sleep state). Class
246
+ fields declared at class-body scope (`= new Vector3(0,0,0)` etc.),
247
+ same V8 hidden-class win.
248
+ - `BodyStorage` — identity SoA (`__entities`, `__generations`,
249
+ `__kinds`, `__flags`, `__alive` as parallel typed arrays) and
250
+ scheduling (`__awake_list`).
251
+ - `Collider` (component, attached separately, possibly compound).
252
+ - `Transform` (independent, shared with rendering / IK / etc.).
253
+
254
+ **GC pressure.** Cannon allocates one `Body` per body, plus the
255
+ `Vec3` / `Quaternion` instance for each field. A scene of 100k bodies
256
+ is ~100k bodies × ~10 Vec3/Quat ≈ 1M small objects on the heap, all
257
+ of which the GC has to walk on every major collection. We allocate
258
+ ~100k bodies × 4 Vector3s (linear/angular vel, accumulated
259
+ force/torque, inertia) ≈ 400k small objects — better, but still on
260
+ the heap. Where we materially win is the **per-step work**: our SoA
261
+ identity tables are TypedArray slabs that don't appear in GC
262
+ traversal at all, and we don't allocate per-step.
263
+
264
+ **Iteration speed.** For the solver inner loop the comparison is a
265
+ wash: both engines access `rb.linearVelocity[0..2]` (us, since
266
+ Vector3 extends Float64Array) or `body.velocity.x/y/z` (Cannon). V8
267
+ should compile both to the same machine code. The difference appears
268
+ at the broadphase / wake phase, where Cannon's per-body iteration
269
+ pulls the whole `Body` into cache for a `sleepState` check while ours
270
+ reads a single byte from `__alive` / `__awake_pos` and jumps over the
271
+ slot.
272
+
273
+ **Observability.** Cannon wins here — `console.log(world.bodies[42])`
274
+ shows you everything. Our `system.__bodies[idx]` shows RigidBody but
275
+ not its colliders, transform, or storage entry; you have to know to
276
+ look in three places. This is the cost of decomposition; it's a
277
+ trade we deliberately made for ECS clarity and we shouldn't undo it.
278
+ But adding a `dump(body_idx)` helper that aggregates the four
279
+ sources would be cheap and would close the gap.
280
+
281
+ ### Broadphase
282
+
283
+ Already covered above; the headline is that **Cannon ships three
284
+ broadphases and defaults to the worst one**. The `NaiveBroadphase`
285
+ default exists for tutorial-grade simplicity ("first 50 bodies just
286
+ work, then read the docs"). For a real physics engine `SAPBroadphase`
287
+ is the standard choice and it's still single-axis SAP, which is
288
+ state-of-the-art-1990s.
289
+
290
+ A subtle point worth flagging: cannon-es's `SAPBroadphase` uses
291
+ event listeners on the World (`world.addEventListener('addBody',
292
+ ...)`) to keep its `axisList` in sync. This is allocation-free per
293
+ step but has a hidden cost — the `addBody` / `removeBody` events fire
294
+ synchronously on dataset mutations, and adding / removing many bodies
295
+ mid-step is **explicitly fragile in Cannon**. We sidestep this
296
+ entirely because our broadphase reads `storage.awake_at(i)` directly
297
+ and the BVH allocates/frees its own nodes on
298
+ `attach_collider`/`detach_collider`. No event-listener indirection.
299
+
300
+ ### Narrowphase pairs — SAT vs GJK/EPA + closed-form
301
+
302
+ This is the **deepest structural divergence**. Cannon has
303
+ **no GJK and no EPA**. Its entire narrowphase is built on:
304
+
305
+ 1. Closed-form per-pair handlers (`sphereSphere`, `spherePlane`,
306
+ `sphereBox`, `boxBox`, etc. — a large method-per-pair switch on
307
+ `Narrowphase.getContacts`)
308
+ 2. **SAT** for arbitrary `ConvexPolyhedron` shapes (face normals + edge
309
+ crosses), followed by
310
+ 3. **Sutherland-Hodgman clipping** of the incident face against the
311
+ reference face's bounding planes to extract a multi-point manifold
312
+
313
+ Our cascade in `narrowphase/narrowphase_step.js:296-782`:
314
+
315
+ | Pair | Path | Cannon equivalent |
316
+ |---|---|---|
317
+ | sphere-sphere | `sphere_sphere_contact` closed-form (`:309-325`) | `Narrowphase.sphereSphere` |
318
+ | sphere-box | `sphere_box_contact` closed-form, handles centre-inside-box (`:328-359`) | `Narrowphase.sphereBox` |
319
+ | capsule-X | `capsule_*` closed-form family (`:397-483`) | Cannon has no capsule shape |
320
+ | box-box | SAT + Sutherland-Hodgman in `box_box_manifold.js` (`:362-394`) | `ConvexPolyhedron` via `Narrowphase.convexConvex` — same algorithm, more general implementation |
321
+ | convex × concave (heightmap/mesh) | Per-triangle GJK+EPA via decomposition dispatcher (`:498-713`) | `convexTrimesh` / `convexHeightfield` — Trimesh BVH-queries triangles, each treated as a `ConvexPolyhedron` for SAT |
322
+ | anything else convex | GJK + EPA fallback (`:715-781`) | **No fallback exists** — must be `ConvexPolyhedron` with explicit faces |
323
+
324
+ **The structural tradeoff:** SAT works only on shapes with explicit
325
+ face / edge lists. Cannon can't represent a smooth shape (sphere is
326
+ its own special case; capsule, cylinder, cone, ellipsoid, generic
327
+ convex-hull-from-point-cloud — all impossible without an explicit
328
+ polyhedral mesh). We can, via GJK on the support function. But our
329
+ EPA degenerates on smooth shapes (PLAN.md "Limitations / Known
330
+ caveats") so the *practical* shape coverage of the two engines is
331
+ closer than the algorithm choice suggests.
332
+
333
+ **Cannon's `ConvexPolyhedron.clipFaceAgainstHull`** is its quality
334
+ crown jewel for convex-convex contacts. The pipeline:
335
+
336
+ 1. `findSeparatingAxis` iterates face normals of A, then face normals
337
+ of B, then 9 (or `uniqueAxesA × uniqueAxesB`) edge-cross axes.
338
+ Tracks the minimum penetration depth across all axes. Returns the
339
+ normal of the deepest axis or `false` (no overlap).
340
+ 2. Choose the witness face on B whose normal is most parallel to the
341
+ separating normal — this is the *incident face*.
342
+ 3. Clip the incident face polygon against every neighbouring face of
343
+ A's witness face, treated as a half-space. After all clips, the
344
+ surviving vertices that lie *behind* the witness face become
345
+ contact points. Snippet (paraphrased from the source):
346
+
347
+ ```js
348
+ for (let i = 0; i < numVerticesA; i++) {
349
+ localPlaneNormal.copy(this.faceNormals[otherFace]);
350
+ this.clipFaceAgainstPlane(pVtxIn, pVtxOut, planeNormalWS, planeEqWS);
351
+ // swap buffers
352
+ }
353
+ // finally:
354
+ for (let i = 0; i < pVtxIn.length; i++) {
355
+ let depth = planeNormalWS.dot(pVtxIn[i]) + planeEqWS;
356
+ if (depth <= maxDist && depth <= 1e-6) {
357
+ result.push({ point, normal, depth });
358
+ }
359
+ }
360
+ ```
361
+
362
+ Our `box_box_manifold.js` does exactly the same thing but specialised
363
+ to two boxes (3 face normals × 2 + 9 edge crosses = 15 axes, hard-coded).
364
+ The result is 0..4 contact points per box-box pair, same as Cannon.
365
+ **The major gap:** we have no general convex-hull shape, only `BoxShape3D`.
366
+ If we add `ConvexHullShape3D` (on PLAN.md backlog), we'll want
367
+ basically Cannon's `ConvexPolyhedron.clipFaceAgainstHull` ported. The
368
+ algorithm is already in our codebase as the inner clipping pass of
369
+ `box_box_manifold.js`; the missing piece is the SAT outer loop
370
+ that iterates an arbitrary face/edge list rather than the hard-coded 15.
371
+
372
+ ### GJK / EPA — what each engine can actually handle
373
+
374
+ | Shape pair | meep | cannon-es |
375
+ |---|---|---|
376
+ | sphere–sphere | closed-form | closed-form |
377
+ | sphere–box | closed-form (interior-handling) | closed-form |
378
+ | sphere–plane | closed-form (via box flat path) | closed-form |
379
+ | sphere–convex polyhedron | GJK + EPA (works) | `sphereConvex` via vertex-iteration; works for explicit hulls |
380
+ | sphere–trimesh | per-triangle GJK + EPA via decomp | `sphereTrimesh` via BVH triangle query |
381
+ | capsule–anything | closed-form for cap-sphere / cap-cap / cap-box | n/a (no capsule shape) |
382
+ | convex–convex | GJK + EPA fallback (may degenerate on smooth) | SAT + clip via `ConvexPolyhedron` |
383
+ | convex–trimesh | per-triangle GJK + EPA (PLAN.md "drop and settle" tests skipped) | per-triangle SAT + clip (more reliable for polyhedral cases) |
384
+ | concave–concave | refused | not supported |
385
+ | anything smooth | GJK works for overlap test; EPA degenerates on depth recovery | impossible — needs polyhedral approximation |
386
+
387
+ The structural picture is: **Cannon trades expressivity for
388
+ robustness on its supported shape set**. We trade robustness on
389
+ smooth shapes for general support-function-based collision. The cost
390
+ of our choice is the EPA-on-smooth-shapes failure mode, which we
391
+ mitigate by routing all common pairs through closed-form handlers.
392
+ PLAN.md "Limitations / Known caveats" calls this out:
393
+
394
+ > "EPA on smooth shapes: degenerates (no flat face to converge on).
395
+ > Mitigated by closed-form paths for sphere/cube/capsule pairs; exotic
396
+ > convex shapes vs spheres can still fail."
397
+
398
+ ### MPR (Minkowski Portal Refinement)
399
+
400
+ Cannon has nothing comparable. We have `gjk/mpr.js` (XenoCollide,
401
+ Snethen GDC 2009) but it's not yet wired into `narrowphase_step` —
402
+ PLAN.md "Alternative narrowphase: MPR" calls it a candidate for
403
+ falling back when EPA stalls. **Wiring it as the fallback for
404
+ EPA non-convergence is a clear next step** — concrete payoff: torus-knot
405
+ test (currently skipped), ball-on-curved-surface contact stability.
406
+
407
+ ### Solver — SPOOK vs Baumgarte
408
+
409
+ Cannon's solver is built on the **SPOOK formulation** described in
410
+ Lacoursière's PhD thesis and Erleben's papers. The key equations
411
+ (verbatim from `src/equations/Equation.ts.setSpookParams`):
412
+
413
+ ```js
414
+ a = 4.0 / (h * (1 + 4 * d)); // bias gain (analogous to Baumgarte β/h)
415
+ b = (4.0 * d) / (1 + 4 * d); // velocity-error gain
416
+ eps = 4.0 / (h * h * k * (1 + 4 * d)); // regularisation
417
+ ```
418
+
419
+ And the per-iteration update (`GSSolver.solve`):
420
+ ```js
421
+ deltalambda = invC * (B - GWlambda - c.eps * lambdaj);
422
+ // then clamped to [minForce*h, maxForce*h]
423
+ ```
424
+
425
+ The interesting term is **`c.eps * lambdaj`** — the regularisation
426
+ softens the constraint at every iteration in a continuous way, not as
427
+ a one-shot velocity bias. This is fundamentally different from
428
+ Baumgarte: instead of "if penetration > slop, add a corrective
429
+ velocity bias", SPOOK adds a per-iteration drag on the lambda that
430
+ makes the constraint behave like a stiff spring with controlled
431
+ damping. `eps` scales as `1 / (h² k)` — at our 60 Hz timestep
432
+ (`h ≈ 0.0167`) with `k = 1e7`, `eps ≈ 0.024` for `d = 4`. Cannon's
433
+ default produces a stable, soft constraint.
434
+
435
+ Our `solver/solve_contacts.js:442-450`:
436
+ ```js
437
+ let bias = 0;
438
+ if (depth > PENETRATION_SLOP) {
439
+ bias = -BAUMGARTE_BETA / dt * (depth - PENETRATION_SLOP);
440
+ if (bias < -MAX_BAUMGARTE_BIAS) bias = -MAX_BAUMGARTE_BIAS;
441
+ }
442
+ ```
443
+ This is classic Catto / Box2D-Lite Baumgarte: one-shot velocity bias
444
+ proportional to penetration depth minus slop, with a hard cap to stop
445
+ EPA-misreported deep penetrations from launching bodies. The cap is
446
+ itself an admission that pure Baumgarte is fragile against bad depth
447
+ readings — SPOOK's `eps` doesn't have this failure mode because it
448
+ doesn't try to fully correct in one tick.
449
+
450
+ **SPOOK is genuinely more principled and should be considered as a
451
+ swap-in.** Concretely:
452
+
453
+ 1. Replace `bias = -BAUMGARTE_BETA / dt * (depth - PENETRATION_SLOP)`
454
+ with `bias = -a * (depth - PENETRATION_SLOP)` where `a` comes from
455
+ `setSpookParams(k, d, dt)`. Choose `k = 1e7` and `d = 4` as
456
+ Cannon does (these are the field-tested defaults).
457
+ 2. Add the regularisation term to the iteration update — currently
458
+ `lambda_n = -m_eff_n * (vn + bias_n)`, becomes
459
+ `lambda_n = -m_eff_n * (vn + bias_n + eps * j_n_accum)` and
460
+ `m_eff_n = 1 / (k_n + eps)`.
461
+ 3. The `MAX_BAUMGARTE_BIAS` cap can probably be removed — SPOOK's
462
+ softness handles inflated-depth EPA outputs gracefully because
463
+ the regularisation prevents lambda from running away.
464
+
465
+ This is the most concrete and highest-confidence improvement
466
+ opportunity in the review. **Estimated impact:** tall-stack
467
+ stability comparable to Cannon (which handles tens of bodies without
468
+ substepping using SPOOK + 10 iterations), removal of the inflated-EPA
469
+ launch failure mode, more robust feel without changing the public
470
+ API. **Risk:** SPOOK changes contact feel slightly — bodies "settle"
471
+ rather than "snap" to non-penetration. PLAN.md's note about TGS
472
+ substepping being blocked on split-impulse becomes much less urgent.
473
+
474
+ ### Manifold caching — persistent vs per-frame rebuild
475
+
476
+ Cannon has **no persistent manifold cache**. Every step,
477
+ `Narrowphase.getContacts` rebuilds the contact list from scratch:
478
+
479
+ ```
480
+ const oldcontacts = World_step_oldContacts;
481
+ for (i = 0; i !== NoldContacts; i++) { oldcontacts.push(contacts[i]); }
482
+ contacts.length = 0;
483
+ this.narrowphase.getContacts(p1, p2, ..., oldcontacts, ...);
484
+ ```
485
+
486
+ The "oldcontacts" array is a **pool of allocated `ContactEquation`
487
+ objects** to recycle — pool-via-array-of-instances, not warm-start
488
+ information. The per-pair contact identity is rebuilt every frame,
489
+ which means **Cannon cannot do per-pair warm-start at all**. It can
490
+ warm-start within the solver iteration (lambda persists across
491
+ iterations within one frame's solve call) but not across frames.
492
+
493
+ Our `contact/ManifoldStore.js` is a real persistent manifold cache,
494
+ designed exactly like Bullet's `btPersistentManifold`:
495
+ - Keyed by canonical body pair via `PairUint32Map`
496
+ (`ManifoldStore.js:114`).
497
+ - Up to 4 contacts per slot (`MAX_CONTACTS_PER_MANIFOLD = 4`,
498
+ `ManifoldStore.js:10`).
499
+ - Per-contact stride includes `j_n`, `j_t1`, `j_t2` accumulated
500
+ impulses (`ManifoldStore.js:13-33`) that survive across frames.
501
+ - Touched / prev_touched / grace flag scheme so transient one-frame
502
+ separations don't evict the manifold (`ManifoldStore.js:57-69`,
503
+ `416-445`).
504
+ - `begin_refill(slot)` explicitly preserves impulse fields while
505
+ overwriting geometry (`ManifoldStore.js:265-268`).
506
+
507
+ And `narrowphase_step.js:837-931` implements **match-and-merge**:
508
+ candidates from this frame are matched to previous-frame contacts by
509
+ **feature_id first, position-fallback within 2 cm second**, with the
510
+ matched prev-contact's impulses copied to the corresponding new slot
511
+ index. This is a genuine warm-start that survives:
512
+ - Sphere-on-box sliding (feature_id is the box voronoi region,
513
+ `narrowphase_step.js:226-242`, stable while the sphere stays on the
514
+ same face/edge).
515
+ - Triangle-decomposition contacts (feature_id is the triangle index,
516
+ stable per-triangle, `narrowphase_step.js:590`).
517
+ - Box-box (feature_id = 0, falls back to position matching within
518
+ `MATCH_TOL_SQR = 0.0004 m²`).
519
+
520
+ **Status correction.** Prior reviews (Bullet, Jolt, Rapier) flagged
521
+ "warm-start impulses wiped every frame" — that was true at the time
522
+ of those reviews. Reading `ManifoldStore.js` and `narrowphase_step.js`
523
+ as they stand today, **warm-start is wired and working** on the
524
+ steady-state contact path. The only branches that wipe impulses are
525
+ `clear_contacts(slot)` calls (when narrowphase determines no contact
526
+ or empty collider lists), which is correct behaviour — separated
527
+ contacts should not warm-start. The `clear_impulses(slot, k)` for
528
+ unmatched-by-match-and-merge candidates is also correct: a fresh
529
+ contact at a slot index with stale data shouldn't inherit it.
530
+
531
+ This is **a substantial structural advantage over Cannon**. Cannon's
532
+ solver pays 10 iterations of convergence from a cold start every
533
+ frame; ours starts from the previous frame's solution and typically
534
+ converges in 2-3 iterations (with the remaining 7-8 absorbing
535
+ geometry changes). For tall stacks specifically, this is the
536
+ difference between PGS-with-warm-start being usable and needing TGS
537
+ substepping.
538
+
539
+ **Improvement opportunity:** consider lowering iteration count from
540
+ 10 to e.g. 6 for steady-state, with a wake-event re-bump to 10 for
541
+ the first few frames after wake. The savings would scale with the
542
+ warm-start hit rate.
543
+
544
+ ### Sleep — chatter vs atomic
545
+
546
+ Already covered above. Cannon's per-body sleep chatters on
547
+ weakly-connected piles; ours is atomic per island. The improvement
548
+ opportunity is in the **other** direction — Cannon's `sleepTick`
549
+ hooks `Body.dispatchEvent(Body.sleepyEvent)` /
550
+ `Body.dispatchEvent(Body.wakeupEvent)` so user code can subscribe.
551
+ We have `Signal onContactBegin/Stay/End` but no `onBodySleep` /
552
+ `onBodyWake` events. For game code (turn music off when player
553
+ stops, despawn enemies that have been asleep for N seconds), this is
554
+ a missing affordance.
555
+
556
+ ### CCD (Continuous Collision Detection)
557
+
558
+ Cannon has no meaningful CCD — no per-body sweep, no speculative
559
+ margin. Fast-moving bodies tunnel.
560
+
561
+ We have **speculative margin** via the fat-AABB swept extent:
562
+ `compute_fat_world_aabb` pads the broadphase AABB by `linearVelocity
563
+ * dt`, so the broadphase catches imminent collisions and the
564
+ narrowphase sees the future-position pair early. PLAN.md
565
+ "Limitations / Known caveats" notes this is "CCD floor only" — the
566
+ falling-tower repro at 1 km drop onto 1 cm floor still tunnels 180
567
+ out of 1000 bodies because a single-step sweep isn't a true
568
+ swept-shape cast. PLAN.md backlog has "Per-body linear CCD
569
+ shape-cast" as the planned fix.
570
+
571
+ **We are categorically ahead of Cannon here.** Even our "CCD floor"
572
+ catches everything Cannon misses.
573
+
574
+ ### Concave / triangle mesh
575
+
576
+ Cannon's `Trimesh` stores vertices in a `Float32Array`, indices in
577
+ `Int16Array`, normals in `Float32Array`. The narrowphase uses an
578
+ `Octree` to query triangles overlapping the convex's AABB. Each
579
+ returned triangle is **treated as a `ConvexPolyhedron`** for the
580
+ convex-convex SAT + clip path, which is correctness-wise solid for
581
+ polyhedral convex shapes but doesn't help with smooth shapes (which
582
+ Cannon doesn't have anyway).
583
+
584
+ Our `MeshShape3D` + `mesh_enumerate_triangles` does an O(N) linear
585
+ scan with tight per-triangle AABB filtering. No BVH per mesh.
586
+ **This is genuinely a regression compared to Cannon's Octree** for
587
+ large meshes (>10k triangles). PLAN.md doesn't list this as a
588
+ backlog item, but should — a per-mesh BVH built once at attach time
589
+ is exactly the same machinery as the global broadphase BVH, just
590
+ indexed by triangle. **Improvement opportunity: build a per-mesh
591
+ triangle BVH in `MeshShape3D` construction (or first-query), query
592
+ it instead of linear scanning.**
593
+
594
+ Both engines have boundary-edge correctness issues at internal
595
+ triangle junctions. Cannon's polyhedron-per-triangle treats each
596
+ triangle's edges as collision-active, producing the well-known
597
+ "phantom contacts on internal edges" bug. Ours has the same problem
598
+ plus the EPA-on-degenerate-triangle issue (PLAN.md "Limitations").
599
+ Neither engine implements Catto's "voronoi region triangle culling"
600
+ fix. **PLAN.md "Closed-form triangle-vs-primitive solvers"** is the
601
+ correct prescription here.
602
+
603
+ ### Heightmaps
604
+
605
+ Cannon's `Heightfield` constructs an explicit convex polyhedron per
606
+ cell via `getConvexTrianglePillar(i, j, isUpper)` and caches them in
607
+ a `_cachedPillars` dictionary. Cache key is `"i_j_isUpper"` (string!
608
+ allocates on every lookup). Each pillar is a 5-faced extruded
609
+ triangle that goes from the upper surface down to a fixed depth —
610
+ giving the heightfield a "thickness" rather than the infinite-thin
611
+ surface our heightmap has.
612
+
613
+ Our `HeightMapShape3D` is a `Sampler2D`-backed surface; we sample
614
+ heights via `sampleChannelCatmullRomUV` and emit triangles
615
+ on-demand via `heightmap_enumerate_triangles`. No per-cell convex
616
+ caching, no extrusion — the heightmap is a one-sided surface and
617
+ "falling through" from below is undefined behaviour (we explicitly
618
+ reject contacts where the body is on the wrong side via the
619
+ one-sided face-normal check at `narrowphase_step.js:654`).
620
+
621
+ **Tradeoffs:**
622
+
623
+ | Aspect | meep heightmap | cannon-es Heightfield |
624
+ |---|---|---|
625
+ | Memory | O(1) — `Sampler2D` reference + orientation vector | O(cells × cached_pillars) — each pillar is a `ConvexPolyhedron` with 6 vertices, 5 faces |
626
+ | Per-step cost | Catmull-Rom sample × 4 per cell × triangles touched | Polyhedron support × triangle count touched (SAT) |
627
+ | Smoothness | Catmull-Rom interpolation matches the terrain renderer's geometry | Linear interpolation (raw heights) |
628
+ | Two-sidedness | One-sided (per design) | Two-sided (extruded pillar) |
629
+ | Allocation hot-path | None (rebound `Triangle3D` flyweight) | `_cachedPillars` string-key dict + Object spread on miss |
630
+
631
+ The per-step performance is roughly comparable. The Catmull-Rom
632
+ sampling we do is more expensive per-cell than Cannon's linear
633
+ read, but Cannon's polyhedron-per-cell SAT inflates the constant
634
+ factor on the narrowphase side. **Concrete win for us:** zero
635
+ allocation on the heightmap hot path; Cannon's string-key dict
636
+ allocation is a visible GC pressure source.
637
+
638
+ ### Joints / Constraints
639
+
640
+ Cannon has a rich constraint library: `PointToPointConstraint`,
641
+ `DistanceConstraint`, `HingeConstraint`, `LockConstraint`,
642
+ `ConeTwistConstraint`. All are built on `Constraint` (base class)
643
+ that holds an array of `Equation` objects, plus a polymorphic
644
+ `update()` that recomputes the equations' Jacobians from the current
645
+ body poses. The solver iterates contacts AND joint equations
646
+ uniformly because they all share the SPOOK formulation.
647
+
648
+ We have **none**. PLAN.md "Backlog → Features → Joints" lists this
649
+ as planned ("distance, hinge, ball-socket, prismatic") and notes
650
+ "the solver loop is already set up to iterate contacts ∪ joints;
651
+ only constraint pre-step + warm-start hook is missing."
652
+
653
+ **Reading Cannon's `Constraint` API is the right move for the
654
+ joint implementation**. Specifically the SPOOK-equation pattern
655
+ makes it trivial to add new joints — each new joint type just
656
+ specifies how to compute G, what bounds the lambda has, and what `B`
657
+ should be at this pose. If we adopt SPOOK (see solver section) we
658
+ get this "joints and contacts are uniform" property for free.
659
+
660
+ ### Queries
661
+
662
+ Cannon has `World.raycastClosest`, `raycastAny`, `raycastAll` — each
663
+ walks all bodies, then per-body iterates `shapes[i]`, then calls the
664
+ shape's `raycast` method. The shape-side `raycast` is implemented
665
+ per-shape (`Sphere.raycast`, `Box.raycast` via `ConvexPolyhedron`,
666
+ `Plane.raycast`, `Heightfield.raycast`, `Trimesh.raycast`). No BVH
667
+ traversal — the broadphase isn't consulted for raycasts at all.
668
+
669
+ Our `queries/raycast.js` traverses both BVHs by AABB-ray test and
670
+ reports the nearest hit. Currently the result is the leaf AABB hit,
671
+ not refined to the actual shape geometry — so a raycast against a
672
+ sphere reports a hit on the bounding cube. PLAN.md "Public queries"
673
+ notes this: "narrowphase refinement against the actual shape
674
+ geometry is a follow-up — for now `result.t` is the distance to the
675
+ leaf's inflated AABB". This is a **legitimate gap vs Cannon** —
676
+ ours is broadphase-fast but coarse, Cannon's is exact-shape but
677
+ linear-scan. **Improvement opportunity: add per-shape exact-raycast
678
+ refinement** after broadphase narrows candidates. The closed-form
679
+ sphere/box/capsule cases are trivial; the convex case can use
680
+ shape-cast machinery already in place.
681
+
682
+ `shapeCast` is something **Cannon does not have** in any form. Ours
683
+ sweeps a convex shape via swept-AABB broadphase + GJK bisection,
684
+ reporting time-of-impact and contact normal recovered via EPA. This
685
+ is what character controllers need; Cannon users have to roll their
686
+ own from raycasts.
687
+
688
+ `overlap` similarly has no Cannon equivalent. Ours filters
689
+ broadphase candidates by GJK and writes body ids into a caller-sized
690
+ `Uint32Array`. Useful for AOE selection, kinematic probes.
691
+
692
+ ---
693
+
694
+ ## 3. In-depth comparison of our code against Cannon
695
+
696
+ I'll pick eight touchpoints and walk through them.
697
+
698
+ ### 3.1 Per-step body iteration — O(awake) vs O(total)
699
+
700
+ **Our code, `ecs/PhysicsSystem.js:1083`:**
701
+ ```js
702
+ const count = this.storage.awake_count;
703
+ for (let i = 0; i < count; i++) {
704
+ const idx = this.storage.awake_at(i);
705
+ const rb = this.__bodies[idx];
706
+ const tr = this.__transforms[idx];
707
+ integrate_velocity(rb, tr, gx, gy, gz, dt);
708
+ }
709
+ ```
710
+
711
+ **Cannon's pattern, `src/world/World.ts`:**
712
+ ```js
713
+ for (let i = 0; i !== N; i++) {
714
+ const bi = bodies[i];
715
+ bi.sleepTick(this.time);
716
+ // ... unconditional work happens here, sleep state checked inline
717
+ }
718
+ ```
719
+
720
+ Cannon's loop visits all `N` bodies regardless of sleep state. The
721
+ per-iteration cost is dominated by the field access pattern (cache
722
+ miss on cold bodies) + sleep state branch. For a scene with 1M
723
+ bodies and 100 awake, Cannon does 1M unconditional loads + 1M
724
+ branches; we do 100 indexed loads.
725
+
726
+ This is **the single most important scalability difference between
727
+ the two engines**. At our design target of "millions of mostly
728
+ sleeping bodies" Cannon's per-step cost grows linearly with the
729
+ total body count, ours with the awake count. A 1000:1 ratio
730
+ (realistic for a populated town) is a 1000× speedup.
731
+
732
+ **Improvement on our side:** none — the structure is correct. **The
733
+ risk to keep an eye on:** the per-body collider list lives in
734
+ `this.__body_collider_lists[idx]`, a JS array. Each
735
+ `compute_fat_world_aabb` call inside Stage 2 indirects through
736
+ `list[k].collider.shape.compute_bounding_box(...)`. That's three
737
+ pointer-chases per leaf. For a fully-monomorphic hot loop we could
738
+ flatten the leaves into a per-body Float64Array of `(node_id,
739
+ shape_local_aabb)`. Probably not worth the structural cost until
740
+ benchmarks show it dominating.
741
+
742
+ ### 3.2 SPOOK constraint formulation
743
+
744
+ Side-by-side comparison of the per-iteration update:
745
+
746
+ **Cannon `GSSolver`:**
747
+ ```js
748
+ deltalambda = invC * (B - GWlambda - c.eps * lambdaj);
749
+ ```
750
+ Where `invC = 1 / (G·M⁻¹·Gᵀ + eps)`, `B = -g*a - GW*b - h*GiMf`, and
751
+ `eps` is the SPOOK regularisation.
752
+
753
+ **Ours `solve_contacts.js:531`:**
754
+ ```js
755
+ const lambda_n = -m_eff_n * (vn + bias_n);
756
+ ```
757
+ Where `m_eff_n = 1 / k_n` (no eps), `bias_n = -BAUMGARTE_BETA/dt *
758
+ (depth - slop)` capped at `MAX_BAUMGARTE_BIAS = 3 m/s`, plus
759
+ restitution velocity-target if `vn_pre < -RESTITUTION_VELOCITY_THRESHOLD`.
760
+
761
+ **The qualitative difference:**
762
+
763
+ - **SPOOK is unconditional and continuous.** The `eps * lambdaj` term
764
+ is present every iteration, regardless of depth. It acts like a
765
+ spring damping. As lambda grows, the regularisation eats into it,
766
+ preventing runaway.
767
+ - **Baumgarte is conditional and one-shot.** The bias kicks in only
768
+ above the slop threshold, applies a velocity correction once per
769
+ step, and the `MAX_BAUMGARTE_BIAS` cap is a panic guard against
770
+ EPA returning bad depths.
771
+
772
+ **Concrete failure mode of our code:** when EPA misreports depth on a
773
+ torus knot (PLAN.md mentions this as one of the motivating cases for
774
+ MAX_BAUMGARTE_BIAS), our solver caps the bias at 3 m/s. That's
775
+ enough to stop the body launching but not enough to actually
776
+ resolve a 10cm phantom penetration — the body sinks. SPOOK doesn't
777
+ have this dichotomy: the regularisation moderates the response
778
+ naturally.
779
+
780
+ **Recommended action:** when joints land (they need SPOOK or SPOOK-like
781
+ structure anyway for the constraint/joint uniformity), refactor the
782
+ contact solver to use SPOOK. The pre-step changes:
783
+
784
+ ```js
785
+ // In pre-step (where bias is currently computed):
786
+ const k_spook = 1e7; // stiffness
787
+ const d_spook = 4; // relaxation
788
+ const a_spook = 4 / (dt * (1 + 4 * d_spook));
789
+ const b_spook = (4 * d_spook) / (1 + 4 * d_spook);
790
+ const eps = 4 / (dt * dt * k_spook * (1 + 4 * d_spook));
791
+ pre[pre_off + 12] = 1 / (k_n + eps); // includes regularisation
792
+ pre[pre_off + 15] = -a_spook * Math.max(0, depth - PENETRATION_SLOP);
793
+ ```
794
+
795
+ (With a corresponding inner-loop change to subtract `eps * j_n_accum`
796
+ from the lambda before clamping.)
797
+
798
+ This single change probably resolves several existing pain points
799
+ (MAX_BAUMGARTE_BIAS cap, restitution-vs-warm-start interaction in the
800
+ TGS attempt, mesh-contact phantom-depth launching).
801
+
802
+ ### 3.3 Friction model
803
+
804
+ **Cannon's `FrictionEquation`:** one-dimensional, along a single
805
+ tangent direction. The slipForce passes to `Equation` as bounds
806
+ `[-slipForce, slipForce]` where `slipForce = μ * F_normal`.
807
+ Cannon generates **two** FrictionEquations per contact (two tangent
808
+ directions) so the friction is effectively a polyhedral cone, not a
809
+ disk.
810
+
811
+ **Ours `solve_contacts.js:566-571`:**
812
+ ```js
813
+ const want_t1 = j_t1_accum + lambda_t1;
814
+ const want_t2 = j_t2_accum + lambda_t2;
815
+ const max_friction = mus[ci] * new_j_n;
816
+ friction_cone_clamp(scratch_clamp, 0, want_t1, want_t2, max_friction);
817
+ ```
818
+ Disk clamp in the (t1, t2) tangent plane. Mathematically a true
819
+ circular friction cone — strictly more accurate than Cannon's box
820
+ clamp.
821
+
822
+ **Win for us.** The box-vs-disk distinction matters for objects
823
+ sliding in directions not aligned with the tangent basis — Cannon
824
+ allows up to √2 × μ tangential force on a 45°-sliding object, ours
825
+ correctly clamps to μ. The difference is small but observable on
826
+ spinning objects on a flat surface.
827
+
828
+ ### 3.4 Restitution + warm-start interaction
829
+
830
+ **Ours `solve_contacts.js:452-456`:**
831
+ ```js
832
+ if (vn_pre < -RESTITUTION_VELOCITY_THRESHOLD) {
833
+ bias += restitution_combined * vn_pre;
834
+ }
835
+ ```
836
+ Restitution is **added to the velocity bias inside the iterative
837
+ loop**. Combined with the `j_n ≥ 0` clamp, this is what PLAN.md
838
+ identifies as one of three reasons the TGS substep attempt failed:
839
+
840
+ > "Restitution × warm-start clamp. The j_n ≥ 0 clamp lets substep 1+
841
+ > shrink the impulse applied in substep 0, so a restitution=0 ball
842
+ > that should stop instead picks up a phantom upward velocity."
843
+
844
+ **Cannon's `ContactEquation`:**
845
+ ```js
846
+ GW = ePlusOne * vj.dot(n) - ePlusOne * vi.dot(n) + ...
847
+ B = -g * a - GW * b - h * GiMf
848
+ ```
849
+ Restitution scales the **closing velocity** in `GW` before computing
850
+ the bias. Mathematically identical to ours in the simple case but
851
+ structurally different — Cannon's restitution is baked into
852
+ `computeB` and rebuilt every step, ours is added on top of
853
+ Baumgarte every step. Cannon's GS solver doesn't have the
854
+ warm-start-shrinkage problem because there's no warm-start.
855
+
856
+ **A genuine improvement opportunity:** the Box2D / Box2D-Lite
857
+ "split-impulse" pattern that PLAN.md mentions as a prerequisite for
858
+ TGS is also the right structure for clean restitution. Restitution
859
+ should be a **one-shot impulse at first contact**, not a per-iteration
860
+ bias. Bullet does this too. Cannon's approach is less correct (it
861
+ applies on every iteration, but starts cold each frame so the
862
+ warm-start interaction doesn't bite). When we add SPOOK, this is a
863
+ good time to switch to a real restitution model.
864
+
865
+ ### 3.5 ConvexPolyhedron.clipFaceAgainstHull vs box_box_manifold
866
+
867
+ Both implement Sutherland-Hodgman polygon clipping against a series
868
+ of half-spaces. The structural difference:
869
+
870
+ Cannon's `clipFaceAgainstHull` is **N-gon → N-gon** (handles any
871
+ polygon size, output bounded by `numClipFaces × original_size`).
872
+ The clip buffer is a JavaScript `Array<Vec3>` that grows / shrinks
873
+ with `.push()` and `.shift()` — both allocate-and-copy.
874
+
875
+ Our `box_box_manifold.js` clips a quad against 4 edges, hard-coded.
876
+ Output buffer is a `Float64Array(8 * 3)` allocated once at module load
877
+ (`clip_in`, `clip_out` at `box_box_manifold.js:53-54`). Zero
878
+ allocation per call.
879
+
880
+ **Win for us on the box-box case.** For the future ConvexHullShape3D
881
+ we'll want to port Cannon's general case, but with our typed-array
882
+ allocation discipline rather than `Array.push/shift`. The
883
+ key insight from Cannon that we'd otherwise miss: **the inner clip
884
+ loop reuses two ping-pong buffers and a swap-on-output pattern**
885
+ that minimises the polygon-buffer reallocations to two fixed
886
+ `Vec3[]` arrays per clip pass. That pattern translates directly to
887
+ two fixed `Float64Array` slabs for us.
888
+
889
+ ### 3.6 Sleep — atomic island vs per-body chatter
890
+
891
+ Already covered in §1 and §2. Concrete walk-through of the failure:
892
+
893
+ **Cannon's chatter (paraphrased trace):**
894
+ ```
895
+ Frame N: bottom block sleepState=SLEEPY, timeLastSleepy=N*dt
896
+ Frame N+1: top block jitters; broadphase pair list includes block above it
897
+ both blocks `wakeUpAfterNarrowphase=true`
898
+ bottom wakes via velocity propagation
899
+ Frame N+2: bottom resleeps (now SLEEPY again)
900
+ Frame N+3: middle blocks similarly wake from a different jitter
901
+ ...
902
+ ```
903
+
904
+ A 100-block stack chatters at ~3-5 fps of sleep-state churn for
905
+ seconds before fully settling, if it ever does. The user-observable
906
+ effect is GC spikes from per-frame event dispatch plus visible
907
+ micro-jitter.
908
+
909
+ **Our atomic sleep (`ecs/PhysicsSystem.js:942-1006`):**
910
+ ```js
911
+ if (max_v_sqr < threshold_sqr) {
912
+ let min_timer = Infinity;
913
+ for (let i = start; i < end; i++) {
914
+ rb.sleep_timer += dt;
915
+ if (rb.sleep_timer < min_timer) min_timer = rb.sleep_timer;
916
+ }
917
+ if (min_timer >= time_threshold) {
918
+ this.__atomic_sleep_island_range(body_data, start, end);
919
+ }
920
+ }
921
+ ```
922
+ The whole island either accumulates time or doesn't. When it crosses
923
+ the threshold, every member sleeps the same step, and the
924
+ sleep-group circular DLL connects them so a single wake event
925
+ walks the chain.
926
+
927
+ **Verbatim from PLAN.md's "Done" section:** "Per-island atomic sleep:
928
+ ... Replaces the per-body chatter on weakly-connected piles." This
929
+ is one of the engineering claims most clearly validated by the
930
+ Cannon comparison — the algorithm Cannon ships has the exact failure
931
+ mode we explicitly engineered around.
932
+
933
+ ### 3.7 Per-pair narrowphase dispatch
934
+
935
+ Both engines look superficially similar — a cascade of pair-type
936
+ checks dispatching to per-pair contact methods. The differences:
937
+
938
+ **Cannon `Narrowphase.getContacts`:** dispatch table keyed by shape
939
+ type enum (Sphere=1, Box=4, Plane=8, etc.). Lookup pattern:
940
+ ```js
941
+ const resolverIndex = (si.type | sj.type);
942
+ // uses bit-OR of shape type enums as table key
943
+ ```
944
+ Method-per-pair on the `Narrowphase` class (`sphereSphere`,
945
+ `sphereBox`, `sphereConvex`, `convexConvex`, `planeBox`,
946
+ `planeConvex`, `planeTrimesh`, `sphereTrimesh`, `convexTrimesh`,
947
+ `sphereHeightfield`, `convexHeightfield`, `sphereParticle`,
948
+ `planeParticle`, `convexParticle`, `boxConvex`, ...).
949
+
950
+ **Ours `narrowphase_step.js:dispatch_pair`:** cascade of
951
+ `if (isSphereA && isSphereB)`, `if ((isSphereA && isBoxB) || ...)`,
952
+ etc. — handled as boolean checks on `shape.isXxx === true` flags.
953
+
954
+ **Tradeoff:**
955
+
956
+ - Cannon's table is O(1) dispatch but allocates an integer combination
957
+ per call and has unmaintainable enum-OR collision risk. It also
958
+ forces all narrowphase to live on one class instance.
959
+ - Ours is O(K) cascade where K = pair categories handled (currently
960
+ ~7). At small K cascade is faster than table lookup; the
961
+ isShapeType branch predicts well after the first hit. At large K
962
+ the table wins.
963
+
964
+ **Both lose to a real double-dispatch pattern**, but neither is bad
965
+ enough to fix yet.
966
+
967
+ **Improvement opportunity for us:** the cascade in `dispatch_pair`
968
+ checks `isSphereA && isBoxB || isBoxA && isSphereB` which always
969
+ runs both halves of the `||` even after a hit. Rearranging by
970
+ expected frequency (sphere-sphere first if that's the dominant pair,
971
+ which it is for ball-based games) gets us closer to one-branch fast
972
+ path for the common case. Probably <1% real-world win; not urgent.
973
+
974
+ ### 3.8 Allocation patterns
975
+
976
+ **Cannon's allocation hot-spots that cannon-es fixed vs original
977
+ cannon.js:**
978
+
979
+ cannon-es is the result of years of de-allocation work compared to
980
+ the original. Notable fixes documented in the changelog / source
981
+ comments:
982
+
983
+ - Module-level `tmpVec`, `tmpQuat`, `Body_applyForce_rotForce`,
984
+ `Body_applyImpulse_velo` etc. scratch instances declared once per
985
+ module, reused everywhere. Original cannon.js allocated these
986
+ per-call.
987
+ - `World_step_oldContacts` and `World_step_oldFrictionEquations`
988
+ arrays reused frame-to-frame, with `.length = 0` + push pattern.
989
+ - `SAPBroadphase` insertion-sort with persistent `axisList`.
990
+
991
+ **Where Cannon STILL allocates per-step** (and we don't):
992
+ - `Narrowphase.getContacts` uses `Array.push` extensively for the
993
+ output contact list. Even with the `contactPointPool` recycling
994
+ the `ContactEquation` instances, the outer array grows via `push`
995
+ and resets via `.length = 0`. This is **the** cannon-es per-frame
996
+ allocation hot path. Our equivalent — writing into a fixed
997
+ `Float64Array` slab via `set_contact(slot, idx, ...)` — has zero
998
+ steady-state allocation.
999
+ - `SplitSolver` BFS over equations builds a fresh `nodes` /
1000
+ `children` graph each call. Pool exists but graph-shape changes
1001
+ defeat it.
1002
+ - `Heightfield._cachedPillars` uses string keys (`"i_j_isUpper"`) —
1003
+ every lookup allocates a string. Hot path on heightmap collision.
1004
+
1005
+ **JS-specific optimisation insights from cannon-es worth absorbing:**
1006
+
1007
+ 1. **Module-level scratch instances declared in a uniform pattern.**
1008
+ The Cannon convention is `const Body_methodName_purpose = new
1009
+ Vec3()`. Naming makes the scratch's owner and intent obvious. We
1010
+ do the same with prefixed `scratch_*` names; cannon's discipline
1011
+ is more enforced. The convention is good — names like
1012
+ `scratch_inertia_a` / `scratch_inertia_b` in our solver follow
1013
+ the pattern correctly. A grep for `const scratch_` shows we're
1014
+ consistent.
1015
+
1016
+ 2. **Body field initialisation order matters for V8 hidden classes.**
1017
+ Both engines initialise fields in the constructor (or as class
1018
+ fields) in a fixed order, ensuring all `RigidBody` / `Body`
1019
+ instances share the same hidden class. Our class-field syntax
1020
+ (`linearVelocity = new Vector3(0, 0, 0)` at class-body scope) is
1021
+ the modern equivalent of Cannon's constructor assignments. Both
1022
+ approaches achieve hidden-class consistency. **Risk:** if any
1023
+ call site does `body.somethingNew = value`, V8 transitions to a
1024
+ new hidden class. We should keep RigidBody field-set strictly
1025
+ closed; users wanting extra state should attach side components.
1026
+
1027
+ 3. **Avoid Float32Array for hot constraint data.** cannon-es uses
1028
+ `Vec3` (3 numbers as object fields, not typed array). Vec3 access
1029
+ is faster than Float32Array indexing on V8 (no bounds check). But
1030
+ for **bulk solver state** that lives in slabs (our manifold
1031
+ `data_buffer`, our `scratch_pre` per-contact stride), Float64Array
1032
+ is correct — bulk reads / writes by offset compile to direct
1033
+ memory operations. The lesson is "use Vec3-like for individual
1034
+ vectors, TypedArray for bulk slabs". We follow this pattern
1035
+ correctly.
1036
+
1037
+ 4. **Avoid object-spread / Object.assign on hot path.** Cannon has a
1038
+ couple of places (in Equation reset code) that do `Object.assign`
1039
+ on equation instances — measurable per-frame cost. We don't have
1040
+ any such patterns; worth a periodic audit.
1041
+
1042
+ 5. **Use shift-by-N on bit-packed flags rather than separate
1043
+ booleans.** Our `ManifoldStore` packs `count | touched |
1044
+ prev_touched | grace` into one Uint32 word
1045
+ (`ManifoldStore.js:57-61`). Cannon uses separate boolean fields
1046
+ on `Body` (`allowSleep`, `wakeUpAfterNarrowphase`, etc.) — fine
1047
+ for code clarity but more memory per body. At million-body
1048
+ scale our bit-packing saves 4 bytes per slot × N slots.
1049
+
1050
+ ### 3.9 Numerical stability — degenerate cases
1051
+
1052
+ **Cannon's degenerate-case handling:**
1053
+
1054
+ - `findSeparatingAxis` skips edge-cross axes where the cross is
1055
+ almost zero (`if (!Cross.almostZero())`). Prevents NaN normals on
1056
+ parallel edge configurations.
1057
+ - `ConvexPolyhedron` constructor warns if a face normal points
1058
+ inward (`console.error` — doesn't fix).
1059
+ - `Trimesh` validates index range when computing per-triangle
1060
+ normals.
1061
+
1062
+ **Ours:**
1063
+
1064
+ - `narrowphase_step.js:728`: `if (!(depth > 0) || !Number.isFinite(depth))
1065
+ return count;` — rejects NaN/Inf/zero depths from EPA. Good.
1066
+ - `narrowphase_step.js:620`: same check inside the concave loop.
1067
+ - `solve_contacts.js:151, 261`: `inv_mass_of` and
1068
+ `angular_jacobian_contribution` guard against zero mass /
1069
+ inertia. Good.
1070
+ - **Missing:** no zero-area triangle guard in `mesh_enumerate_triangles`
1071
+ or `heightmap_enumerate_triangles`. A degenerate triangle (collinear
1072
+ vertices) would feed a zero face normal into GJK / EPA and the
1073
+ one-sided rejection at `narrowphase_step.js:654` would silently
1074
+ skip it. **Probably fine in practice** — degenerate triangles in a
1075
+ source mesh are usually filtered at import — but worth a paranoid
1076
+ guard.
1077
+ - `solver/solve_contacts.js:418-420` guards K=0 with
1078
+ `k_n > 0 ? 1 / k_n : 0` — prevents NaN m_eff on two
1079
+ zero-mass bodies (kinematic-vs-kinematic, which the solver
1080
+ should never see, but defensively).
1081
+
1082
+ **Concrete improvement opportunity:** add `if (Math.abs(face_normal_sq) <
1083
+ EPS) continue;` at `narrowphase_step.js:599` (after computing
1084
+ `fnx_l/fny_l/fnz_l` in the triangle loop). This rejects degenerate
1085
+ triangles before they reach EPA.
1086
+
1087
+ ---
1088
+
1089
+ ## 4. Simplicity & uniformity
1090
+
1091
+ ### Code organisation
1092
+
1093
+ | Aspect | meep | cannon-es |
1094
+ |---|---|---|
1095
+ | Module layout | One concept per file, hierarchical directories (`body/`, `broadphase/`, `solver/`, `gjk/`, `narrowphase/`, ...) | Mostly one class per file, flat-ish (`src/objects/`, `src/collision/`, `src/equations/`, `src/solver/`, `src/shapes/`) |
1096
+ | Class structure | Mostly free functions + a few classes (Storage, ManifoldStore, IslandBuilder, PhysicsSystem) | Class hierarchy everywhere (Body, Shape→Sphere/Box/..., Equation→ContactEquation/FrictionEquation, Constraint→PointToPoint/Distance/...) |
1097
+ | Public API surface | Concentrated on `PhysicsSystem` | Spread across World + Body + Shape + Constraint subclasses |
1098
+ | File count | ~50 files in `physics/` (excluding tests) | ~50 files in cannon-es `src/` |
1099
+ | LOC | Roughly comparable; we have more in the broadphase + decomposition machinery |
1100
+
1101
+ Both engines are flat and avoid deep hierarchies (unlike Bullet's
1102
+ `btCollisionObject < btRigidBody < btSoftBody` etc.). Cannon does
1103
+ have one inheritance chain that matters: `Shape` → `Sphere`, `Box`
1104
+ (which is a `ConvexPolyhedron` subclass), `Plane`, `Heightfield`,
1105
+ `Particle`, `ConvexPolyhedron`, `Trimesh`. The `Shape` base class
1106
+ exposes `volume()`, `boundingSphereRadius`, `updateBoundingSphereRadius()`,
1107
+ `calculateLocalInertia()`, `calculateWorldAABB()` — virtual methods
1108
+ that every shape overrides. Adding a new shape means subclassing
1109
+ `Shape` and implementing 5 virtuals.
1110
+
1111
+ Our `AbstractShape3D` plays the equivalent role. New shape = same
1112
+ contract.
1113
+
1114
+ ### Adding a new shape pair — workflow
1115
+
1116
+ **Cannon:** add a new method on `Narrowphase`, register it in the
1117
+ dispatch table. Add the shape class as a `Shape` subclass.
1118
+
1119
+ **Ours:** add a new closed-form file in `narrowphase/`, add a branch
1120
+ to the `dispatch_pair` cascade in `narrowphase_step.js`. Add the
1121
+ shape class as an `AbstractShape3D` implementation.
1122
+
1123
+ **Equivalent friction.** Adding capsule-mesh would be the same
1124
+ amount of work in either engine.
1125
+
1126
+ ### Adding a new constraint
1127
+
1128
+ **Cannon:** subclass `Constraint`, instantiate the Equations it
1129
+ owns, implement `update()` to recompute Jacobians. SPOOK + GSSolver
1130
+ handle the rest.
1131
+
1132
+ **Ours:** **not currently supported**. The solver knows about
1133
+ contacts only; adding joints requires extending `solve_island` to
1134
+ iterate a `joints` list parallel to contacts. PLAN.md says this is
1135
+ planned but not done.
1136
+
1137
+ **Cannon's design is the right reference.** The "constraints and
1138
+ contacts are both Equations" abstraction is what makes Cannon's
1139
+ joint library so flexible. Adopting SPOOK structurally enables
1140
+ copying this design.
1141
+
1142
+ ### Adding a new query
1143
+
1144
+ **Cannon:** implement per-shape `raycast` method; `World.raycastClosest`
1145
+ iterates all bodies + shapes.
1146
+
1147
+ **Ours:** add a query function in `queries/`. Use the BVH for
1148
+ broadphase, dispatch per-shape narrowphase.
1149
+
1150
+ **We win on structure** — queries live in their own subsystem and
1151
+ share the broadphase. Cannon's per-shape `raycast` is duplicated
1152
+ across every shape class and bypasses the broadphase entirely.
1153
+
1154
+ ### Where Cannon's `Body` god-class wins
1155
+
1156
+ Despite our decomposition argument, **Cannon's single-class `Body`
1157
+ makes some things simpler**:
1158
+ - One reference to pass around (`body`).
1159
+ - `addEventListener('sleep', ...)` etc. for application code.
1160
+ - `body.applyForce(force, worldPoint)` reads naturally.
1161
+
1162
+ Our `system.applyForceAt(rigidBody, transform, force, worldPoint)`
1163
+ needs four arguments because force application needs the transform
1164
+ (for the r = worldPoint - position calculation) and the system
1165
+ (for the wake call), and the rigid body itself. The system-level
1166
+ API is necessarily more verbose. **This is the unavoidable cost of
1167
+ ECS decomposition** — we picked it for good reasons (transform is
1168
+ shared with rendering, components are independently
1169
+ serialisable / observable) and the price is API verbosity.
1170
+
1171
+ ### Where Cannon's API is a cautionary tale
1172
+
1173
+ 1. **`world.bodies` is a public mutable array.** Users mutate it
1174
+ directly, which violates every encapsulation invariant. We
1175
+ correctly hide `__bodies` and expose only `entityOf(packed_id)`.
1176
+
1177
+ 2. **Body events fire via `dispatchEvent` on the Body itself.** This
1178
+ means every Body has its own listener list. At 100k bodies that's
1179
+ 100k tiny listener arrays. We use Signal-per-system + per-entity
1180
+ event channel (`PhysicsEvents`), which scales better.
1181
+
1182
+ 3. **`World.contacts` rebuilt every frame as a fresh array.** Visible
1183
+ GC pressure. Our `ManifoldStore` is persistent and slab-based.
1184
+
1185
+ 4. **`Heightfield._cachedPillars` string-key dict.** Allocates per
1186
+ lookup. Our `HeightMapShape3D` has no equivalent caching layer —
1187
+ it samples and emits triangles on-demand into a flyweight, zero
1188
+ allocation.
1189
+
1190
+ 5. **`ConvexPolyhedron.findSeparatingAxis` allocates `Vec3`
1191
+ intermediates** despite cannon-es's de-allocation pass. Our
1192
+ `box_box_manifold.js` is entirely typed-array, zero allocation.
1193
+
1194
+ ### Where we could simplify further
1195
+
1196
+ - The cascade of `is*A && is*B || is*A && is*B` in `dispatch_pair`
1197
+ has each pair-type check duplicated for ordering. A small enum +
1198
+ pair-table would compress this without sacrificing inlining. But
1199
+ it's not urgent — the cascade is still readable at 7 pair types.
1200
+
1201
+ - `narrowphase_step.js` has the SAT/clip box-box code linked but the
1202
+ general convex-hull case TODO. When we add `ConvexHullShape3D`,
1203
+ the box-box code wants to be subsumed into the general path
1204
+ rather than maintained as a parallel implementation. Don't add
1205
+ ConvexHull without refactoring box-box at the same time, or we'll
1206
+ have two copies of essentially the same algorithm.
1207
+
1208
+ - `solve_contacts.js` has separate friction and normal blocks that
1209
+ could be unified under SPOOK's "every equation is the same shape"
1210
+ model. This is the joint-system prerequisite anyway.
1211
+
1212
+ ---
1213
+
1214
+ ## Top recommendations
1215
+
1216
+ In rough priority order:
1217
+
1218
+ 1. **Adopt SPOOK constraint regularisation** in the solver
1219
+ (`solver/solve_contacts.js`). Removes the `MAX_BAUMGARTE_BIAS`
1220
+ hack, improves tall-stack stability comparable to Cannon, and is
1221
+ the structural prerequisite for clean joint implementation.
1222
+ Estimated effort: medium (one solver rewrite, no public API
1223
+ change). Estimated payoff: high.
1224
+
1225
+ 2. **Wire MPR as the EPA-non-convergence fallback** in
1226
+ `narrowphase_step.js`. Code exists at `gjk/mpr.js` per PLAN.md;
1227
+ one branch on EPA's NaN/zero-depth path swaps it in. Unblocks the
1228
+ torus-knot test and the smooth-shape contact stability issue.
1229
+ Effort: low. Payoff: high for smooth-shape cases.
1230
+
1231
+ 3. **Add per-shape exact raycast refinement** after BVH broadphase
1232
+ (`queries/raycast.js`). Currently returns leaf-AABB hit which
1233
+ underestimates `t` for non-AABB shapes. Cannon's per-shape
1234
+ raycast methods are the reference. Effort: medium. Payoff:
1235
+ makes the raycast query genuinely useful for character
1236
+ controllers / weapon hit detection.
1237
+
1238
+ 4. **Build per-mesh triangle BVH for `MeshShape3D`**. Cannon's
1239
+ `Trimesh.tree` is the reference (Octree there; BVH for us since
1240
+ we have the infrastructure). Replace the O(N) linear scan in
1241
+ `mesh_enumerate_triangles`. Effort: low (BVH is in
1242
+ `core/bvh2/`). Payoff: linear-to-log mesh scaling, opens the door
1243
+ to genuinely large static-mesh worlds.
1244
+
1245
+ 5. **Closed-form triangle-vs-primitive** narrowphase (already on
1246
+ PLAN.md backlog — re-emphasising because it's the gating issue
1247
+ for several skipped tests). Cannon's "treat triangle as
1248
+ ConvexPolyhedron and SAT it" works but has the same internal-edge
1249
+ phantom contact issue we have. Catto's voronoi triangle
1250
+ classification is the standard fix.
1251
+
1252
+ 6. **Add `onBodySleep` / `onBodyWake` signals** on `PhysicsSystem`.
1253
+ Cannon's per-body events are the wrong scale (event listener per
1254
+ body), but a system-level signal carrying island membership is
1255
+ useful for game code. Effort: trivial. Payoff: gameplay-quality
1256
+ of life.
1257
+
1258
+ 7. **Consider lowering default solver iterations from 10 to 6 once
1259
+ warm-start is verified stable in production.** Cannon defaults
1260
+ to 10 because it can't warm-start. Ours can; the savings scale
1261
+ with warm-start hit rate (typically >80% in steady-state
1262
+ contacts). Effort: trivial (one constant). Validation: medium
1263
+ (need a stack-stability benchmark sweep).
1264
+
1265
+ 8. **Audit `mesh_enumerate_triangles` for zero-area triangle
1266
+ handling.** Add an `Math.abs(face_normal_sq) < EPS` skip. Cheap
1267
+ defensive guard. Effort: trivial.
1268
+
1269
+ ---
1270
+
1271
+ ## Summary
1272
+
1273
+ Cannon is our closest peer on engineering constraints (pure JS,
1274
+ single-threaded, no SIMD) and the comparison is consistently
1275
+ favourable. **Where we are clearly ahead:**
1276
+ - Broadphase (BVH × 2 vs default NaiveBroadphase O(N²))
1277
+ - Body iteration scaling (O(awake) vs O(total))
1278
+ - Atomic island sleep + atomic wake (vs per-body chatter on weakly-connected piles)
1279
+ - Persistent manifold cache with cross-frame warm-start (vs frame-rebuilt contact list)
1280
+ - CCD floor via speculative margin (vs nothing)
1281
+ - Disk-clamped friction cone (vs polyhedral box clamp)
1282
+ - Zero-allocation hot paths in narrowphase + heightmap (vs string-key dict, Array.push)
1283
+ - shapeCast + overlap queries (Cannon has neither)
1284
+ - ECS decomposition + observability hooks
1285
+
1286
+ **Where Cannon is ahead or has the better idea:**
1287
+ - SPOOK constraint formulation (vs our Baumgarte + cap hack)
1288
+ - Joints / Constraints library (we have none)
1289
+ - General convex polyhedron shape (we have only `BoxShape3D`)
1290
+ - Per-shape exact raycast (vs our AABB-only result)
1291
+ - Per-mesh BVH (Octree) for large triangle meshes (vs our O(N) linear scan)
1292
+ - Per-body sleep/wake event listeners (we have signals at system level only)
1293
+
1294
+ **Headline structural insight:** Cannon is "what JS rigid-body
1295
+ physics looked like in 2014, slowly de-allocated by community
1296
+ maintenance to 2023." We are "what JS rigid-body physics could be in
1297
+ 2026 if you start from the modern reference architectures
1298
+ (Jolt/Bullet/Box2D) and then constrain to pure JS." The
1299
+ ecosystem-position is genuinely interesting — there's no other
1300
+ pure-JS engine attempting our design ambitions.