@woosh/meep-engine 2.139.0 → 2.141.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 (199) 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/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
  6. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
  7. package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
  8. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  9. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  11. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
  12. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  13. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  14. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  15. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  16. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  17. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  18. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  20. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  21. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  22. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  23. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  24. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  25. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  26. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  27. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  28. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  29. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  30. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  31. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  32. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  41. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  42. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  43. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  44. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  45. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  46. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  47. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  48. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  49. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  50. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  53. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  54. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  55. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  56. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  57. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  59. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  60. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  62. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  63. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  64. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  65. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  66. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  71. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  72. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  73. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  74. package/src/engine/control/first-person/TODO.md +173 -127
  75. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  76. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  77. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  78. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  79. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  80. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  81. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  82. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  83. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  84. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  85. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  86. package/src/engine/physics/PLAN.md +578 -236
  87. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  88. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  89. package/src/engine/physics/REVIEW_002.md +151 -0
  90. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  91. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  92. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  93. package/src/engine/physics/constraint/DofMode.js +35 -0
  94. package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
  95. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  96. package/src/engine/physics/constraint/solve_constraints.js +436 -0
  97. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  98. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  99. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  100. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  101. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  102. package/src/engine/physics/ecs/Joint.d.ts +179 -0
  103. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  104. package/src/engine/physics/ecs/Joint.js +234 -0
  105. package/src/engine/physics/ecs/PhysicsSystem.d.ts +180 -20
  106. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  107. package/src/engine/physics/ecs/PhysicsSystem.js +1423 -1159
  108. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  109. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  110. package/src/engine/physics/fluid/FluidField.js +14 -10
  111. package/src/engine/physics/fluid/FluidSimulator.js +1 -1
  112. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  113. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  114. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  115. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  116. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  117. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  118. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  119. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  120. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  121. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +30 -6
  122. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  123. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +44 -18
  124. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  125. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  126. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  127. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  128. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  129. package/src/engine/physics/gjk/gjk.js +421 -378
  130. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  131. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  132. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  133. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  134. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  135. package/src/engine/physics/gjk/mpr.js +344 -0
  136. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  137. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  138. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  139. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  140. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  141. package/src/engine/physics/integration/integrate_position.js +43 -12
  142. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  143. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  144. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  145. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  146. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  147. package/src/engine/physics/island/IslandBuilder.js +33 -16
  148. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  149. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  150. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  151. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  152. package/src/engine/physics/narrowphase/box_box_manifold.js +140 -18
  153. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  154. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  155. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  156. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  157. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  158. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  159. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  160. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  161. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  162. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  163. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  164. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  165. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  166. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  167. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  168. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  169. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  170. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  171. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  172. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  173. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  174. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  175. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  176. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  177. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  178. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  179. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +41 -2
  180. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  181. package/src/engine/physics/narrowphase/narrowphase_step.js +1497 -382
  182. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  183. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  184. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  185. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  186. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  187. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  188. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  189. package/src/engine/physics/queries/overlap_shape.js +183 -0
  190. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  191. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  192. package/src/engine/physics/queries/shape_cast.js +387 -0
  193. package/src/engine/physics/solver/solve_contacts.d.ts +146 -32
  194. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  195. package/src/engine/physics/solver/solve_contacts.js +809 -223
  196. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  197. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  198. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  199. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -1,236 +1,578 @@
