@woosh/meep-engine 2.140.0 → 2.142.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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  3. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  4. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  5. package/src/engine/control/first-person/prototype_first_person_controller.js +5 -0
  6. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.d.ts.map +1 -1
  7. package/src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js +67 -42
  8. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts +12 -22
  9. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.d.ts.map +1 -1
  10. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +340 -186
  11. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts +44 -0
  12. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.d.ts.map +1 -0
  13. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOUpscaleShader.js +151 -0
  14. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts +14 -0
  15. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.d.ts.map +1 -0
  16. package/src/engine/graphics/render/buffer/simple-fx/ao/generateHilbertNoiseTexture.js +78 -0
  17. package/src/engine/physics/PLAN.md +705 -461
  18. package/src/engine/physics/REVIEW_002.md +151 -0
  19. package/src/engine/physics/REVIEW_003.md +166 -0
  20. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  21. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  22. package/src/engine/physics/constraint/DofMode.js +35 -0
  23. package/src/engine/physics/constraint/solve_constraints.d.ts +38 -0
  24. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  25. package/src/engine/physics/constraint/solve_constraints.js +673 -0
  26. package/src/engine/physics/ecs/Joint.d.ts +294 -0
  27. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  28. package/src/engine/physics/ecs/Joint.js +402 -0
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
  30. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
  32. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  33. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  34. package/src/engine/physics/fluid/FluidField.js +14 -10
  35. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  36. package/src/engine/physics/fluid/FluidSimulator.js +0 -1
  37. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  38. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  39. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  40. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  41. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  42. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  43. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  44. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  45. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  46. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
  47. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  48. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
  49. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  50. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  51. package/src/engine/physics/island/IslandBuilder.js +33 -16
  52. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
  54. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
  55. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  56. package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
  57. package/src/engine/physics/narrowphase/ray_shapes.d.ts +66 -0
  58. package/src/engine/physics/narrowphase/ray_shapes.d.ts.map +1 -0
  59. package/src/engine/physics/narrowphase/ray_shapes.js +187 -0
  60. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +16 -0
  61. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -0
  62. package/src/engine/physics/narrowphase/refine_ray_concave.js +145 -0
  63. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +39 -0
  64. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -0
  65. package/src/engine/physics/narrowphase/refine_ray_hit.js +78 -0
  66. package/src/engine/physics/queries/raycast.d.ts +11 -9
  67. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  68. package/src/engine/physics/queries/raycast.js +108 -159
  69. package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
  70. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  71. package/src/engine/physics/solver/solve_contacts.js +169 -1
  72. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +114 -0
  73. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -0
  74. package/src/engine/physics/vehicle/RaycastVehicle.js +333 -0
