@woosh/meep-engine 2.139.0 → 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 (172) 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_to_matrix3.d.ts +54 -0
  9. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  11. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  12. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  13. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  14. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  15. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  16. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  17. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  18. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  20. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  21. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  22. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  23. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  24. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  25. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  26. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  27. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  29. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  30. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  31. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  32. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  41. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  42. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  43. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  44. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  45. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  46. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  47. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  48. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  49. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  50. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  53. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  54. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  55. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  56. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  57. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  59. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  60. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  62. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  63. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  64. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  65. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  66. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  71. package/src/engine/control/first-person/TODO.md +173 -127
  72. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  73. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  74. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  75. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  76. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  77. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  78. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  79. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  80. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  81. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  82. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  83. package/src/engine/physics/PLAN.md +461 -236
  84. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  85. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  86. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  87. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  88. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  89. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  90. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  91. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  92. package/src/engine/physics/ecs/PhysicsSystem.d.ts +128 -20
  93. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  94. package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
  95. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  96. package/src/engine/physics/fluid/FluidSimulator.js +2 -1
  97. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -6
  98. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  99. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +39 -17
  100. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  101. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  102. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  103. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  104. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  105. package/src/engine/physics/gjk/gjk.js +421 -378
  106. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  107. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  108. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  109. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  110. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  111. package/src/engine/physics/gjk/mpr.js +344 -0
  112. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  113. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  114. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  115. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  116. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  117. package/src/engine/physics/integration/integrate_position.js +43 -12
  118. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  119. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  120. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  121. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  122. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  123. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  124. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  125. package/src/engine/physics/narrowphase/box_box_manifold.js +113 -17
  126. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  127. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  128. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  129. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  130. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  131. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  132. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  133. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  134. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  135. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  136. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  137. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  138. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  139. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  140. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  141. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  142. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  143. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  144. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  145. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  146. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  147. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  148. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  149. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  150. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  151. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  152. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +8 -2
  153. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  154. package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -382
  155. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  156. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  157. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  158. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  159. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  160. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  161. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  162. package/src/engine/physics/queries/overlap_shape.js +183 -0
  163. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  164. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  165. package/src/engine/physics/queries/shape_cast.js +387 -0
  166. package/src/engine/physics/solver/solve_contacts.d.ts +116 -30
  167. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  168. package/src/engine/physics/solver/solve_contacts.js +641 -223
  169. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  170. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  171. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  172. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -1,236 +1,461 @@
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). The fix is
261
+ per-substep contact re-detection for pairs involving a concave body
262
+ (re-running narrowphase, or at least re-selecting the deepest triangle,
263
+ inside the substep loop) while convex pairs keep the cheap analytic
264
+ refresh — a hybrid the substep architecture already accommodates. Medium+
265
+ *perfectly* axis-aligned cube stacks can fail to fully settle (and so never
266
+ sleep), but *erratically* with height and exact placement — a 5-stack may
267
+ jitter while a 7-stack sleeps, and a sub-mm gap flips the outcome. That
268
+ chaotic, configuration-sensitive signature is box-box contact-point jitter
269
+ (Sutherland-Hodgman clipping selecting different points frame to frame),
270
+ NOT the solver — confirmed by running the same stacks through both the
271
+ single-step and TGS paths. It's the separate box-box-manifold robustness /
272
+ stable-feature-ID backlog item, independent of TGS. Realistic (slightly
273
+ perturbed, mixed-shape) stacks are unaffected; mass ratios up to ~100:1 and
274
+ 4-/7-/8-cube aligned stacks settle and sleep cleanly under TGS.
275
+
276
+ ---
277
+
278
+ ## Backlog (planned, in scope)
279
+
280
+ ### Solver quality (next major work)
281
+
282
+ These items move the engine from "competent" to "great". TGS is the next
283
+ significant solver-architecture change; joints come after, once the TGS
284
+ scaffolding is in place.
285
+
286
+ - **TGS (Temporal Gauss-Seidel) substepping with split-impulse** — Phases
287
+ 1–3 **LANDED**. The solver is now a staged TGS pipeline
288
+ (`solver/solve_contacts.js`: `prepare_contacts` → per substep
289
+ [`refresh_contacts` → `warm_start_contacts` → `solve_velocity` →
290
+ `solve_position`] → `apply_restitution`), driven by the substep loop in
291
+ `PhysicsSystem.fixedUpdate`. Defaults: `substeps = 4`,
292
+ `velocityIterations = 4`, `positionIterations = 1` (all fields on
293
+ `PhysicsSystem`).
294
+ - **Phase 1 — split impulse.** Position correction runs on a per-body
295
+ pseudo-velocity (`__pseudo_velocity`) folded into the pose by
296
+ `integrate_position` and discarded; depth correction never
297
+ contaminates persistent velocity.
298
+ - **Phase 2 — one-shot restitution.** Velocity pass is pure
299
+ non-penetration; restitution is a single post-loop pass driving
300
+ `vn → -e·vn_approach`, gated on a running max normal impulse
301
+ (`maxNormalImpulse`) so transient collisions still bounce under
302
+ per-substep warm-start.
303
+ - **Phase 3 — substep loop.** `substeps` sub-iterations at `h = dt/N`.
304
+ Forces consumed once at full `dt` before the loop; gravity applied
305
+ per substep; **warm-start replayed per substep** (the crux — a
306
+ per-substep impulse balances one substep of gravity, so resting
307
+ stacks hold at zero velocity). Contact geometry is re-derived
308
+ **analytically** each substep from frozen local witness anchors +
309
+ the trusted prepare-time depth (a sign-robust delta), so narrowphase
310
+ runs **once** per outer step — cheaper than the originally-planned
311
+ per-substep match-and-merge refresh, and exact for convex
312
+ primitives whose contact feature is stable under small motion.
313
+
314
+ Results vs the single-step solver: a 100:1 mass ratio now stacks
315
+ instead of crushing through (regression test added); 8-cube stacks
316
+ settle to zero velocity and sleep (were impossible long-term under SI);
317
+ falling-tower bench cost unchanged (~48 ms/1000 active bodies);
318
+ `substeps = 1` reproduces the single-step result bit-for-bit-ish
319
+ (one-frame restitution delay aside).
320
+
321
+ **Hard-won lessons (for REVIEW_002):**
322
+ - Warm-start MUST be per-substep, not once. Replaying a full-frame
323
+ impulse once while gravity arrives per substep over-pushes resting
324
+ contacts and *explodes* deep stacks. Per-substep warm-start +
325
+ per-substep gravity cancel exactly at rest.
326
+ - Restitution must gate on the *running max* normal impulse, not the
327
+ end-of-loop value — per-substep warm-start relaxes a transient
328
+ contact's `j_n` back to ~0 by the end, which would suppress the
329
+ bounce.
330
+ - Analytic separation re-derivation beats per-substep narrowphase
331
+ for convex shapes (cheaper, no manifold-lifecycle churn) but is
332
+ only as good as the frozen normal — see the concave caveat below.
333
+
334
+ Remaining (Phases 4–6, backlog):
335
+ - More regression coverage: heavy-on-light *pyramid*, a
336
+ ragdoll-stub once joints exist.
337
+ - **Per-substep contact re-detection for concave pairs** to lift the
338
+ dynamic-concave-body limitation (see Limitations) and un-skip the
339
+ torus-knot dynamic-settle test. The analytic refresh freezes *which*
340
+ triangle is the contact feature across substeps, which is wrong for
341
+ a body rocking on a mesh; convex pairs keep the cheap analytic path.
342
+ - REVIEW_002 retrospective.
343
+
344
+ References: Catto 2018 ("Soft Constraints" GDC talk + the TGS
345
+ follow-up); Box2D v3 source (`b2ApplyRestitution`, the substep solver
346
+ stages); Rapier as the closest architectural sibling.
347
+
348
+ - [ ] **Joints** (distance, hinge, ball-socket, prismatic — and beyond).
349
+ *To be refined when we get to it.* Joints want the TGS substep
350
+ iteration model in place first — joint-chain convergence is a TGS
351
+ sweet spot and a PGS pain point, and any constraint structures
352
+ written against PGS today become migration cost once TGS lands.
353
+ The solver loop is already set up to iterate
354
+ `contacts ∪ joints` and the manifold-style impulse persistence is
355
+ there; what's missing is the constraint structures themselves,
356
+ joint-limit handling, the motor / soft-constraint surface, and
357
+ the authoring API. Plan the phased breakdown once TGS lands —
358
+ until then this stays as a visible dependency placeholder.
359
+
360
+ ### Stability
361
+ - [ ] **Closed-form triangle-vs-primitive solvers**
362
+ (`triangle_sphere_contact`, `triangle_box_contact`,
363
+ `triangle_capsule_contact`). The decomposition machinery is in
364
+ place (`Triangle3D` flyweight, `heightmap_enumerate_triangles` /
365
+ `mesh_enumerate_triangles`, `decompose_to_triangles` dispatcher,
366
+ `aabb_world_to_local`, `narrowphase_step.js` concave branch), but
367
+ the per-triangle narrowphase uses GJK + EPA which hits the
368
+ smooth-shape iteration cap and `Triangle3D`'s degenerate-support
369
+ issue. Closed-form solvers per primitive bypass both. This is now
370
+ the single biggest accuracy gap in the engine — it would:
371
+ - Unblock the `narrowphase_concave.spec.js` skipped tests (ball
372
+ drops on heightmap / mesh-cube settle correctly).
373
+ - Unblock the `PhysicsSystem.spec.js` torus-knot test.
374
+ - Improve `compute_penetration`'s closed-mesh accuracy (currently
375
+ documented over-reports on side faces).
376
+ Existing primitive pair solvers (`sphere_box_contact`,
377
+ `capsule_box_multi_contacts`, `box_box_manifold`) are the
378
+ blueprint. Triangle is roughly a box with two half-extents = 0.
379
+ - [ ] **Edge-edge multi-point manifold** for skewed box contacts.
380
+ - [ ] **Per-contact source-collider tracking** so multi-material compound
381
+ bodies get accurate per-contact friction/restitution. Requires
382
+ stashing the collider identity in the manifold contact stride.
383
+
384
+ ### Performance / Scale
385
+ - [ ] **Per-body linear CCD shape-cast**: optional opt-in for fast-moving
386
+ bodies where speculative margin isn't enough. The bench's falling
387
+ tower (1km drop onto a 1cm floor) is the concrete reproducer —
388
+ 180 / 1000 bodies tunnel.
389
+ - [ ] **Per-island parallel solve**: today's island data layout would
390
+ allow worker-based solving once `SharedArrayBuffer` is available.
391
+ Out-of-scope unless / until SAB is universally usable.
392
+
393
+ ### Features
394
+ - [ ] **Convex hull shape** with eigen-based principal-axes inertia
395
+ derivation. Hooks `matrix_eigenvalues_in_place` from the existing
396
+ linalg layer.
397
+ - [ ] **Cylinder / cone shapes** (closed-form pairs against the existing
398
+ family + GJK+EPA fallback for general convex).
399
+
400
+ ### API polish
401
+ - [x] **`overlap(shape, position, rotation, output, output_offset,
402
+ filter?)`** — broadphase + narrowphase overlap query for kinematic
403
+ / AOE / selection use cases. Body_ids written into a caller-sized
404
+ Uint32Array buffer. Convex query shape only; concave candidates
405
+ are routed through the per-triangle decomposition path.
406
+ - [x] **`shapeCast(ray, shape, rotation, result, filter?)`** for
407
+ character controllers and kinematic shape sweeps. Broadphase
408
+ swept-AABB against both BVHs; per-candidate AABB-slab interval
409
+ narrowing + coarse step + GJK bisection for time-of-impact. The
410
+ output `result.normal` is the true contact-surface normal at the
411
+ kiss point, computed by re-running GJK + EPA at `best_t` on the
412
+ winning candidate (falls back to `-ray.direction` only on EPA
413
+ degeneracies).
414
+ - [x] **`compute_penetration(out_direction, shape_a, pos_a, rot_a,
415
+ shape_b, pos_b, rot_b)`** — standalone geometry primitive (no
416
+ PhysicsSystem) for resolving overlap between two shapes at given
417
+ poses. Returns depth + outward direction. Convex × convex via
418
+ GJK + EPA; convex × concave via per-triangle half-space test.
419
+
420
+ ---
421
+
422
+ ## Future / out-of-scope
423
+
424
+ These are explicit architectural exclusions or post-v1 explorations.
425
+
426
+ ### Architecture
427
+ - **Cross-runtime bit-exact determinism**: a soft-float library would
428
+ replace `Math.sin/cos/exp/log/pow` in the hot path. The codebase is
429
+ already structured to make this a swap-in at `quat_integrate.js` and
430
+ tangent-basis construction in `build_manifold.js`. Not pursued because
431
+ the same-runtime determinism we have covers the common cases (single-
432
+ device replay, networked lockstep where all clients run the same JS
433
+ engine).
434
+ - **WASM / SIMD**: the engine targets pure-JS portability. SIMD would
435
+ invalidate the determinism story (V8 doesn't expose deterministic
436
+ Float64x2 ops).
437
+ - **Multi-threaded solver**: workers don't share memory cheaply without
438
+ `SharedArrayBuffer` plus the COOP/COEP HTTP headers, which are not
439
+ always available. Single-threaded is good-enough for the awake-body
440
+ budget that matters.
441
+
442
+ ### Simulation extensions
443
+ - **Soft body / cloth / fluids**: the SoA layout in `BodyStorage` and the
444
+ manifold cache are rigid-body shaped. A soft-body system would be a
445
+ parallel subsystem, not an extension.
446
+ - **Reduced-coordinate articulations** (MuJoCo / Featherstone-style):
447
+ game-physics audience runs in maximal coordinates by convention. Not
448
+ on the roadmap.
449
+
450
+ ### Game-side
451
+ - **Vehicle physics** (suspensions, drivetrains): a domain layer that
452
+ sits on top of the rigid-body primitives, not in `meep/`.
453
+ - **Character controllers**: same — `engine/control/first-person/` is the
454
+ natural home.
455
+
456
+ ---
457
+
458
+ ## Notable design files
459
+
460
+ - Original design plan: `C:\Users\Alex\.claude\plans\let-s-plan-to-implement-transient-harp.md`
461
+ - This file (state of play): `engine/physics/PLAN.md`