@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
@@ -0,0 +1,642 @@
1
+ # REVIEW_001 — Action Plan
2
+
3
+ Plan derived from the consolidated executive summary of three independent
4
+ deep-reviewer reports (Bullet / Jolt / Rapier — all on opus, each
5
+ instructed to read both sides line-by-line and write to disk).
6
+
7
+ Source reviews live alongside this file:
8
+
9
+ - `BULLET_REVIEW.md`
10
+ - `JOLT_REVIEW.md`
11
+ - `RAPIER_REVIEW.md`
12
+
13
+ Each priority below cites the convergent findings from those reports and
14
+ breaks the work into concrete actionable units with verification
15
+ criteria. Effort estimates match the executive summary's prioritisation
16
+ table.
17
+
18
+ ---
19
+
20
+ ## P0 — Correctness blockers (hours)
21
+
22
+ ### P0.1 — Fix warm-start wipe
23
+
24
+ **Status**: LANDED, activated alongside P1.2.
25
+
26
+ **Bug.** Every frame for every active manifold slot, `dispatch_pair`'s
27
+ accumulator path calls `manifolds.clear_contacts(slot)`. `clear_contacts`
28
+ at `ManifoldStore.js:233` does `this.__data.fill(0, data_off, data_off + SLOT_DATA_STRIDE)`
29
+ — zeroing the FULL slot stride, including offsets 10/11/12 (`j_n`,
30
+ `j_t1`, `j_t2`). `set_contact` at line 258 then only writes offsets 0..9
31
+ (position / normal / depth), leaving the warm-start impulses at zero.
32
+ The comment at line 270 — `// j_n, j_t1, j_t2 are warm-start; preserved
33
+ across calls` — is true at the per-call level but contradicted by the
34
+ upstream wipe. Solver's warm-start sees zero accumulated impulses every
35
+ frame.
36
+
37
+ **Fix path (infrastructure landed).** `ManifoldStore` now exposes two
38
+ operations:
39
+
40
+ 1. `begin_refill(slot)`: resets the contact count to zero (so the
41
+ next `set_contact(slot, 0, …)` lands at index 0) but does NOT zero
42
+ the data slab. Warm-start impulses survive. **Implemented.**
43
+ 2. `clear_contacts(slot)` is unchanged (genuine eviction zeroes
44
+ everything — still used at line 672 when narrowphase determines
45
+ the pair has no colliders).
46
+
47
+ **Activation blocked.** The narrowphase callsite at line 687
48
+ intentionally still calls `clear_contacts`. We tried flipping it to
49
+ `begin_refill` and ran the full physics spec suite — the 4-cube
50
+ atomic-sleep test (`PhysicsSystem.spec.js`) fails because the stack
51
+ **collapses** within ~3 simulated seconds. Diagnostic capture at tick
52
+ 794:
53
+
54
+ | cube | y (expected) | y (actual) | vy | sleep_timer |
55
+ |------|--------------|------------|--------------|-------------|
56
+ | 0 | 0.5 | 0.4996 | 0 | 0.504 → SLEEPING (singleton) |
57
+ | 1 | 1.5 | 0.4983 | ~1e-9 | 0.083 |
58
+ | 2 | 2.5 | 0.689 | +0.21 | 0 |
59
+ | 3 | 3.5 | 0.707 | bouncing | 0 |
60
+
61
+ Cube 1 has sunk **1 m below** its proper resting position (interpenetrating
62
+ cube 0 by a whole edge). The reviewer's assumption "slot ordering is
63
+ usually preserved" is false: `reduce_candidates` (`narrowphase_step.js:104`)
64
+ reorders contacts by depth + max-spread, so the cached `j_n` at slot
65
+ index `i` next frame is applied to a **different geometric contact**
66
+ than the one whose impulse converged last frame. Stale impulses applied
67
+ to wrong contacts destabilise the solver immediately on stacks.
68
+
69
+ **Conclusion: P0.1 was bundled with P1.2.** Both landed together: the
70
+ `begin_refill` primitive replaced `clear_contacts` at the narrowphase
71
+ refill callsite, while P1.2's match-and-merge pass ensures the cached
72
+ impulses follow the right contact slot index across the per-frame
73
+ reshuffle that `reduce_candidates` introduces. The 4-cube atomic-sleep
74
+ test (which collapsed when `begin_refill` was activated without
75
+ matching) now passes with warm-start active.
76
+
77
+ **Verification (landed).**
78
+ - `ManifoldStore.spec.js`: asserts `j_n` / `j_t1` / `j_t2` survive a
79
+ `begin_refill` + `set_contact` cycle; `clear_contacts` still wipes
80
+ them; multi-contact preservation works.
81
+ - `PhysicsSystem.spec.js`: 4-cube atomic-sleep test, 16-cube short
82
+ window stack, and all dependent integration tests pass with warm
83
+ start active.
84
+
85
+ ---
86
+
87
+ ### P0.2 — Remove `debugger;` from EPA hot path
88
+
89
+ **Status**: confirmed at `gjk/expanding_polytope_algorithm.js:136`.
90
+
91
+ A `debugger;` statement is sitting in the EPA expansion loop. Production
92
+ ship-blocker. Likely a leftover from a prior debug session.
93
+
94
+ **Fix.** Delete line 136 (and 135 / 137 if they're related guard /
95
+ comment). Confirm by grepping `^\s*debugger` across the package — there
96
+ should be zero hits afterward.
97
+
98
+ **Verification.** `grep -rn "debugger" src/engine/physics/` returns
99
+ nothing.
100
+
101
+ **Effort**: minutes.
102
+
103
+ ---
104
+
105
+ ## P1 — Largest accuracy/correctness wins (days)
106
+
107
+ ### P1.1 — Closed-form sphere / box / capsule-vs-triangle
108
+
109
+ **Status**: LANDED in three sub-commits — P1.1a (sphere), P1.1b
110
+ (box), P1.1c (capsule).
111
+
112
+ **Problem.** The narrowphase concave dispatch (added in commit 28ef0e894)
113
+ decomposes heightmaps / meshes to triangles and runs per-triangle GJK +
114
+ EPA. `Triangle3D` has a degenerate support along its face-normal axis
115
+ (all 3 vertices project to the same value), so GJK precision is poor —
116
+ hence the skipped settle tests in `narrowphase_concave.spec.js` and the
117
+ documented sink-through-heightmap behaviour. `compute_penetration`
118
+ already works around this for sphere-vs-heightmap by using the
119
+ half-space test on the face plane, but the narrowphase still uses
120
+ per-triangle GJK+EPA.
121
+
122
+ **Files to add** (suggested layout, paralleling existing
123
+ `sphere_box_contact.js` / `capsule_contacts.js`):
124
+
125
+ - `engine/physics/narrowphase/sphere_triangle_contact.js` ✅ LANDED —
126
+ closest-point on triangle (uses
127
+ `computeTriangleClosestPointToPointBarycentric`), distance to sphere
128
+ centre, depth = R − dist when overlap. Output stride matches
129
+ `sphere_box_contact`. Includes upfront degenerate-triangle guard
130
+ (zero-area triangle returns false) and dist=0 fallback to face
131
+ normal. 11 unit tests covering face, edge, vertex regions,
132
+ separation, tangent, singular, degenerate, tilted, indexing.
133
+ - `engine/physics/narrowphase/box_triangle_contact.js` ✅ LANDED —
134
+ SAT over 13 axes (3 box face normals + 1 triangle normal + 9 edge-edge
135
+ crosses) with the corrected asymmetric `min(push_pos, push_neg)`
136
+ per-axis MTV (triangle projection is NOT symmetric around its
137
+ centroid, unlike box's). Three contact-generation branches:
138
+ - **Box face winner** → reference = box face, incident = triangle.
139
+ Project triangle into the face's (u, v) basis, clip with 4 axis-aligned
140
+ Sutherland-Hodgman passes, recover world contacts on the triangle
141
+ plane.
142
+ - **Triangle face winner** → reference = triangle, incident = box
143
+ face most antiparallel to contact normal. Project the box quad
144
+ onto the triangle plane (basis = AB normalised + tn × AB), clip
145
+ with 3 general-half-plane passes against the triangle's 3 edges,
146
+ recover world contacts on the box face plane.
147
+ - **Edge-cross winner** → identify the relevant box edge (one of 4
148
+ parallel edges, selected by sign of `-n` projected onto the two
149
+ perpendicular box axes) and triangle edge, find closest pair via
150
+ `line3_closest_points_segment_segment`, emit single contact.
151
+
152
+ 12 unit tests covering separation, degenerate triangle, box face
153
+ winner (single + multi-point), triangle face winner, deep
154
+ penetration, +/-Y face flip, self-consistency reconstruction
155
+ (`tri_pt = box_pt + depth * n`), rotated box, near-corner case,
156
+ translated box, output indexing.
157
+ - `engine/physics/narrowphase/capsule_triangle_contact.js` ✅ LANDED —
158
+ multi-point manifold via primary segment-vs-triangle closest-pair
159
+ + cap-centre sphere queries. The `segment_triangle_closest` helper
160
+ takes the minimum of 6 candidates: (case 0) segment crosses
161
+ triangle plane inside the face → distance 0; (cases 1, 2) each
162
+ segment endpoint vs `computeTriangleClosestPointToPointBarycentric`;
163
+ (cases 3, 4, 5) `line3_closest_points_segment_segment` against each
164
+ triangle edge. Primary contact emitted from the closest pair; each
165
+ cap centre then runs `sphere_triangle_contact` and is spatially
166
+ deduplicated against the primary. Result: 1..3 contacts.
167
+ Pattern matches `capsule_box_multi_contacts`. 10 unit tests.
168
+
169
+ **Wiring.** Inside `narrowphase_step.js`'s concave branch (the one that
170
+ calls `decompose_to_triangles`), per-triangle dispatch by convex
171
+ shape type:
172
+
173
+ ```js
174
+ if (convex_shape.isUnitSphereShape3D) → per-triangle sphere_triangle_contact [LANDED]
175
+ else if (convex_shape.isBoxShape3D) → per-triangle box_triangle_contact [LANDED]
176
+ else if (convex_shape.isCapsuleShape3D) → per-triangle capsule_triangle_contact [LANDED]
177
+ else → existing per-triangle GJK+EPA fallback
178
+ ```
179
+
180
+ Each closed-form path reads the triangle's three vertices from the
181
+ decomposition buffer at stride 10, rotates them to world space (3
182
+ quat rotations per triangle — ~63 flops, negligible vs. the GJK + EPA
183
+ cost being replaced), runs its solver, then runs the same one-sided
184
+ face-normal rejection and normal-based dedup as the EPA path. The
185
+ closed-form paths use ACTUAL surface witnesses (closest point on
186
+ triangle, sphere surface point) rather than body centres — exact in
187
+ both directions, unlike the EPA fallback where support witnesses can
188
+ be arbitrarily far from the contact patch.
189
+
190
+ **Verification (P1.1 fully landed).**
191
+ - 11 unit tests in `sphere_triangle_contact.spec.js`.
192
+ - 12 unit tests in `box_triangle_contact.spec.js`.
193
+ - 10 unit tests in `capsule_triangle_contact.spec.js`.
194
+ - The two sphere settle tests in `narrowphase_concave.spec.js`
195
+ un-skipped and passing.
196
+ - The torus-knot test in `PhysicsSystem.spec.js` un-skipped and
197
+ passing.
198
+ - Full physics suite: 686 passing (was 650 pre-P1.1a — delta:
199
+ +11 sphere_triangle + 12 box_triangle + 10 capsule_triangle unit
200
+ tests + 2 un-skipped sphere settle tests + 1 un-skipped torus-knot
201
+ test).
202
+
203
+ **Optional follow-up** (not blocking).
204
+ - Tighten `compute_penetration.spec.js`'s mesh-cube test (currently
205
+ documented as approximate due to closed-mesh over-reporting on side
206
+ faces). Note: `compute_penetration` is a separate code path from
207
+ `narrowphase_step.js` — it uses its own GJK+EPA / MPR pipeline and
208
+ the half-space workaround; landing the narrowphase fast-path
209
+ doesn't change it. Refactoring `compute_penetration` to share the
210
+ sphere / box / capsule-triangle fast-paths is a separate cleanup.
211
+
212
+ **Effort**: LANDED.
213
+
214
+ ---
215
+
216
+ ### P1.2 — Feature-id contact tracking across frames
217
+
218
+ **Status**: LANDED.
219
+
220
+ **Problem.** Even after the P0 warm-start wipe is fixed, the manifold
221
+ slot reuses contact-index ordering across frames. Contact slice indices
222
+ shift due to:
223
+
224
+ - Depth-spread reduction in `reduce_candidates` (`narrowphase_step.js:104`)
225
+ — picks deepest, then max-spread; ordering depends on candidate
226
+ generation order which depends on shape-pair details.
227
+ - Box-box face clipping reorders contacts based on which clipped
228
+ vertices survive (`box_box_manifold.js`).
229
+ - The triangle decomposition emits cells in row-major order — a sphere
230
+ rolling along a heightmap sees different cell indices each frame.
231
+
232
+ Reviewers cite two reference designs:
233
+
234
+ 1. **Parry's `TrackedContact { fid1, fid2 }`** — each contact carries
235
+ feature ids on both sides; the cache matches by `(fid1, fid2)` pair.
236
+ 2. **Jolt's `mContactPointPreserveLambdaMaxDistSq`** — match by
237
+ local-space position proximity (default 0.04 m squared).
238
+
239
+ We're already laid out for (1): the decomposition pipeline emits stable
240
+ feature ids per triangle, and the convex primitive narrowphase paths
241
+ can derive feature ids from vertex / edge / face indices.
242
+
243
+ **What landed.**
244
+
245
+ 1. `CONTACT_STRIDE` extended from 13 to 14 (added `feature_id` at
246
+ offset 13). `CANDIDATE_STRIDE` extended from 10 to 11 in
247
+ `narrowphase_step.js` (mirroring slot layout).
248
+ 2. `set_contact` gained an optional `feature_id` parameter (default 0
249
+ — preserves backwards-compatible call signatures).
250
+ 3. New `clear_impulses(slot, idx)` method on `ManifoldStore`: zeroes
251
+ only the j_n / j_t1 / j_t2 fields at one contact index. Used by
252
+ the match-and-merge pass when a new candidate has no matching
253
+ prev-frame contact.
254
+ 4. New `feature_id_of(slot, idx)` accessor for testability.
255
+ 5. Per-dispatch feature_id derivation in `narrowphase_step.js`:
256
+ - `sphere_sphere`: fid = 1 (single contact, real feature)
257
+ - `sphere_box`: fid = voronoi region 1..27 via new
258
+ `sphere_box_voronoi_fid` helper (inverse-rotates sphere centre
259
+ to box-local frame, buckets each component into {<-h, [-h,h], >h})
260
+ - `box_box`: fid = 0 (clipped-contact ordering isn't cross-frame
261
+ stable; position-fallback handles per-contact matching)
262
+ - `capsule_capsule`, `capsule_sphere`: fid = 1
263
+ - `capsule_box` multi-point: fid = k+1 (per-sub-contact index)
264
+ - Triangle (concave) path: fid = triangle's pre-existing
265
+ decomposition fid
266
+ - GJK+EPA fallback: fid = 0 (no good source)
267
+ 6. Match-and-merge pass replaces `clear_contacts(slot)` at the
268
+ narrowphase refill callsite:
269
+ - Snapshots prev contacts' `(feature_id, world_a, j_n, j_t1, j_t2)`
270
+ upfront into scratch (decouples read from write to avoid hazards
271
+ when the matching mapping shuffles indices)
272
+ - For each new candidate, finds best prev match — feature_id first
273
+ (only if both sides have fid ≠ 0), position-fallback within
274
+ `MATCH_TOL_SQR = 0.02 * 0.02` second
275
+ - Each prev contact can be claimed by at most one candidate
276
+ (`prev_claimed[]` flags)
277
+ - Calls `begin_refill(slot)` (count → 0, data preserved)
278
+ - Writes each new candidate at slot index k; copies matched prev
279
+ impulses to slot index k OR calls `clear_impulses(slot, k)` for
280
+ unmatched candidates
281
+ 7. Separation path (cand_count === 0) calls `clear_contacts(slot)` —
282
+ genuine eviction, drops stale impulses before contact is
283
+ re-established at a different feature.
284
+
285
+ **Verification.**
286
+ - `ManifoldStore.spec.js`: 5 new tests covering `set_contact`
287
+ feature_id round-trip, default fid = 0, impulse-preservation under
288
+ fid'd writes, `clear_impulses` scoping (one index, geometry +
289
+ feature_id intact), and per-index targeting.
290
+ - `PhysicsSystem.spec.js`: 4-cube atomic-sleep test passes (was the
291
+ regression that gated P0.1 alone — confirms warm-start activation
292
+ is now stable for stacks).
293
+ - Full physics test suite: 650 passing (was 645 pre-P1.2 — the
294
+ delta is the 5 new ManifoldStore tests; the 4-cube atomic-sleep
295
+ test which transiently regressed under P0.1-alone is now stable).
296
+
297
+ **Effort**: landed.
298
+
299
+ ---
300
+
301
+ ## P2 — Performance + precision (day-scale)
302
+
303
+ ### P2.1 — GJK separating-axis cache per manifold slot
304
+
305
+ **Status**: LANDED (second attempt). The first attempt regressed basic
306
+ settle tests and was reverted; this second pass landed the same
307
+ optimisation via a different mechanism (no try/finally) in 6 inert /
308
+ single-activation steps, each independently verified.
309
+
310
+ **Mechanism — `gjk_core` + `gjk_with_axis`.** Instead of try/finally,
311
+ gjk's body was extracted into `gjk_core(simplex, A, B, dir)` taking
312
+ the search-direction buffer as a parameter. The existing
313
+ `gjk(simplex, A, B)` is now a thin wrapper that sets the module's
314
+ `scratch_dir` to `(1, 0, 0)` and delegates — semantically identical to
315
+ the old code. The new `gjk_with_axis(simplex, A, B, axis_buf, axis_off)`
316
+ creates a `subarray` view into `axis_buf[axis_off..+2]` and passes it
317
+ to `gjk_core`; in-place writes to the view propagate to the caller's
318
+ buffer automatically, so no try/finally writeback is needed. A guard
319
+ catches zero / NaN / Inf seeds and resets to `(1, 0, 0)`.
320
+
321
+ **Storage — `ManifoldStore.__slot_axis`.** A `Float64Array(capacity * 3)`
322
+ parallel to the data buffer. Zeroed on slot `acquire` (so a recycled
323
+ slot doesn't leak the previous pair's axis). Grows alongside the
324
+ other slot arrays. Public accessors `slot_axis_buffer` and
325
+ `slot_axis_offset(slot)`.
326
+
327
+ **Activation — narrowphase.** The outer narrowphase loop computes
328
+ `(gjk_axis_buf, gjk_axis_off)` from the slot and passes them through
329
+ `dispatch_pair`. Both GJK + EPA fallback paths (body-level for
330
+ non-closed-form convex pairs; per-triangle for non-(sphere/box/
331
+ capsule) convex shapes against concave shapes) call `gjk_with_axis`
332
+ with these arguments.
333
+
334
+ **Sequencing — six steps with independent verification.** Each step
335
+ either had no behaviour change (1, 3, 4) or was a small enough
336
+ activation that a regression would be precisely revertable (5, 6).
337
+ Verified at every step against the full physics suite:
338
+
339
+ | Step | Change | Tests passing |
340
+ |---|---|---|
341
+ | 1 | Extract `gjk_core` from `gjk`; existing wrapper preserves semantics | 687 / 687 |
342
+ | 2 | Add `gjk_with_axis` + 6 unit tests | 693 / 693 |
343
+ | 3 | Add `ManifoldStore.__slot_axis` (dead code) | 693 / 693 |
344
+ | 4 | Plumb axis args through `dispatch_pair` (dead code) | 693 / 693 |
345
+ | 5 | Activate at the body-level GJK + EPA fallback | 693 / 693 |
346
+ | 6 | Activate at the per-triangle EPA path in concave dispatch | 693 / 693 |
347
+
348
+ **Why this attempt worked where the first didn't.** The first attempt
349
+ wrapped the existing gjk body in try/finally to write back the cached
350
+ axis on every return path. That should have been semantically
351
+ identical for existing (cache-less) callers, yet basic settle tests
352
+ regressed. Working theory: V8's JIT shape-inference handled the
353
+ try/finally-wrapped function differently in ways that surfaced in the
354
+ existing physics tests' numerical paths. The second attempt avoids
355
+ try/finally entirely — `gjk_core` takes `dir` as a parameter, and
356
+ writeback is automatic via shared memory (the caller's buffer IS the
357
+ working buffer through a subarray view).
358
+
359
+ **Improvement.** Bullet's `m_cachedSeparatingAxis` and Jolt's `ioV`
360
+ in/out pattern store the last successful separation direction (or
361
+ overlap-resolution direction) per manifold slot. Reuse next frame as
362
+ GJK's initial direction. Reviewers cite 5–10× iteration reduction for
363
+ quiescent persistent contacts — the common case in any settled stack.
364
+
365
+ **Plan.**
366
+
367
+ 1. Add 3 floats to `SLOT_META_STRIDE` (or a parallel `Float32Array(slot_count * 3)`)
368
+ in `ManifoldStore` for the cached axis.
369
+ 2. Add an overload (or new entry point) to `gjk` accepting an initial
370
+ direction. Backward-compatible: existing callers (shape_cast,
371
+ compute_penetration, overlap) pass `null` and default to the
372
+ current `(1, 0, 0)`.
373
+ 3. In `narrowphase_step.js`'s `dispatch_pair`, before the GJK+EPA
374
+ fallback (line 312 area), read the cached axis from the slot; pass
375
+ it to GJK. After GJK terminates (with or without overlap), write
376
+ the final search direction back to the slot.
377
+
378
+ **Verification.**
379
+ - Microbench: count GJK iterations on the existing
380
+ `PhysicsSystem.bench.spec.js` torus-knot scene (currently skipped
381
+ for accuracy but useful here for iteration count). Expect 5–10×
382
+ reduction.
383
+ - Determinism: same scene state → same iteration counts.
384
+
385
+ **Effort**: 1 day.
386
+
387
+ ---
388
+
389
+ ### P2.2 — Adaptive EPA tolerance + bail-out fix
390
+
391
+ **Status**: PARTIALLY LANDED. Adaptive tolerance landed; bail-out
392
+ magnitude consistency reverted (caused energy injection in closed-form
393
+ dispatch paths that share the EPA depth metric — sphere-vs-sphere
394
+ stack regressions launched bodies metres straight up).
395
+
396
+ **Two issues.**
397
+
398
+ 1. `EPA_TOLERANCE = 0.0001` is absolute. For small-depth contacts (sub-mm)
399
+ this is comparable to the depth itself; EPA terminates with whatever
400
+ intermediate face it has, producing a noisy normal direction. Jolt
401
+ uses a relative tolerance scaled by current closest-face distance.
402
+ 2. The bail-out path at line 343 uses `dot_p_search_dir` where the
403
+ converged path uses `min_dist`. Inconsistent under iteration-cap
404
+ exit — the documented "best-effort fallback" returns a different
405
+ magnitude than the converged path of the same geometry.
406
+
407
+ **Plan and outcome.**
408
+
409
+ 1. ✅ LANDED. Tolerance is relative: `EPA_TOLERANCE_REL = 1e-4`
410
+ floored by `EPA_TOLERANCE_ABS = 1e-6`. Terminate when
411
+ `dot_p_search_dir − min_dist < EPS_REL * max(|min_dist|, EPS_ABS)`.
412
+ This fixes EPA's failure-to-converge for sub-mm contacts where the
413
+ old absolute tolerance was larger than the depth itself.
414
+ 2. ❌ REVERTED. Swapping the convergence-path result magnitude from
415
+ `dot_p_search_dir` to `min_dist` (for consistency with the
416
+ iteration-cap bail-out path) was tried — and caused energy
417
+ injection in closed-form dispatch tests that share the EPA depth
418
+ metric (sphere-vs-sphere with restitution 0 launched a ball to 7
419
+ m). The convergence path retains its original magnitude. The
420
+ "inconsistency with bail-out" the reviewer noted is real but
421
+ benign in practice: bail-out fires on smooth-shape EPA failure
422
+ modes that the closed-form paths now bypass entirely (P1.1
423
+ sphere/box/capsule-vs-triangle); the consistency gap doesn't
424
+ surface anywhere observable.
425
+
426
+ **Verification.**
427
+ - EPA spec needs new cases at very small depth (sub-µm) confirming
428
+ termination is now stable and direction is well-defined.
429
+ - The shape_cast bail-out fallback comparison should produce
430
+ consistent normals between converged and non-converged exits.
431
+
432
+ **Effort**: ½ day. Less critical post-P0 because `shape_cast` and
433
+ `compute_penetration` now use MPR for shallow-overlap normal recovery
434
+ — this fix is for the GJK+EPA fallback in `narrowphase_step.js` only.
435
+
436
+ ---
437
+
438
+ ## P3 — Mitigation improvements (half-day each)
439
+
440
+ ### P3.1 — Wire MPR as EPA fallback for mesh-involving pairs
441
+
442
+ **Status**: LANDED.
443
+
444
+ `gjk/mpr.js` was already implemented and tested; `shape_cast.js` and
445
+ `compute_penetration.js` already used it. The two narrowphase EPA call
446
+ sites — the per-triangle EPA path inside the concave dispatch (when
447
+ the convex shape is not sphere/box/capsule) and the body-level
448
+ GJK+EPA fallback — now both fall back to MPR when EPA returns
449
+ zero / negative / NaN / Inf depth. MPR's portal-refinement convergence
450
+ properties tend to succeed where EPA's face-expansion hits its
451
+ iteration cap.
452
+
453
+ **Plan.**
454
+
455
+ 1. In `dispatch_pair`'s GJK+EPA fallback, after EPA returns:
456
+ - If depth > 0 and direction passes the body-centre sanity check
457
+ → keep the EPA result.
458
+ - Else → re-run with MPR (same simplex inputs are not needed; MPR
459
+ starts from the PosedShape pair). If MPR returns true with a
460
+ positive depth, use that.
461
+ 2. For pairs involving a non-convex shape (heightmap/mesh dispatch via
462
+ the concave path), once the closed-form triangle solvers from P1.1
463
+ land, MPR fallback is only needed for the GJK+EPA leg of the
464
+ concave loop — same pattern.
465
+
466
+ **Verification.**
467
+ - The torus-knot test in `PhysicsSystem.spec.js` (currently skipped)
468
+ should make progress with the MPR fallback even before P1.1 lands.
469
+ - `compute_penetration.spec.js`'s smooth-shape direction-precision
470
+ tests can tighten their tolerances.
471
+
472
+ **Effort**: ½ day.
473
+
474
+ ---
475
+
476
+ ### P3.2 — Box-box edge-edge closest-pair manifold
477
+
478
+ **Status**: LANDED.
479
+
480
+ In `box_box_manifold.js`'s edge-cross-axis-winner branch, the previous
481
+ body-centre-midpoint fallback (both contact points collapsed to the
482
+ midpoint of the two box centres — wrong lever arms, slow drift in
483
+ skewed-box-stack scenarios) is replaced with a proper closest-pair
484
+ computation:
485
+
486
+ 1. Decode `(i, j)` from `best_source = 6 + i*3 + j` — box A's local
487
+ axis index and box B's.
488
+ 2. Pick A's edge among 4 parallel edges along axis `i` by signing the
489
+ perpendicular axes against `-n` (the direction from A's centre
490
+ toward B). Same for B with `+n`.
491
+ 3. `line3_closest_points_segment_segment(out_st, edge_a, edge_b)`
492
+ returns `(s, t)`; reconstruct `contact_on_A = edge_a_p1 + s * (edge_a_p2 - edge_a_p1)`
493
+ and similarly for B.
494
+
495
+ The "second contact for near-parallel edges" sub-improvement from the
496
+ original plan turned out to be unnecessary in practice: when two box
497
+ edges are near-parallel, the SAT axis `cross(edge_dir_a, edge_dir_b)`
498
+ is degenerate (magnitude below `PARALLEL_EPS_SQR`) and the test_axis
499
+ helper rejects it before it can win. Near-parallel-edge configurations
500
+ end up with a face-axis SAT winner, which the face-clipping path
501
+ already handles with multiple contacts.
502
+
503
+ **Improvement.** When SAT identifies an edge-edge separating axis
504
+ between two boxes (the cross-product axes in `box_box_manifold.js`),
505
+ the current fallback emits a single contact at the midpoint of the
506
+ edges' projections. The codebase already has
507
+ `core/geom/3d/line/line3_closest_points_segment_segment.js` from a
508
+ previous slice. Use it to emit the proper closest-pair, then evaluate
509
+ spread for a second contact when the edges are near-parallel (within
510
+ some `EDGE_PARALLEL_COS_TOL` like 0.999).
511
+
512
+ **Plan.**
513
+
514
+ 1. In `box_box_manifold.js`'s edge-edge branch, replace the midpoint
515
+ contact with `line3_closest_points_segment_segment`'s output.
516
+ 2. Detect near-parallel edges via the cos-angle test on the edge
517
+ direction vectors; in that case emit a second contact at the other
518
+ endpoint of the parallel overlap interval. Standard SAT-with-clipping
519
+ pattern.
520
+
521
+ **Verification.**
522
+ - A box stacked at an angle on another box should reach steady state
523
+ without the slow drift that the single-midpoint contact causes.
524
+ Add a regression test alongside the existing box-box stack test.
525
+
526
+ **Effort**: ½ day.
527
+
528
+ ---
529
+
530
+ ## P4 — Strategic items (weeks; defer until pressed)
531
+
532
+ ### P4.1 — Split-impulse architecture → TGS substepping
533
+
534
+ **Status**: PLAN.md documents this with the three interacting failure
535
+ modes; reviewers confirmed the path forward in detail.
536
+
537
+ **Recommendation.** Don't pursue until tall stacks become a frequent
538
+ gameplay pain point. The current 4-cube atomic-sleep test passes; the
539
+ 16-cube short-window test catches manifold regressions. The pure-JS
540
+ single-threaded budget is the more likely scaling wall — TGS doesn't
541
+ help there.
542
+
543
+ If/when we do it: Box2D-Lite-style split impulse — separate
544
+ position-correction pseudo-velocity that doesn't contaminate real
545
+ velocity, restitution as one-shot impulse at first contact (not a bias
546
+ inside the loop), forces apply once at full dt before substeps.
547
+
548
+ **Effort**: weeks.
549
+
550
+ ### P4.2 — Per-body linear CCD shape-cast
551
+
552
+ **Status**: PLAN.md backlog ("Per-body linear CCD shape-cast").
553
+
554
+ **Recommendation.** Already a focused future-work item with the 1km
555
+ falling-tower reproducer (~180/1000 bodies tunnel). Worth scheduling
556
+ once a gameplay use case demands it (e.g. fast projectiles, vehicle
557
+ physics).
558
+
559
+ **Effort**: week.
560
+
561
+ ---
562
+
563
+ ## Not prioritised in the executive summary
564
+
565
+ ### Shape-cast bisection vs GJK conservative advancement
566
+
567
+ Reviewers noted Parry's `gjk::directional_distance` (3–5× fewer GJK
568
+ calls per query) but did not prioritise it. Our current bisection +
569
+ slab-narrowing is functionally correct and acceptable; flag for revisit
570
+ if `shape_cast` shows up in profiles. Effort would be a day.
571
+
572
+ ---
573
+
574
+ ## Out of scope
575
+
576
+ Confirmed from the consolidated review — these continue to be valid
577
+ non-goals:
578
+
579
+ - SIMD / WASM — V8 doesn't expose deterministic Float64x2 ops.
580
+ - `SharedArrayBuffer` / multi-threaded solver — COOP/COEP headers
581
+ aren't always available.
582
+ - Cross-runtime bit-exact determinism — would require a soft-float
583
+ library; same-runtime determinism covers single-device replay +
584
+ lockstep clients on matched JS engines.
585
+ - Reduced-coordinate articulations — game-physics audience runs in
586
+ maximal coordinates by convention.
587
+
588
+ ---
589
+
590
+ ## What's already strong (no action; for record)
591
+
592
+ All three reviewers independently called these out as better than at
593
+ least one reference engine. Useful confirmation that the design bets
594
+ in PLAN.md paid off.
595
+
596
+ - End-to-end SoA layout with generation-tracked stable IDs and
597
+ min-heap free list (deterministic ID reuse).
598
+ - Atomic per-island sleep with chain wake via circular doubly-linked
599
+ `sleep_group_next` / `sleep_group_prev` — wake propagation
600
+ completes in one frame, vs Bullet/Jolt's bottom-up multi-frame
601
+ propagation.
602
+ - Deterministic island roots (union-find with union-by-min-index) —
603
+ Rapier and Jolt do not strictly guarantee this.
604
+ - Flat shape hierarchy, linear narrowphase dispatch,
605
+ monomorphic-when-hot `support()`.
606
+ - Coulomb-cone disk friction clamp (true cone) — Bullet's default is
607
+ the anisotropic box clamp.
608
+ - One-sided face-normal rejection on concave dispatch.
609
+
610
+ ---
611
+
612
+ ## Recommended sequencing
613
+
614
+ 1. **P0.2 + P0.1 infra prep (LANDED)** — `debugger;` removed from EPA;
615
+ `ManifoldStore.begin_refill` added. Surgical, no behaviour change.
616
+ The initial attempt at full P0.1 activation (flipping the callsite
617
+ to `begin_refill`) was reverted after the 4-cube atomic-sleep test
618
+ caught a stack collapse, confirming the reviewer's "slot ordering
619
+ is usually preserved" assumption is false for stacks.
620
+ 2. **P1.2 + P0.1 activation (LANDED)** — feature-id contact tracking
621
+ and the match-and-merge pass. Warm-start now active and stable on
622
+ stacks; full physics suite green (650 passing).
623
+ 3. **P1.1 in three sub-commits — LANDED**:
624
+ - **P1.1a (sphere-vs-triangle).** `sphere_triangle_contact` module
625
+ + spec + narrowphase wiring; un-skipped two settle tests in
626
+ `narrowphase_concave.spec.js`.
627
+ - **P1.1b (box-vs-triangle).** `box_triangle_contact` module (SAT
628
+ + clipping for both face winners + closest-pair for edge-cross)
629
+ + spec + narrowphase wiring; un-skipped the torus-knot settle
630
+ test in `PhysicsSystem.spec.js`.
631
+ - **P1.1c (capsule-vs-triangle).** `capsule_triangle_contact`
632
+ module (segment-triangle closest-pair + cap-centre sphere
633
+ queries with spatial dedup) + spec + narrowphase wiring.
634
+ 4. **P2.1 (GJK axis cache) + P2.2 (EPA tolerance) together** — both
635
+ are localised, neither depends on the others.
636
+ 5. **P3.1 (MPR fallback) — LANDED.** Wired at both narrowphase EPA
637
+ call sites (per-triangle concave + body-level fallback). Behind
638
+ the existing depth-validity gate so it only fires when EPA fails.
639
+ 6. **P3.2 (box-box edge-edge) — LANDED.** `line3_closest_points_segment_segment`-based
640
+ contact-pair reconstruction; near-parallel edges already handled
641
+ by SAT degeneracy fallthrough to face axes.
642
+ 7. **P4 items**: defer until gameplay pressure.
@@ -1,4 +1,4 @@
1
- import { aabb_transform_oriented } from "./aabb_transform_oriented.js";
1
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
2
2
 
3
3
  const scratch_local = new Float64Array(6);
4
4
 
@@ -41,7 +41,7 @@ export function compute_fat_world_aabb(
41
41
  const p = transform.position;
42
42
  const q = transform.rotation;
43
43
 
44
- aabb_transform_oriented(
44
+ aabb3_transform_oriented(
45
45
  result, result_offset,
46
46
  scratch_local[0], scratch_local[1], scratch_local[2],
47
47
  scratch_local[3], scratch_local[4], scratch_local[5],