1
- # Physics engine — state of play
2
-
3
- Tracker for what's built, what's pending, and what's deferred.
4
-
5
- ---
6
-
7
- ## Context
8
-
9
- Deterministic JS rigid-body physics engine for the meep ECS. Target: game
10
- scenarios with up to millions of mostly-sleeping bodies, deterministic replays
11
- for netcode and reproducible debugging, broad shape coverage for common game
12
- collisions. Pure JS — no WASM, no SIMD, no worker threads.
13
-
14
- Architectural references for design choices:
15
- - **Jolt** — pre-allocated body pool, active-list iteration, two-tree
16
- broadphase (static + dynamic).
17
- - **Bullet** — `btPersistentManifold` cache layout with up to 4 points.
18
- - **Box2D / Catto** — sequential impulse with warm-starting, Sutherland-Hodgman
19
- face clipping for box-box.
20
-
21
- ---
22
-
23
- ## Done
24
-
25
- ### Foundations
26
- - `RigidBody`, `Collider`, `BodyKind`, `RigidBodyFlags`, `ColliderFlags`,
27
- `SleepState`, `PhysicsEvents`.
28
- - `BodyStorage`: SoA pool, generation-tracked stable IDs, dense awake list,
29
- min-heap free for deterministic ID reuse.
30
- - `PhysicsSystem`: full public API surface (gravity, force/impulse with and
31
- without application point, torque, velocity setter, wake/sleep, contact
32
- filter callback).
33
- - Binary serialization adapters for `RigidBody` and `Collider` (transient
34
- runtime state deliberately excluded).
35
- - `PairUint32Map`: open-addressed Robin Hood + Fibonacci hash for the
36
- pair → manifold-slot index (the one new collection added to `core/collection/`).
37
-
38
- ### Pipeline (`PhysicsSystem.fixedUpdate`)
39
- 1. Velocity integration (semi-implicit Euler, linear + angular, gravity,
40
- damping, world-frame inverse-inertia for torque)
41
- 2. Per-collider broadphase refit with fat AABB (Box2D-style velocity-padded
42
- slack)
43
- 3. Pair generation: per-leaf query against both BVHs (static + dynamic),
44
- canonical `(min, max)` pairs, dedup via manifold touched flag
45
- 4. Wake propagation for sleeping bodies in the pair list
46
- 5. Narrowphase cross-product over collider lists
47
- 6. Sequential-impulse solver (Catto-style, warm-start, friction, Baumgarte)
48
- 7. Position integration (linear + quaternion)
49
- 8. Sleep test (per-body velocity² below threshold for ≥ 0.5 s)
50
- 9. Manifold diff → `ContactBegin` / `Stay` / `End` event dispatch
51
- 10. `manifolds.advance_frame()` — roll touched bits, evict grace-expired slots
52
-
53
- ### Shape coverage
54
- | Pair | Path | Manifold |
55
- |---|---|---|
56
- | sphere-sphere | closed-form | 1 point |
57
- | sphere-box | closed-form (handles centre-inside-box) | 1 point |
58
- | capsule-sphere | point-on-segment closed-form | 1 point |
59
- | capsule-capsule | segment-segment closest pair | 1 point |
60
- | capsule-box | iterative segment-vs-OBB (primary) + cap-centre sphere-vs-OBB at each endpoint | up to 3 |
61
- | box-box face-face | SAT + Sutherland-Hodgman clipping | up to 4 |
62
- | box-box edge-edge | SAT + midpoint fallback | 1 point |
63
- | anything else | GJK + EPA | 1 point (may fail on smooth shapes) |
64
-
65
- ### Solver
66
- - Sequential impulse with warm-starting (10 velocity iterations by default).
67
- - Coulomb friction with disk-clamped tangent impulses.
68
- - Baumgarte position correction folded into the velocity solve.
69
- - Full angular Jacobian (`I_w⁻¹ = R · diag · R^T`) and angular impulse
70
- application.
71
- - Public force/impulse-at-point API (`applyForceAt`, `applyImpulseAt`,
72
- `applyTorque`).
73
-
74
- ### Sleep + events
75
- - Per-island **atomic sleep**: an island sleeps when `max(|v|² + |ω|²)`
76
- across all members stays below the threshold long enough; the whole
77
- island sleeps in the same frame. Replaces the per-body chatter on
78
- weakly-connected piles.
79
- - **Atomic wake**: members of a sleeping island are threaded into a
80
- circular doubly-linked list (`sleep_group_next` / `sleep_group_prev`);
81
- waking any one member walks the chain and wakes the rest in the same
82
- call. A 100-block stack hit at the base wakes top-down in one frame
83
- rather than over 100 frames of broadphase propagation.
84
- - `DisableSleep` on any island member exempts the whole island.
85
- - ContactBegin / Stay / End buffer + dispatch through both
86
- `PhysicsSystem.onContactBegin/Stay/End` Signals and the per-entity
87
- `entity.sendEvent(PhysicsEvents.ContactBegin, ...)` channel (when a
88
- dataset is attached).
89
-
90
- ### Islands
91
- - **Union-find** with path halving + union by min-index over the awake-body
92
- + touched-contact graph (`engine/physics/island/union_find.js`).
93
- - **`IslandBuilder`** produces deterministic CSR-style output: bodies and
94
- manifold slots grouped by island, sorted ascending within and across
95
- islands. Static / kinematic bodies are constraint anchors only — they
96
- don't merge islands, so disjoint piles on the same floor are separate
97
- islands.
98
- - **Solver iterates per island**: impulse convergence happens inside an
99
- island without waiting on unrelated bodies' Gauss-Seidel updates, and
100
- disconnected awake bodies don't pay each other's solver cost.
101
-
102
- ### Compound bodies
103
- - A body has 0..N attached colliders. Each collider has its own world
104
- transform and its own BVH leaf.
105
- - Same-entity colliders, child-entity colliders (via `ParentEntity`), or
106
- hybrids all supported.
107
- - `ColliderObserverSystem` auto-attaches colliders via the dataset when
108
- paired with `PhysicsSystem` in an EntityManager.
109
- - Narrowphase runs the cross-product over both bodies' collider lists per
110
- body-pair, accumulates candidates, reduces to ≤4 contacts by
111
- depth + spread.
112
-
113
- ### Public queries
114
- - `raycast(origin, dir, max_dist, filter?)` — nearest broadphase AABB hit
115
- across both trees.
116
-
117
- ### Determinism
118
- - Direct typed-array writes on hot paths (bypassing `Vector3#set`'s observer
119
- dispatch) Transform writes still go through `set()` because external
120
- systems subscribe (TransformAttachment, EntityNode, FogOfWarRevealer,
121
- ViewportPosition).
122
- - Active body iteration sorted by body index.
123
- - Pair canonicalisation `(min, max)`.
124
- - Min-heap free list for slot reuse.
125
- - No `Math.random` anywhere in the simulation step.
126
- - Same-runtime bit-exact determinism by design; cross-runtime is a known
127
- future seam.
128
-
129
- ### Migration
130
- - `Motion` / `MotionSystem` / `MotionSerializationAdapter` relocated from
131
- the meep core (`engine/ecs/`) to the game-domain layer
132
- (`mir-engine/model/game/ecs/`). meep no longer ships the legacy shim.
133
-
134
- ### Bonus utilities
135
- - `core/geom/3d/line/line3_closest_points_segment_segment.js` — generally
136
- useful 3D segment-segment closest-pair via Ericson §5.1.9.
137
- - `core/collection/PairUint32Map.js` non-allocating
138
- `Map<(u32, u32) u32>` with Robin Hood + Fibonacci hash.
139
-
140
- ---
141
-
142
- ## Limitations / Known caveats
143
-
144
- - **Multi-collider material precision**: solver reads friction/restitution
145
- from the first-attached collider of each body. Mixed-material compound
146
- bodies lose accuracy here. The contact-filter callback's `colliderA/B`
147
- arguments are similarly the body's primary collider, not the specific
148
- collider in contact.
149
- - **EPA on smooth shapes**: degenerates (no flat face to converge on).
150
- Mitigated by closed-form paths for sphere/cube/capsule pairs; exotic
151
- convex shapes vs spheres can still fail.
152
- - **Box-box edge-edge contact**: single midpoint contact rather than
153
- multi-point. Skewed-orientation cube collisions are stable-enough but
154
- not as precise as face-face.
155
- - **CCD floor only**: speculative margin via the fattened AABB prevents
156
- most tunnelling. No per-body swept shape-cast for very fast objects.
157
- - **Cross-runtime determinism is not guaranteed**: `Math.sin/cos/exp/log`
158
- are ULP-correct but not bit-exact across V8 / SpiderMonkey / JSC.
159
-
160
- ---
161
-
162
- ## Backlog (planned, in scope)
163
-
164
- ### Stability
165
- - [ ] **Edge-edge multi-point manifold** for skewed box contacts.
166
- - [ ] **Per-contact source-collider tracking** so multi-material compound
167
- bodies get accurate per-contact friction/restitution. Requires
168
- stashing the collider identity in the manifold contact stride.
169
-
170
- ### Performance / Scale
171
- - [ ] **TGS (Temporal Gauss-Seidel) substepping**: optional alternative
172
- to PGS for high-quality stacking with large mass ratios.
173
- - [ ] **Per-body linear CCD shape-cast**: optional opt-in for fast-moving
174
- bodies where speculative margin isn't enough.
175
- - [ ] **Per-island parallel solve**: today's island data layout would
176
- allow worker-based solving once `SharedArrayBuffer` is available.
177
- Out-of-scope unless / until SAB is universally usable.
178
-
179
- ### Features
180
- - [ ] **Joints**: distance, hinge, ball-socket, prismatic. The solver loop
181
- is already set up to iterate `contacts ∪ joints`; only constraint
182
- pre-step + warm-start hook is missing.
183
- - [ ] **Convex hull shape** with eigen-based principal-axes inertia
184
- derivation. Hooks `matrix_eigenvalues_in_place` from the existing
185
- linalg layer.
186
- - [ ] **Cylinder / cone shapes** (closed-form pairs against the existing
187
- family + GJK+EPA fallback for general convex).
188
-
189
- ### API polish
190
- - [ ] **`overlapShape(shape, position, rotation, filter?)`** public query
191
- (broadphase + narrowphase) for AOE and selection use cases.
192
- - [ ] **`castShape(shape, from, to, rotation, filter?)`** for character
193
- controllers and kinematic shape sweeps.
194
-
195
- ---
196
-
197
- ## Future / out-of-scope
198
-
199
- These are explicit architectural exclusions or post-v1 explorations.
200
-
201
- ### Architecture
202
- - **Cross-runtime bit-exact determinism**: a soft-float library would
203
- replace `Math.sin/cos/exp/log/pow` in the hot path. The codebase is
204
- already structured to make this a swap-in at `quat_integrate.js` and
205
- tangent-basis construction in `build_manifold.js`. Not pursued because
206
- the same-runtime determinism we have covers the common cases (single-
207
- device replay, networked lockstep where all clients run the same JS
208
- engine).
209
- - **WASM / SIMD**: the engine targets pure-JS portability. SIMD would
210
- invalidate the determinism story (V8 doesn't expose deterministic
211
- Float64x2 ops).
212
- - **Multi-threaded solver**: workers don't share memory cheaply without
213
- `SharedArrayBuffer` plus the COOP/COEP HTTP headers, which are not
214
- always available. Single-threaded is good-enough for the awake-body
215
- budget that matters.
216
-
217
- ### Simulation extensions
218
- - **Soft body / cloth / fluids**: the SoA layout in `BodyStorage` and the
219
- manifold cache are rigid-body shaped. A soft-body system would be a
220
- parallel subsystem, not an extension.
221
- - **Reduced-coordinate articulations** (MuJoCo / Featherstone-style):
222
- game-physics audience runs in maximal coordinates by convention. Not
223
- on the roadmap.
224
-
225
- ### Game-side
226
- - **Vehicle physics** (suspensions, drivetrains): a domain layer that
227
- sits on top of the rigid-body primitives, not in `meep/`.
228
- - **Character controllers**: same `engine/control/first-person/` is the
229
- natural home.
230
-
231
- ---
232
-
233
- ## Notable design files
234
-
235
- - Original design plan: `C:\Users\Alex\.claude\plans\let-s-plan-to-implement-transient-harp.md`
236
- - This file (state of play): `engine/physics/PLAN.md`
1
+ # Physics engine — state of play
2
+
3
+ Tracker for what's built, what's pending, and what's deferred.
4
+
5
+ ---
6
+
7
+ ## Context
8
+
9
+ Deterministic JS rigid-body physics engine for the meep ECS. Target: game
10
+ scenarios with up to millions of mostly-sleeping bodies, deterministic replays
11
+ for netcode and reproducible debugging, broad shape coverage for common game
12
+ collisions. Pure JS — no WASM, no SIMD, no worker threads.
13
+
14
+ Architectural references for design choices:
15
+ - **Jolt** — pre-allocated body pool, active-list iteration, two-tree
16
+ broadphase (static + dynamic).
17
+ - **Bullet** — `btPersistentManifold` cache layout with up to 4 points.
18
+ - **Box2D / Catto** — sequential impulse with warm-starting, Sutherland-Hodgman
19
+ face clipping for box-box.
20
+
21
+ ---
22
+
23
+ ## Done
24
+
25
+ ### Foundations
26
+ - `RigidBody`, `Collider`, `BodyKind`, `RigidBodyFlags`, `ColliderFlags`,
27
+ `SleepState`, `PhysicsEvents`.
28
+ - `BodyStorage`: SoA pool, generation-tracked stable IDs, dense awake list,
29
+ min-heap free for deterministic ID reuse.
30
+ - `PhysicsSystem`: full public API surface (gravity, force/impulse with and
31
+ without application point, torque, velocity setter, wake/sleep, contact
32
+ filter callback).
33
+ - Binary serialization adapters for `RigidBody` and `Collider` (transient
34
+ runtime state deliberately excluded).
35
+ - `PairUint32Map`: open-addressed Robin Hood + Fibonacci hash for the
36
+ pair → manifold-slot index (the one new collection added to `core/collection/`).
37
+
38
+ ### Pipeline (`PhysicsSystem.fixedUpdate`)
39
+ 1. Velocity integration (semi-implicit Euler, linear + angular, gravity,
40
+ damping, world-frame inverse-inertia for torque)
41
+ 2. Per-collider broadphase refit with fat AABB (Box2D-style velocity-padded
42
+ slack)
43
+ 3. Pair generation: per-leaf query against both BVHs (static + dynamic),
44
+ canonical `(min, max)` pairs, dedup via manifold touched flag
45
+ 4. Wake propagation for sleeping bodies in the pair list
46
+ 5. Narrowphase cross-product over collider lists
47
+ 6. Sequential-impulse solver (Catto-style, warm-start, friction, Baumgarte)
48
+ 7. Position integration (linear + quaternion)
49
+ 8. Sleep test (per-body velocity² below threshold for ≥ 0.5 s)
50
+ 9. Manifold diff → `ContactBegin` / `Stay` / `End` event dispatch
51
+ 10. `manifolds.advance_frame()` — roll touched bits, evict grace-expired slots
52
+
53
+ ### Shape coverage
54
+ | Pair | Path | Manifold |
55
+ |---|---|---|
56
+ | sphere-sphere | closed-form | 1 point |
57
+ | sphere-box | closed-form (handles centre-inside-box) | 1 point |
58
+ | capsule-sphere | point-on-segment closed-form | 1 point |
59
+ | capsule-capsule | segment-segment closest pair | 1 point |
60
+ | capsule-box | iterative segment-vs-OBB (primary) + cap-centre sphere-vs-OBB at each endpoint | up to 3 |
61
+ | box-box face-face | SAT + Sutherland-Hodgman clipping | up to 4 |
62
+ | box-box edge-edge | SAT + midpoint fallback | 1 point |
63
+ | convex × concave (heightmap, mesh) | per-triangle GJK + EPA via decomposition dispatcher | 1 point per triangle (deepest wins) |
64
+ | anything else | GJK + EPA | 1 point (may fail on smooth shapes) |
65
+
66
+ ### Non-convex shapes
67
+ - **`is_convex` flag** on `AbstractShape3D.prototype` (default `true`).
68
+ Overridden to `false` on `HeightMapShape3D`, `MeshShape3D`, `UnionShape3D`.
69
+ `TransformedShape3D` inherits via getter that reads the wrapped subject.
70
+ - **`HeightMapShape3D`** — orientation-vector + `Sampler2D`-backed terrain
71
+ shape. Heights sampled via `sampleChannelCatmullRomUV` (matching the
72
+ terrain system's geometry construction). Compute_bounding_box,
73
+ contains_point, signed_distance, nearest_point_on_surface all
74
+ implemented; `support` throws (non-convex by construction).
75
+ - **`Triangle3D`** buffer-flyweight convex shape. `bind(buffer, offset)`
76
+ repoints at 9 consecutive floats in an external Float64Array. Zero
77
+ allocation per emission; used by the decomposition path.
78
+ - **Triangle decomposition machinery** under
79
+ `engine/physics/narrowphase/decomposition/`:
80
+ - `TRIANGLE_FLOAT_STRIDE = 10` per triangle (`vA.xyz`, `vB.xyz`,
81
+ `vC.xyz`, `feature_id`).
82
+ - `heightmap_enumerate_triangles(out, offset, shape, ...aabb)`
83
+ Arvo-projects the convex's AABB into heightmap-local, intersects
84
+ with the footprint to derive a cell range, emits 2 triangles per
85
+ cell with stable feature_ids.
86
+ - `mesh_enumerate_triangles(out, offset, shape, ...aabb)` — linear
87
+ O(N) scan over `MeshShape3D.indices` with tight per-triangle AABB
88
+ filtering. feature_id = triangle index.
89
+ - `aabb_world_to_local(out, world_aabb, pos, rot)` — 8-corner
90
+ projection of a world AABB into a body's local frame.
91
+ - `decompose_to_triangles(...)` dispatcher switching on shape
92
+ type marker.
93
+ - **Narrowphase concave dispatch** in `narrowphase_step.js`: detects
94
+ `is_convex === false`, computes convex's world AABB, projects to
95
+ concave's local frame, decomposes, per-triangle GJK + EPA with
96
+ one-sided face-normal rejection and contact-normal dedup. Concave-vs-
97
+ concave dynamic pairs are explicitly refused.
98
+
99
+ ### Solver
100
+ - Sequential impulse with warm-starting (10 velocity iterations by default).
101
+ - Coulomb friction with disk-clamped tangent impulses.
102
+ - Baumgarte position correction folded into the velocity solve.
103
+ - Full angular Jacobian (`I_w⁻¹ = R · diag · R^T`) and angular impulse
104
+ application.
105
+ - Public force/impulse-at-point API (`applyForceAt`, `applyImpulseAt`,
106
+ `applyTorque`).
107
+
108
+ ### Sleep + events
109
+ - Per-island **atomic sleep**: an island sleeps when `max(|v|² + |ω|²)`
110
+ across all members stays below the threshold long enough; the whole
111
+ island sleeps in the same frame. Replaces the per-body chatter on
112
+ weakly-connected piles.
113
+ - **Atomic wake**: members of a sleeping island are threaded into a
114
+ circular doubly-linked list (`sleep_group_next` / `sleep_group_prev`);
115
+ waking any one member walks the chain and wakes the rest in the same
116
+ call. A 100-block stack hit at the base wakes top-down in one frame
117
+ rather than over 100 frames of broadphase propagation.
118
+ - `DisableSleep` on any island member exempts the whole island.
119
+ - ContactBegin / Stay / End buffer + dispatch through both
120
+ `PhysicsSystem.onContactBegin/Stay/End` Signals and the per-entity
121
+ `entity.sendEvent(PhysicsEvents.ContactBegin, ...)` channel (when a
122
+ dataset is attached).
123
+
124
+ ### Islands
125
+ - **Union-find** with path halving + union by min-index over the awake-body
126
+ + touched-contact graph (`engine/physics/island/union_find.js`).
127
+ - **`IslandBuilder`** produces deterministic CSR-style output: bodies and
128
+ manifold slots grouped by island, sorted ascending within and across
129
+ islands. Static / kinematic bodies are constraint anchors only — they
130
+ don't merge islands, so disjoint piles on the same floor are separate
131
+ islands.
132
+ - **Solver iterates per island**: impulse convergence happens inside an
133
+ island without waiting on unrelated bodies' Gauss-Seidel updates, and
134
+ disconnected awake bodies don't pay each other's solver cost.
135
+
136
+ ### Compound bodies
137
+ - A body has 0..N attached colliders. Each collider has its own world
138
+ transform and its own BVH leaf.
139
+ - Same-entity colliders, child-entity colliders (via `ParentEntity`), or
140
+ hybrids all supported.
141
+ - `ColliderObserverSystem` auto-attaches colliders via the dataset when
142
+ paired with `PhysicsSystem` in an EntityManager.
143
+ - Narrowphase runs the cross-product over both bodies' collider lists per
144
+ body-pair, accumulates candidates, reduces to ≤4 contacts by
145
+ depth + spread.
146
+
147
+ ### Public queries
148
+ - `raycast(origin, dir, max_dist, filter?)` — nearest broadphase AABB hit
149
+ across both trees.
150
+ - `shapeCast(ray, shape, rotation, result, filter?)` broadphase swept
151
+ AABB against both BVHs; per-candidate AABB-slab interval narrowing,
152
+ coarse step over the narrowed window, GJK bisection to time-of-impact.
153
+ Output normal is the true contact-surface normal at the kiss point,
154
+ recovered by re-running GJK + EPA at `best_t` on the winning candidate.
155
+ Falls back to `-ray.direction` only on EPA degeneracies (NaN / zero
156
+ depth). Tests cover axis-aligned, off-axis, and oblique cube-vs-cube;
157
+ sphere-vs-smooth-shape near-tangent has documented angular tolerance
158
+ bands inherited from EPA on smooth supports.
159
+ - `overlap(shape, position, rotation, output, output_offset, filter?)`
160
+ — broadphase + per-candidate GJK overlap detection. Writes body_ids
161
+ into a caller-sized buffer; returns count. Convex query shapes only
162
+ (concave throws). Concave candidates routed through the per-triangle
163
+ decomposition path. Designed for speculative kinematic queries on
164
+ kinematic bodies (character controllers, AOE selection).
165
+
166
+ ### Standalone narrowphase utilities
167
+ - `compute_penetration(out_direction, shape_a, pos_a, rot_a, shape_b,
168
+ pos_b, rot_b)` non-system geometry primitive: positive penetration
169
+ depth + outward direction (B → A convention) on overlap, 0 otherwise.
170
+ Convex × convex uses GJK + EPA. Convex × concave uses per-triangle
171
+ half-space test (`convex.support(-face_normal)` projected onto each
172
+ triangle's plane), aggregated deepest-wins. The half-space approach
173
+ sidesteps `Triangle3D`'s degenerate support along face-normal axes
174
+ (the same issue that makes per-triangle GJK return false positives
175
+ on clearly non-overlapping sphere-above-flat configurations).
176
+ Concave × concave throws (M×N triangle pairs is out of scope).
177
+ Naturally handles "body inside the concave solid" — reports the depth
178
+ needed to push back through the nearest face. Documented limitation:
179
+ closed meshes can over-report on side faces whose 2D extent the
180
+ convex shape's flank crosses; a future closed-form triangle-vs-X
181
+ solver fixes this.
182
+
183
+ ### Determinism
184
+ - Direct typed-array writes on hot paths (bypassing `Vector3#set`'s observer
185
+ dispatch) — Transform writes still go through `set()` because external
186
+ systems subscribe (TransformAttachment, EntityNode, FogOfWarRevealer,
187
+ ViewportPosition).
188
+ - Active body iteration sorted by body index.
189
+ - Pair canonicalisation `(min, max)`.
190
+ - Min-heap free list for slot reuse.
191
+ - No `Math.random` anywhere in the simulation step.
192
+ - Same-runtime bit-exact determinism by design; cross-runtime is a known
193
+ future seam.
194
+
195
+ ### Migration
196
+ - `Motion` / `MotionSystem` / `MotionSerializationAdapter` relocated from
197
+ the meep core (`engine/ecs/`) to the game-domain layer
198
+ (`mir-engine/model/game/ecs/`). meep no longer ships the legacy shim.
199
+
200
+ ### Alternative narrowphase: MPR
201
+ - `engine/physics/gjk/mpr.js` — Minkowski Portal Refinement (XenoCollide,
202
+ Snethen GDC 2009). Single-pass overlap test + MTV computation,
203
+ output convention matches EPA so it's drop-in compatible at any
204
+ narrowphase call site. Tends to converge in 5–15 iterations on
205
+ smooth shapes where EPA stalls (the polytope-on-curved-surface
206
+ failure mode the torus-knot reproducer exercised). Not yet wired
207
+ into `narrowphase_step` available as a swap candidate / per-pair
208
+ preference once we want to fall back to it on EPA non-convergence,
209
+ or as the default for any shape pair that involves a mesh.
210
+
211
+ ### Bonus utilities
212
+ - `core/geom/3d/line/line3_closest_points_segment_segment.js` generally
213
+ useful 3D segment-segment closest-pair via Ericson §5.1.9.
214
+ - `core/collection/PairUint32Map.js` non-allocating
215
+ `Map<(u32, u32) → u32>` with Robin Hood + Fibonacci hash.
216
+
217
+ ---
218
+
219
+ ## Limitations / Known caveats
220
+
221
+ - **Multi-collider material precision**: solver reads friction/restitution
222
+ from the first-attached collider of each body. Mixed-material compound
223
+ bodies lose accuracy here. The contact-filter callback's `colliderA/B`
224
+ arguments are similarly the body's primary collider, not the specific
225
+ collider in contact.
226
+ - **EPA on smooth shapes**: degenerates (no flat face to converge on).
227
+ Mitigated by closed-form paths for sphere/cube/capsule pairs; exotic
228
+ convex shapes vs spheres can still fail.
229
+ - **EPA on `Triangle3D`** (concave-shape narrowphase): the triangle's
230
+ support is degenerate along its face-normal axis (all 3 vertices
231
+ project to the same value), so per-triangle GJK + EPA in the
232
+ narrowphase concave dispatch produces imprecise depths near the
233
+ iteration cap. A sphere dropping onto a flat heightmap decelerates
234
+ ~70% on first contact but eventually sinks through over ~50 steps —
235
+ the `narrowphase_concave.spec.js` "drop and settle" cases are
236
+ `test.skip` for this reason. Workaround in `compute_penetration` is
237
+ the half-space pre-test that avoids running GJK on degenerate
238
+ triangle supports altogether; long-term fix is closed-form
239
+ triangle-vs-primitive solvers.
240
+ - **Box-box edge-edge contact**: single midpoint contact rather than
241
+ multi-point. Skewed-orientation cube collisions are stable-enough but
242
+ not as precise as face-face.
243
+ - **CCD floor only**: speculative margin via the fattened AABB prevents
244
+ most tunnelling. No per-body swept shape-cast for very fast objects.
245
+ - **Cross-runtime determinism is not guaranteed**: `Math.sin/cos/exp/log`
246
+ are ULP-correct but not bit-exact across V8 / SpiderMonkey / JSC.
247
+ - **Dynamic concave bodies settle poorly under TGS**: the substep loop
248
+ re-derives contact geometry analytically from the per-triangle contact
249
+ feature (witness anchors + normal) captured once by narrowphase and held
250
+ fixed for the whole outer step. For a convex body the contact feature is
251
+ stable under the small per-step motion, so this is exact; for a *dynamic
252
+ concave mesh body* (e.g. a torus knot rocking on its own lobes) the
253
+ supporting triangle itself changes as the body rocks, so freezing the
254
+ feature pumps a little energy in and the body rocks / slowly sinks instead
255
+ of settling (the `PhysicsSystem.spec.js` torus-knot dynamic-settle test is
256
+ `test.skip` for this reason). Note this is NOT a contact-precision issue —
257
+ the knot already uses the exact closed-form box-triangle solver (P1.1b);
258
+ the problem is purely that TGS freezes *which* feature is in contact across
259
+ substeps. The common concave case — a convex dynamic body on static concave
260
+ terrain — is unaffected (the convex side's feature is stable), and that is
261
+ the only concave case the engine targets.
262
+
263
+ **Interim fix (implemented): per-substep concave re-detection.** For
264
+ contact pairs involving a concave body, the substep loop re-runs the
265
+ concave narrowphase geometry at the current substep pose (instead of the
266
+ analytic refresh that freezes the feature) and re-prepares those contacts
267
+ from the fresh witness/normal/depth — so the contact normal tracks the
268
+ rocking body and no energy is pumped in. Convex pairs keep the cheap
269
+ analytic refresh. This is ~Nx narrowphase cost on concave-involved pairs
270
+ (acceptable — they're rare), gated by collider convexity. Un-skips the
271
+ torus-knot dynamic-settle test.
272
+
273
+ **Better long-term fix: convex collision proxies (not raw concave).** Every
274
+ major engine (Box2D, Jolt, PhysX, Rapier) requires dynamic bodies to be
275
+ convex or convex-decomposed; raw concave meshes are static-only. The right
276
+ granularity is a *few* convex pieces — NOT the thousands of tets a
277
+ volumetric mesher produces (tet count ≈ collider/BVH-leaf count, which
278
+ explodes the broadphase for an awake body; tet meshing is for a future
279
+ FEM/soft-body subsystem, not rigid collision). See the "Convex collision
280
+ proxies for dynamic concave bodies" backlog item — a 3D convex hull builder
281
+ (single-hull proxy covers most dynamic objects) plus an optional
282
+ few-hull (V-HACD-style) decomposition. Those supersede the interim
283
+ per-substep re-detection once built.
284
+
285
+ ---
286
+
287
+ ## Backlog (planned, in scope)
288
+
289
+ ### Solver quality (next major work)
290
+
291
+ These items move the engine from "competent" to "great". TGS is the next
292
+ significant solver-architecture change; joints come after, once the TGS
293
+ scaffolding is in place.
294
+
295
+ - **TGS (Temporal Gauss-Seidel) substepping with split-impulse** — Phases
296
+ 1–3 **LANDED**. The solver is now a staged TGS pipeline
297
+ (`solver/solve_contacts.js`: `prepare_contacts` → per substep
298
+ [`refresh_contacts` → `warm_start_contacts` → `solve_velocity` →
299
+ `solve_position`] → `apply_restitution`), driven by the substep loop in
300
+ `PhysicsSystem.fixedUpdate`. Defaults: `substeps = 4`,
301
+ `velocityIterations = 4`, `positionIterations = 1` (all fields on
302
+ `PhysicsSystem`).
303
+ - **Phase 1 — split impulse.** Position correction runs on a per-body
304
+ pseudo-velocity (`__pseudo_velocity`) folded into the pose by
305
+ `integrate_position` and discarded; depth correction never
306
+ contaminates persistent velocity.
307
+ - **Phase 2 — one-shot restitution.** Velocity pass is pure
308
+ non-penetration; restitution is a single post-loop pass driving
309
+ `vn → -e·vn_approach`, gated on a running max normal impulse
310
+ (`maxNormalImpulse`) so transient collisions still bounce under
311
+ per-substep warm-start.
312
+ - **Phase 3 — substep loop.** `substeps` sub-iterations at `h = dt/N`.
313
+ Forces consumed once at full `dt` before the loop; gravity applied
314
+ per substep; **warm-start replayed per substep** (the crux — a
315
+ per-substep impulse balances one substep of gravity, so resting
316
+ stacks hold at zero velocity). Contact geometry is re-derived
317
+ **analytically** each substep from frozen local witness anchors +
318
+ the trusted prepare-time depth (a sign-robust delta), so narrowphase
319
+ runs **once** per outer step — cheaper than the originally-planned
320
+ per-substep match-and-merge refresh, and exact for convex
321
+ primitives whose contact feature is stable under small motion.
322
+
323
+ Results vs the single-step solver: a 100:1 mass ratio now stacks
324
+ instead of crushing through (regression test added); 8-cube stacks
325
+ settle to zero velocity and sleep (were impossible long-term under SI);
326
+ falling-tower bench cost unchanged (~48 ms/1000 active bodies);
327
+ `substeps = 1` reproduces the single-step result bit-for-bit-ish
328
+ (one-frame restitution delay aside).
329
+
330
+ **Hard-won lessons (for REVIEW_002):**
331
+ - Warm-start MUST be per-substep, not once. Replaying a full-frame
332
+ impulse once while gravity arrives per substep over-pushes resting
333
+ contacts and *explodes* deep stacks. Per-substep warm-start +
334
+ per-substep gravity cancel exactly at rest.
335
+ - Restitution must gate on the *running max* normal impulse, not the
336
+ end-of-loop value — per-substep warm-start relaxes a transient
337
+ contact's `j_n` back to ~0 by the end, which would suppress the
338
+ bounce.
339
+ - Analytic separation re-derivation beats per-substep narrowphase
340
+ for convex shapes (cheaper, no manifold-lifecycle churn) but is
341
+ only as good as the frozen normal — see the concave caveat below.
342
+
343
+ Follow-ups since the core landed:
344
+ - [x] **Box-box SAT reference tie-break deadband** — aligned cube
345
+ stacks (4–10 high) now settle to zero velocity and sleep; the
346
+ reference-face flip-flop that creeped/toppled them is gone.
347
+ - [x] **Per-substep contact re-detection for concave pairs** — lifts
348
+ the dynamic-concave-body limitation; the torus-knot dynamic-settle
349
+ test is un-skipped. Concave pairs re-run narrowphase geometry each
350
+ substep (`redetect_concave_contacts`); convex pairs keep the cheap
351
+ analytic refresh.
352
+
353
+ Remaining (Phases 4–6) — now complete:
354
+ - [x] Regression coverage: heavy-on-light pyramid (10× capstone on two
355
+ light cubes settles + sleeps) and a ragdoll-stub (shoulder
356
+ ball-socket + elbow hinge arm hangs, stays articulated, settles).
357
+ - [x] REVIEW_002 retrospective — `engine/physics/REVIEW_002.md`.
358
+
359
+ References: Catto 2018 ("Soft Constraints" GDC talk + the TGS
360
+ follow-up); Box2D v3 source (`b2ApplyRestitution`, the substep solver
361
+ stages); Rapier as the closest architectural sibling.
362
+
363
+ - [ ] **Constraints / joints — the next major work.** Now unblocked: TGS is
364
+ in (joint-chain convergence is a TGS sweet spot), warm-start +
365
+ per-substep + island machinery is reusable, and the SPOOK compliance
366
+ dial already in the contact solver gives soft/spring constraints
367
+ essentially for free. Target use cases: chains/ropes, ragdolls,
368
+ vehicles (incl. suspension), plus the common mechanical set (doors,
369
+ pistons, welds, grab/drag, winches, drivetrains).
370
+
371
+ **Foundational work (do first): generalise the solver to constraint
372
+ rows.** Today `solver/solve_contacts.js` is hard-coded to the
373
+ contact-shape constraint (normal + 2 friction tangents, ≥0 clamp,
374
+ restitution, penetration bias). Joints are equality / inequality
375
+ constraints on relative velocity at anchors, generally bilateral
376
+ (impulse may be ±) with optional limits and motors. The clean shape —
377
+ and what Jolt / Box2D-v3 do — is a **generic constraint row**: a
378
+ Jacobian (linear + angular parts per body), an effective mass, a bias
379
+ (position error × SPOOK gain, or motor target), and impulse bounds
380
+ `[lo, hi]` (`[0,∞)` for a contact/limit, `(−∞,∞)` for an equality,
381
+ `[−maxForce·h, +maxForce·h]` for a motor). Each joint type just fills
382
+ in its rows; the existing per-body impulse-apply primitive
383
+ (`apply_impulse_to_body` + `world_inverse_inertia_apply`), the
384
+ per-substep warm-start, the islands, and the split-impulse / SPOOK
385
+ position handling are all reused. Contacts become *one* constraint
386
+ type among several rather than the hard-coded path.
387
+
388
+ The specific constraint set, its use-case mapping, and per-type
389
+ architecture-fit assessment are under review (see the constraints
390
+ sketch). High level: ball-socket / distance / spring / weld and the
391
+ grab constraint are near drop-ins on the row machinery; hinge /
392
+ prismatic / cone-twist / motors / limits add angular-row + bounded-row
393
+ mechanics (still within the impulse framework); raycast vehicles,
394
+ conveyor surface-velocity, and gear/pulley coupling are higher-level
395
+ systems or contact modifiers that sit *on top of* the primitives
396
+ rather than being generic rows.
397
+
398
+ **Decision: build ONE configurable 6-DOF constraint** (PhysX D6 / Jolt
399
+ SixDOF), implemented mode-by-mode. The `Joint` ECS component carries
400
+ `dofMode[6]` (3 linear, 3 angular) each `{locked|free|limited|spring|
401
+ motor}` + per-DOF limit/spring/motor config + warm-start accumulators.
402
+ Concrete joints are configs, not new code (ball-socket = lock 3 linear;
403
+ hinge = lock 3 linear + 2 angular; weld = lock 6; cone-twist = lock 3
404
+ linear + limit 3 angular; suspension = spring 1 linear + lock rest).
405
+
406
+ Phasing:
407
+ 1. [x] Constraint-row solver as a **parallel row set** in the TGS
408
+ substep loop (contacts left untouched, not ported — lower risk).
409
+ `constraint/solve_constraints.js` reuses `world_inverse_inertia`,
410
+ per-substep warm-start, and the SPOOK position bias; `Joint`
411
+ component + `link_joint`/`unlink_joint` in PhysicsSystem;
412
+ `jointIterations` knob. Bodies need no collider.
413
+ 2. [x] **LOCKED linear DOFs → ball-socket.** Pendulum (anchor pinned
414
+ to a world pivot, body swings) and a 2-link chain (body↔body,
415
+ joints stay connected, chain hangs) pass. → **chains, ropes,
416
+ pendulums working.**
417
+ 3. [x] LOCKED angular + linear DOFs in the frame basis — **weld,
418
+ hinge, prismatic done**. Joint frame bases
419
+ (`localBasisA`/`localBasisB`); BOTH linear and angular rows now
420
+ resolve in frame A's axes (cleared the world-axis linear debt — the
421
+ solver is fully frame-relative). Angular: relative rotation
422
+ `qD = conj(qA)·qB` → small-angle error, ωB−ωA rows + SPOOK bias.
423
+ Linear: `C·axis` error, vA−vB rows. `asWeld()` / `asHinge(axis)` /
424
+ `asPrismatic(axis)` presets. Verified: weld holds pose + orientation
425
+ against an off-centre torque; hinge swings about its free axis only
426
+ (locked axes < 0.02); prismatic slides along its one free axis,
427
+ locked on the others; all LOCKED-mode tests still green after the
428
+ frame-basis rewrite.
429
+ 4. [ ] LIMITED + MOTOR (bounded rows) → doors, pistons, wheel
430
+ spin/drive, joint ROM.
431
+ 5. [ ] SPRING (SPOOK soft) → suspension, bungees, soft ragdolls.
432
+ 6. [ ] Cone-twist / swing-twist angular limits → ragdolls.
433
+ 7. [ ] Vehicle layer — recommend a **raycast-vehicle controller**
434
+ (raycast + suspension force + tire friction; what most games ship)
435
+ on top of the queries, with simulated wheels via the 6-DOF as an
436
+ option. → vehicles.
437
+ 8. [ ] Extras: pulley, gear, conveyor (contact surface-velocity),
438
+ breakable-joint flag.
439
+
440
+ Foundation gaps — both now closed:
441
+ - [x] **Island integration.** Jointed dynamic-dynamic bodies are
442
+ unioned into one island (`IslandBuilder` Pass 1b), so a chain /
443
+ ragdoll sleeps and wakes as a unit; `__wake_joints` propagates wake
444
+ across a joint when one side is awake and the other asleep
445
+ (e.g. a kinematic/motor driver pulling a sleeping chain). Verified:
446
+ a damped chain settles and both links sleep in one sleep group.
447
+ - [x] **Generation-checked body references.** `solve_joints`,
448
+ `IslandBuilder` Pass 1b and `__wake_joints` all gate on
449
+ `storage.is_valid(packedId)`, so a joint to an unlinked / slot-reused
450
+ body goes inert instead of attaching to the wrong body or crashing.
451
+ Verified: unlinking a jointed body leaves the joint inert and the
452
+ survivor free.
453
+
454
+ References: Catto / Box2D-v3 joint solvers; Jolt's `Constraint` base
455
+ (`SetupVelocityConstraint` / `WarmStartVelocityConstraint` /
456
+ `SolveVelocityConstraint` / `SolvePositionConstraint`); PhysX D6 /
457
+ ODE joint taxonomy.
458
+
459
+ ### Stability
460
+ - [ ] **Closed-form triangle-vs-primitive solvers**
461
+ (`triangle_sphere_contact`, `triangle_box_contact`,
462
+ `triangle_capsule_contact`). The decomposition machinery is in
463
+ place (`Triangle3D` flyweight, `heightmap_enumerate_triangles` /
464
+ `mesh_enumerate_triangles`, `decompose_to_triangles` dispatcher,
465
+ `aabb_world_to_local`, `narrowphase_step.js` concave branch), but
466
+ the per-triangle narrowphase uses GJK + EPA which hits the
467
+ smooth-shape iteration cap and `Triangle3D`'s degenerate-support
468
+ issue. Closed-form solvers per primitive bypass both. This is now
469
+ the single biggest accuracy gap in the engine — it would:
470
+ - Unblock the `narrowphase_concave.spec.js` skipped tests (ball
471
+ drops on heightmap / mesh-cube settle correctly).
472
+ - Unblock the `PhysicsSystem.spec.js` torus-knot test.
473
+ - Improve `compute_penetration`'s closed-mesh accuracy (currently
474
+ documented over-reports on side faces).
475
+ Existing primitive pair solvers (`sphere_box_contact`,
476
+ `capsule_box_multi_contacts`, `box_box_manifold`) are the
477
+ blueprint. Triangle is roughly a box with two half-extents = 0.
478
+ - [ ] **Edge-edge multi-point manifold** for skewed box contacts.
479
+ - [ ] **Per-contact source-collider tracking** so multi-material compound
480
+ bodies get accurate per-contact friction/restitution. Requires
481
+ stashing the collider identity in the manifold contact stride.
482
+
483
+ ### Performance / Scale
484
+ - [ ] **Per-body linear CCD shape-cast**: optional opt-in for fast-moving
485
+ bodies where speculative margin isn't enough. The bench's falling
486
+ tower (1km drop onto a 1cm floor) is the concrete reproducer —
487
+ 180 / 1000 bodies tunnel.
488
+ - [ ] **Per-island parallel solve**: today's island data layout would
489
+ allow worker-based solving once `SharedArrayBuffer` is available.
490
+ Out-of-scope unless / until SAB is universally usable.
491
+
492
+ ### Features
493
+ - [ ] **Convex collision proxies for dynamic concave bodies.** The long-term
494
+ replacement for the interim per-substep concave re-detection (see
495
+ Limitations) — and how every major engine handles dynamic non-convex
496
+ shapes: collide a *few* convex pieces, never the raw concave mesh.
497
+ 1. **3D convex hull builder** (meep has only 2D hulls today —
498
+ `core/geom/2d/convex-hull/`). A single hull of a mesh is one
499
+ collider / one broadphase leaf and covers the overwhelming majority
500
+ of dynamic objects (thrown props, debris). Pairs with the existing
501
+ "Convex hull shape + eigen-inertia" item below.
502
+ 2. **Few-hull (V-HACD-style) approximate convex decomposition** for
503
+ shapes whose concavity matters (a cup, a chair): ~8–64 fat convex
504
+ hulls = 8–64 colliders, two orders of magnitude below a tet mesh.
505
+ Each hull is convex → stable contact feature → the TGS analytic refresh
506
+ is exact → no per-substep re-detection, no rocking. Granularity is the
507
+ whole point: collider/BVH-leaf count must stay small for an *awake*
508
+ dynamic body (the volumetric tet-mesher under `core/geom/3d/tetrahedra/`
509
+ is the wrong tool here — thousands of pieces — and belongs to a future
510
+ FEM/soft-body subsystem, not rigid collision).
511
+ - [ ] **Convex hull shape** with eigen-based principal-axes inertia
512
+ derivation. Hooks `matrix_eigenvalues_in_place` from the existing
513
+ linalg layer.
514
+ - [ ] **Cylinder / cone shapes** (closed-form pairs against the existing
515
+ family + GJK+EPA fallback for general convex).
516
+
517
+ ### API polish
518
+ - [x] **`overlap(shape, position, rotation, output, output_offset,
519
+ filter?)`** — broadphase + narrowphase overlap query for kinematic
520
+ / AOE / selection use cases. Body_ids written into a caller-sized
521
+ Uint32Array buffer. Convex query shape only; concave candidates
522
+ are routed through the per-triangle decomposition path.
523
+ - [x] **`shapeCast(ray, shape, rotation, result, filter?)`** for
524
+ character controllers and kinematic shape sweeps. Broadphase
525
+ swept-AABB against both BVHs; per-candidate AABB-slab interval
526
+ narrowing + coarse step + GJK bisection for time-of-impact. The
527
+ output `result.normal` is the true contact-surface normal at the
528
+ kiss point, computed by re-running GJK + EPA at `best_t` on the
529
+ winning candidate (falls back to `-ray.direction` only on EPA
530
+ degeneracies).
531
+ - [x] **`compute_penetration(out_direction, shape_a, pos_a, rot_a,
532
+ shape_b, pos_b, rot_b)`** — standalone geometry primitive (no
533
+ PhysicsSystem) for resolving overlap between two shapes at given
534
+ poses. Returns depth + outward direction. Convex × convex via
535
+ GJK + EPA; convex × concave via per-triangle half-space test.
536
+
537
+ ---
538
+
539
+ ## Future / out-of-scope
540
+
541
+ These are explicit architectural exclusions or post-v1 explorations.
542
+
543
+ ### Architecture
544
+ - **Cross-runtime bit-exact determinism**: a soft-float library would
545
+ replace `Math.sin/cos/exp/log/pow` in the hot path. The codebase is
546
+ already structured to make this a swap-in at `quat_integrate.js` and
547
+ tangent-basis construction in `build_manifold.js`. Not pursued because
548
+ the same-runtime determinism we have covers the common cases (single-
549
+ device replay, networked lockstep where all clients run the same JS
550
+ engine).
551
+ - **WASM / SIMD**: the engine targets pure-JS portability. SIMD would
552
+ invalidate the determinism story (V8 doesn't expose deterministic
553
+ Float64x2 ops).
554
+ - **Multi-threaded solver**: workers don't share memory cheaply without
555
+ `SharedArrayBuffer` plus the COOP/COEP HTTP headers, which are not
556
+ always available. Single-threaded is good-enough for the awake-body
557
+ budget that matters.
558
+
559
+ ### Simulation extensions
560
+ - **Soft body / cloth / fluids**: the SoA layout in `BodyStorage` and the
561
+ manifold cache are rigid-body shaped. A soft-body system would be a
562
+ parallel subsystem, not an extension.
563
+ - **Reduced-coordinate articulations** (MuJoCo / Featherstone-style):
564
+ game-physics audience runs in maximal coordinates by convention. Not
565
+ on the roadmap.
566
+
567
+ ### Game-side
568
+ - **Vehicle physics** (suspensions, drivetrains): a domain layer that
569
+ sits on top of the rigid-body primitives, not in `meep/`.
570
+ - **Character controllers**: same — `engine/control/first-person/` is the
571
+ natural home.
572
+
573
+ ---
574
+
575
+ ## Notable design files
576
+
577
+ - Original design plan: `C:\Users\Alex\.claude\plans\let-s-plan-to-implement-transient-harp.md`
578
+ - This file (state of play): `engine/physics/PLAN.md`