@@ -0,0 +1,151 @@
1
+ # REVIEW_002 — TGS solver, contact robustness, and constraints
2
+
3
+ Retrospective on the "competent → great" push: promoting the solver from
4
+ single-step PGS to substepped TGS, fixing the contact-robustness issues that
5
+ surfaced, and standing up the 6-DOF constraint subsystem. Companion to the
6
+ engine-comparison docs (`CANNON_REVIEW`, `RAPIER_REVIEW`, `JOLT_REVIEW`,
7
+ `BULLET_REVIEW`); this one is a build retrospective, not a comparison.
8
+
9
+ Scope: the work tracked in `PLAN.md` under "Solver quality" + "Constraints /
10
+ joints". What landed, the hard-won lessons, where the implementation
11
+ deviated from the original plan and why, and what's deferred.
12
+
13
+ ---
14
+
15
+ ## What landed
16
+
17
+ **TGS (Temporal Gauss-Seidel), Phases 1–3.** The solver is now a staged
18
+ pipeline driven by a substep loop:
19
+ `prepare → per substep [redetect-concave / refresh-convex → warm-start →
20
+ solve-velocity → solve-position] → restitution`. Defaults: 4 substeps, 4
21
+ velocity + 1 position iteration per substep. Split-impulse position
22
+ correction (pseudo-velocity folded into the pose, never into real velocity);
23
+ one-shot restitution; SPOOK compliance as the soft-constraint dial.
24
+
25
+ **Contact robustness.**
26
+ - Box-box SAT reference tie-break deadband → aligned cube stacks 4–10 high
27
+ settle to zero velocity and sleep.
28
+ - Per-substep concave re-detection → a dynamic concave mesh body (torus knot)
29
+ settles instead of rocking.
30
+
31
+ **Constraints — 6-DOF configurable joint** (PhysX D6 / Jolt SixDOF model):
32
+ one constraint type, each of 3 linear + 3 angular DOFs independently
33
+ locked/free/limited/spring/motor. Landed modes: LOCKED linear (ball-socket)
34
+ and LOCKED angular (hinge, weld). Solved as a parallel row set inside the
35
+ same substep loop; island-integrated; generation-checked against stale body
36
+ references.
37
+
38
+ Coverage went ~698 → 720+ physics tests, all green. Falling-tower bench
39
+ unchanged (~48 ms / 1000 active bodies).
40
+
41
+ ---
42
+
43
+ ## Hard-won lessons
44
+
45
+ These cost real debugging and are the reason the code looks the way it does.
46
+
47
+ 1. **Per-substep warm-start is mandatory under substepping.** The first
48
+ substep attempt warm-started once per outer step and applied gravity per
49
+ substep. That mismatch — replaying a *full-frame* impulse against a
50
+ *single substep* of gravity — over-pushes resting contacts and **explodes**
51
+ deep stacks (a 10-cube stack hit 9 m of drift / 13 m/s). The fix: gravity
52
+ per substep **and** warm-start per substep, so each substep's gravity and
53
+ each substep's replayed impulse cancel exactly at rest. A resting body
54
+ holds at `v = 0`, `j_n ≈ m·g·h`.
55
+
56
+ 2. **Restitution must gate on the running-max normal impulse, not the
57
+ end-of-loop value.** With per-substep warm-start, a transient collision's
58
+ accumulated `j_n` relaxes back to ~0 by the end of the loop (no sustained
59
+ load to hold it), so gating restitution on the final `j_n` silently killed
60
+ every bounce. Tracking `maxNormalImpulse` over the whole step (Box2D-v3's
61
+ trick) is what makes restitution fire.
62
+
63
+ 3. **Analytic separation re-derivation is great for convex, wrong for
64
+ dynamic concave.** Re-deriving each substep's penetration from frozen
65
+ witness anchors + current pose (no narrowphase re-run) is cheap and exact
66
+ *while the contact feature is stable* — true for a convex primitive under
67
+ the small per-step motion. For a concave body rocking on a mesh, the
68
+ supporting triangle (and its normal) genuinely changes mid-step, so
69
+ freezing it pumps energy in. Resolution: convex pairs use the cheap
70
+ analytic refresh; concave-involved pairs re-run narrowphase geometry each
71
+ substep (`redetect_concave_contacts`). Hybrid, gated by collider convexity.
72
+
73
+ 4. **The aligned-stack instability was not the solver.** Cube stacks crept
74
+ and toppled; the instinct was "solver convergence". Dumping
75
+ `box_box_manifold` across a settling pair showed the normal and contact
76
+ *count* were stable, but the *reference face* flip-flopped between A and B
77
+ each frame (their SAT overlaps are exactly equal for aligned boxes, so
78
+ float noise from a sub-degree wobble decided the winner). That reordered
79
+ the contacts → flipped the Gauss-Seidel sweep order → alternating bias →
80
+ creep. A 6-line relative+absolute tie-break deadband (bias ties toward the
81
+ earlier-tested axis) fixed stacks 4–10. **Lesson: diagnose the manifold
82
+ before blaming the solver.**
83
+
84
+ 5. **Granularity is everything for dynamic concave.** The first instinct
85
+ ("decompose the mesh into the exact Delaunay tets we already have") is
86
+ wrong: a Suzanne is ~8000 tets → ~8000 colliders → ~8000 dynamic-BVH
87
+ leaves refit every frame. Tet meshing is a *volumetric* (FEM/soft-body)
88
+ tool. Rigid collision wants the *fewest* convex pieces — a single convex
89
+ hull for most dynamic objects, or a few-hull (V-HACD-style) decomposition.
90
+ Recorded as the long-term path; the per-substep re-detection is the
91
+ interim.
92
+
93
+ 6. **One configurable 6-DOF constraint beats N joint types.** The row math is
94
+ contact-shaped (Jacobian + effective mass + bias + bounds), so it slots
95
+ into the existing solver. Concrete joints become *config*: ball-socket =
96
+ lock 3 linear; hinge = lock 3 linear + 2 angular; weld = lock 6. This
97
+ minimises distinct code paths — the whole point.
98
+
99
+ 7. **Sign conventions: linear A−B, angular B−A.** The linear rows use
100
+ "A minus B" (impulse +to A); the angular rows use "B minus A" (impulse
101
+ +to B). Each is internally consistent — the body-order difference just
102
+ folds the sign in. Derived carefully against the SPOOK Baumgarte target
103
+ (`dC/dt = vrel`); both worked first try once written down.
104
+
105
+ ---
106
+
107
+ ## Deviations from the original plan (and why)
108
+
109
+ - **Joints run as a parallel row set, not by porting contacts onto a shared
110
+ constraint base.** The plan floated either. Porting the working,
111
+ well-tuned contact path carried regression risk for no immediate benefit;
112
+ running joints as a second row family in the same substep loop gets the
113
+ coupling (shared warm-start / islands / substep cadence) at far lower risk.
114
+ Unifying remains optional.
115
+
116
+ - **Convex contacts use analytic refresh, not per-substep match-and-merge.**
117
+ The plan said "substeps refresh contacts via match-and-merge". Analytic
118
+ re-derivation is cheaper and avoids manifold-lifecycle churn for the convex
119
+ majority; match-and-merge-style re-detection is reserved for the concave
120
+ minority (lesson 3).
121
+
122
+ - **Dynamic concave: per-substep re-detection now, convex proxies later** —
123
+ not the tet-compound idea first sketched (lesson 5).
124
+
125
+ ---
126
+
127
+ ## Deferred (with rationale)
128
+
129
+ - **Constraints**: prismatic (needs frame-basis linear rows — today's linear
130
+ locks use world axes, exact only when all 3 are locked), limits, motors,
131
+ springs (SPOOK soft), swing-twist (ragdoll cone), the raycast-vehicle
132
+ layer, and extras (pulley/gear/conveyor/breakable). Sequenced in `PLAN.md`.
133
+ - **Trajectory accuracy**: gravity is substepped, so ballistic integration is
134
+ at the substep rate — good. No higher-order integrator pursued.
135
+ - **Per-island parallel solve / CCD shape-cast / closed-form
136
+ triangle-vs-primitive remainder**: unchanged backlog items.
137
+ - **Convex hull builder + few-hull decomposition**: the production answer for
138
+ dynamic concave (lesson 5), not yet built.
139
+
140
+ ---
141
+
142
+ ## Bottom line
143
+
144
+ The engine moved from "competent PGS" to a substepped TGS solver with stable
145
+ stacks, accurate restitution, robust mass ratios, settling dynamic concave
146
+ bodies, and a working 6-DOF constraint (ball-socket / hinge / weld) — i.e.
147
+ chains, ropes, and the structural joints for ragdolls and mechanisms. The
148
+ costliest mistakes were all about *substepping invariants* (warm-start and
149
+ restitution-gating cadence) and *not trusting assumptions* (the stack bug was
150
+ narrowphase, not the solver). The remaining constraint work is incremental
151
+ on a validated framework.
@@ -0,0 +1,166 @@
1
+ # REVIEW_003 — 6-DOF joints, full DOF-mode set, and the raycast vehicle
2
+
3
+ Retrospective on the constraints push: completing the 6-DOF joint across every
4
+ per-DOF mode (lock / free / limit / motor / spring), adding accurate cone-twist
5
+ range-of-motion, and building the raycast-vehicle layer on top. Companion to
6
+ `REVIEW_002` (TGS solver + contact robustness + the first joint modes); this one
7
+ covers `PLAN.md` → "Constraints / joints" phases 3–7.
8
+
9
+ Scope: what landed, the hard-won lessons, where the build deviated from the plan
10
+ and why, and what's deferred.
11
+
12
+ ---
13
+
14
+ ## What landed
15
+
16
+ **One mode-agnostic constraint row.** The joint solver resolves every DOF —
17
+ linear or angular — through a single scalar velocity row parameterised by
18
+ `(effective mass, bias, impulse clamp, regularisation γ)`. The `DofMode` only
19
+ chooses those parameters:
20
+
21
+ - **LOCKED** — bilateral, `bias = β/h · C`, clamp `(−∞, +∞)`. Ball-socket /
22
+ hinge / weld / prismatic are configurations of locked vs free DOFs.
23
+ - **LIMITED** — a *speculative (β=1) one-sided* velocity constraint: `bias =
24
+ (pos − bound)/h`, clamp one-sided. Slider end-stops and joint ROM.
25
+ - **MOTOR** — `bias = −targetVelocity`, clamp `±maxForce·h`. Powered doors,
26
+ wheel drive, pistons.
27
+ - **SPRING** — a regularised (compliant) row: `γ = 1/(h·(c+h·k))`, `bias =
28
+ (k/denom)·C`, softened mass `1/(K+γ)`, plus the `+γ·λ_accum` term. Suspension,
29
+ bungees, soft-return.
30
+
31
+ Adding each mode was a localised change to the row-parameter setup; the
32
+ warm-start and the iteration loop never changed shape. SPRING added exactly one
33
+ term (`+γ·λ_accum`, γ defaulting to 0), so LOCKED/LIMITED/MOTOR stayed
34
+ bit-for-bit identical.
35
+
36
+ **Frame-basis everything.** Both linear and angular rows resolve in frame A's
37
+ basis axes, clearing the early world-axis linear debt — the solver is fully
38
+ frame-relative.
39
+
40
+ **Cone-twist (swing-twist).** Opt-in `Joint.swingTwist` switches the angular
41
+ position measure from the per-axis small-angle vector to a swing-twist
42
+ decomposition (X = twist, Y/Z = swing) with *exact* angles, reusing the LIMITED
43
+ / SPRING rows. Wide ragdoll ROM now holds at the true angle.
44
+
45
+ **Raycast vehicle.** `vehicle/RaycastVehicle.js` — a chassis body + raycast
46
+ wheels, a controller on the public `raycast` + `applyForceAt`/`applyImpulseAt`
47
+ API. Suspension (spring+damper along the contact normal) + tyre friction
48
+ (lateral grip + drive/brake clamped to a friction circle μ·N) + steering. The
49
+ 6-DOF spring+motor is the alternative "simulated wheel" path.
50
+
51
+ Coverage ~720 → **732 physics tests**, all green. Joint tests 9 → 19; vehicle
52
+ tests +6; one swing-twist micro-bench (`test.skip`).
53
+
54
+ ---
55
+
56
+ ## Hard-won lessons
57
+
58
+ 1. **A limit must not bounce — velocity-Baumgarte injects energy.** The first
59
+ LIMITED cut waited for the DOF to cross a stop, then pushed back with a
60
+ Baumgarte bias. On a spin-into-stop with no opposing load the restorative
61
+ *velocity* it added didn't dissipate — the DOF coasted back through the whole
62
+ range. This is the same energy injection the contact solver avoids by using
63
+ split-impulse instead of velocity-Baumgarte. The fix: a **speculative (β=1)
64
+ one-sided velocity constraint** that removes exactly the approach velocity so
65
+ the DOF *lands on* the stop (zero penetration → zero rebound), self-gates to
66
+ a no-op away from the bound, and — because only the push-out side of the bias
67
+ is clamped — eases a teleport out instead of yanking. A loaded stop (gravity
68
+ on a slider) rests cleanly: per-substep load and per-substep warm-start
69
+ cancel, the resting-contact invariant again.
70
+
71
+ 2. **One row, many modes — but only because the *fixed point* is shared.**
72
+ Folding LOCKED and LIMITED (and later MOTOR/SPRING) into one solve worked
73
+ because they differ only in `(bias, clamp, γ)`. The proof it was safe was
74
+ mechanical: LOCKED's prior tests stayed green untouched after each
75
+ generalisation. Resisting the urge to special-case kept the loop tiny and
76
+ made every later mode a few lines.
77
+
78
+ 3. **Spring = regularisation, and it reproduces the analytic equilibrium.** The
79
+ SPOOK soft constraint (`γ` compliance + `(k/denom)·C` bias + softened mass)
80
+ isn't an approximation to dial in — a vertical strut settles at *exactly*
81
+ `m·g/k`, an undamped one oscillates and conserves energy, a stiff one stays
82
+ stable at 4 substeps. Getting the discrete coefficients right (Catto's
83
+ implicit-Euler form) made it behave like a real spring on the first run.
84
+
85
+ 4. **Swing-twist: exact position, first-order Jacobian.** The win is measuring
86
+ the limit *position* as a true angle (a 1.2 rad swing stops at 1.2, not the
87
+ small-angle proxy's ~1.287). The velocity Jacobian stays the cheap `ω·axis`
88
+ first-order one — PhysX/Jolt do the same. Keeping it opt-in means welds and
89
+ tight hinges don't pay the `atan2`/`sqrt` cost.
90
+
91
+ 5. **Benchmark before inlining — but inline when it's 5×.** Rather than assume,
92
+ the swing-twist decomposition was measured inlined (allocation-free scalar
93
+ math) vs the object-based `Quaternion.computeSwingAndTwist`: ~5× faster than
94
+ the method with reused out-params, ~10× vs naive fresh allocation (property
95
+ getters + `normalize` + a quaternion multiply + GC). In a per-substep
96
+ per-joint hot loop that margin justifies the duplicated math; the Quaternion
97
+ method stays for general callers. Making all three variants produce identical
98
+ results (matching sinks) was what made the comparison trustworthy.
99
+
100
+ 6. **The raycast vehicle is a force controller, and its failure modes are
101
+ physical.** Too much drive on a light, high-CG car wheelies and flips; too
102
+ much speed into a hard steer rolls it. These aren't solver bugs — they're the
103
+ model being honest. The tests use a low, heavy car and moderate inputs so the
104
+ *controller* (suspension equilibrium, grip, drive, steer) is what's under
105
+ test, not the operator. Spotting "the car launched, went airborne, then slept
106
+ on its roof" from `contacts=0, y=0.29, sleep=1` saved chasing a non-bug.
107
+
108
+ 7. **An external per-frame controller can't fully honour the substep
109
+ invariant.** Suspension is applied once per frame as a `dt`-force, but gravity
110
+ is per-substep — so a resting chassis carries a ~`g·h` end-of-step
111
+ velocity-sample artifact (it hovers position-stable to sub-cm, but the
112
+ sampled `v_y` isn't zero the way a resting *contact* body's is). Contacts
113
+ avoid this because their warm-start cancels each substep's gravity inside the
114
+ loop; a frame-level controller has no such hook. Documented, not hidden.
115
+
116
+ ---
117
+
118
+ ## Deviations from the plan (and why)
119
+
120
+ - **LIMITED is speculative-velocity, not split-impulse.** The plan sketched
121
+ limits as bounded rows; the bounce lesson pushed it to a β=1 speculative
122
+ velocity constraint, which needs no separate position pass and rests cleanly.
123
+ - **Swing-twist is inlined, not `Quaternion.computeSwingAndTwist`.** The note
124
+ said "reuse the Quaternion method"; the benchmark said inline (lesson 5). The
125
+ method stays for non-hot callers.
126
+ - **Swing-twist is opt-in (`swingTwist` flag), not the default measure.** Keeps
127
+ the cheaper small-angle path for welds/hinges and zero regression risk; the
128
+ flag is a genuine angular-mode choice (PhysX D6 / Jolt SwingTwist), not an
129
+ auto/override sentinel.
130
+ - **Vehicle is a standalone controller, not an ECS component+system.** It rides
131
+ the public query+force API exactly as the plan framed it ("on top of the
132
+ queries"), which keeps it decoupled and matches Bullet/Godot usage.
133
+
134
+ ---
135
+
136
+ ## Deferred (with rationale)
137
+
138
+ - **Raycast narrowphase refinement** — the suspension ray (and every shape
139
+ query) currently resolves to the leaf's inflated AABB: exact for axis-aligned
140
+ box ground, coarse for spheres/capsules/rotated boxes/meshes. This is the next
141
+ item; it sharpens vehicle ride height and all query accuracy. Tracked
142
+ separately.
143
+ - **Vehicle extras** — anti-roll bars, engine/gearbox curves, configurable
144
+ weight transfer / anti-squat, wheel-collision (vs raycast-only). The core is
145
+ in; these are tuning layers.
146
+ - **Elliptical/conical swing limit** — the cone is currently box-shaped
147
+ (independent Y/Z swing limits). A single combined swing-magnitude row would
148
+ give a circular/elliptical cone.
149
+ - **Per-substep joint warm-start for the vehicle** — would remove the `g·h`
150
+ velocity-sample artifact, at the cost of running the controller inside the
151
+ substep loop.
152
+ - Carried from REVIEW_002: convex-hull / few-hull decomposition for production
153
+ dynamic concave; per-island parallel solve; shape-cast CCD.
154
+
155
+ ---
156
+
157
+ ## Bottom line
158
+
159
+ The 6-DOF joint now spans the whole taxonomy from one row solve — ball-socket,
160
+ hinge, weld, prismatic, slider/ROM end-stops, powered doors/wheels, springs/
161
+ suspension, and accurate ragdoll cone-twist — and the raycast vehicle sits on
162
+ top of it and the query API. The costliest lessons were again about *not
163
+ injecting energy* (the limit bounce, mirroring the contact split-impulse
164
+ rationale) and *measuring before optimising* (the swing-twist inline bench). The
165
+ constraints plan (phases 1–7) is complete; the highest-leverage next step is
166
+ raycast narrowphase, which every query — and the vehicle — would sharpen.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Per-degree-of-freedom mode for a 6-DOF constraint ({@link Joint }).
3
+ *
4
+ * A 6-DOF constraint has 3 linear DOFs (translation of body B's anchor frame
5
+ * relative to body A's, along A's frame axes) and 3 angular DOFs (relative
6
+ * rotation, swing-twist decomposed). Each DOF independently takes one of these
7
+ * modes — which is how a single configurable constraint expresses the whole
8
+ * joint taxonomy (PhysX D6 / Jolt SixDOF / Bullet Generic6DOF):
9
+ *
10
+ * - ball-socket = lock 3 linear
11
+ * - hinge = lock 3 linear + 2 angular
12
+ * - prismatic = lock 2 linear + 3 angular
13
+ * - weld = lock all 6
14
+ * - cone-twist = lock 3 linear + limit 3 angular
15
+ * - suspension = spring 1 linear + lock the rest
16
+ *
17
+ * Implementation lands mode-by-mode: LOCKED first (covers ball-socket / hinge
18
+ * / prismatic / weld), then LIMITED, SPRING, MOTOR.
19
+ */
20
+ export type DofMode = number;
21
+ export namespace DofMode {
22
+ let LOCKED: number;
23
+ let FREE: number;
24
+ let LIMITED: number;
25
+ let SPRING: number;
26
+ let MOTOR: number;
27
+ }
28
+ //# sourceMappingURL=DofMode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DofMode.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/constraint/DofMode.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;sBAqBU,MAAM"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Per-degree-of-freedom mode for a 6-DOF constraint ({@link Joint}).
3
+ *
4
+ * A 6-DOF constraint has 3 linear DOFs (translation of body B's anchor frame
5
+ * relative to body A's, along A's frame axes) and 3 angular DOFs (relative
6
+ * rotation, swing-twist decomposed). Each DOF independently takes one of these
7
+ * modes — which is how a single configurable constraint expresses the whole
8
+ * joint taxonomy (PhysX D6 / Jolt SixDOF / Bullet Generic6DOF):
9
+ *
10
+ * - ball-socket = lock 3 linear
11
+ * - hinge = lock 3 linear + 2 angular
12
+ * - prismatic = lock 2 linear + 3 angular
13
+ * - weld = lock all 6
14
+ * - cone-twist = lock 3 linear + limit 3 angular
15
+ * - suspension = spring 1 linear + lock the rest
16
+ *
17
+ * Implementation lands mode-by-mode: LOCKED first (covers ball-socket / hinge
18
+ * / prismatic / weld), then LIMITED, SPRING, MOTOR.
19
+ *
20
+ * @author Alex Goldring
21
+ * @copyright Company Named Limited (c) 2026
22
+ * @enum {number}
23
+ */
24
+ export const DofMode = {
25
+ /** Constrained to zero (relative position/angle held at the rest value). */
26
+ LOCKED: 0,
27
+ /** Unconstrained — the DOF moves freely. */
28
+ FREE: 1,
29
+ /** Free within `[lower, upper]`, constrained (one-sided) at the limits. */
30
+ LIMITED: 2,
31
+ /** A soft spring (stiffness + damping) pulling toward a rest target. */
32
+ SPRING: 3,
33
+ /** Driven toward a target velocity, bounded by a maximum force. */
34
+ MOTOR: 4,
35
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Swing-twist decomposition of the relative rotation `qD = conj(qA)⊗qB`, giving
3
+ * the per-frame-axis angular positions used by wide-cone angular DOFs.
4
+ *
5
+ * Decomposes `qD = swing ⊗ twist`, with **twist** the rotation about frame
6
+ * axis X (the bone / twist axis) and **swing** the residual tilt (a rotation
7
+ * about an axis in the YZ plane that carries X to its new direction). Writes,
8
+ * into `out`:
9
+ * - `out[0]` = signed twist angle about X, in `(−π, π]` — *exact*, so a twist
10
+ * limit engages at the true angle, not the `2·sin(θ/2)` small-angle proxy;
11
+ * - `out[1]`, `out[2]` = the swing angle distributed over Y and Z
12
+ * (`φ·axis`, φ the true swing angle), so independent Y/Z swing limits form
13
+ * an accurate (box-shaped) cone that holds at large tilt.
14
+ * Reduces continuously to the small-angle vector near identity. Pure scratch /
15
+ * locals — no allocation, suitable for the per-substep hot loop. (The Quaternion
16
+ * class has an object-based `computeSwingAndTwist`; this is the inlined,
17
+ * allocation-free form — see the bench that justifies it.)
18
+ *
19
+ * @param {number} dx @param {number} dy @param {number} dz @param {number} dw qD
20
+ * @param {Float64Array} out length-3 destination (frame-axis positions)
21
+ */
22
+ export function swing_twist_error(dx: number, dy: number, dz: number, dw: number, out: Float64Array): void;
23
+ /**
24
+ * Solve every joint for one substep: recompute geometry at the current poses,
25
+ * derive each DOF's row parameters from its mode, replay the per-substep
26
+ * warm-start, and run `iters` velocity iterations.
27
+ *
28
+ * Called once per substep from `PhysicsSystem.fixedUpdate`, after the contact
29
+ * solve so the two share the substep / warm-start cadence.
30
+ *
31
+ * @param {Joint[]} joints live joints (sparse array; holes skipped)
32
+ * @param {PhysicsSystem} system reads `__bodies` / `__transforms` / index map
33
+ * @param {number} dt_sub substep size in seconds (the SPOOK gain is derived
34
+ * from it, matching the contact solver's per-substep position stiffness)
35
+ * @param {number} iters velocity iterations
36
+ */
37
+ export function solve_joints(joints: Joint[], system: PhysicsSystem, dt_sub: number, iters: number): void;
38
+ //# sourceMappingURL=solve_constraints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solve_constraints.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/constraint/solve_constraints.js"],"names":[],"mappings":"AA+MA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,sCAHW,MAAM,MAAa,MAAM,MAAa,MAAM,MAAa,MAAM,OAC/D,YAAY,QAkCtB;AA0JD;;;;;;;;;;;;;GAaG;AACH,qCANW,OAAO,iCAEP,MAAM,SAEN,MAAM,QAsPhB"}