# REVIEW_003 — 6-DOF joints, full DOF-mode set, and the raycast vehicle

Retrospective on the constraints push: completing the 6-DOF joint across every
per-DOF mode (lock / free / limit / motor / spring), adding accurate cone-twist
range-of-motion, and building the raycast-vehicle layer on top. Companion to
`REVIEW_002` (TGS solver + contact robustness + the first joint modes); this one
covers `PLAN.md` → "Constraints / joints" phases 3–7.

Scope: what landed, the hard-won lessons, where the build deviated from the plan
and why, and what's deferred.

---

## What landed

**One mode-agnostic constraint row.** The joint solver resolves every DOF —
linear or angular — through a single scalar velocity row parameterised by
`(effective mass, bias, impulse clamp, regularisation γ)`. The `DofMode` only
chooses those parameters:

- **LOCKED** — bilateral, `bias = β/h · C`, clamp `(−∞, +∞)`. Ball-socket /
  hinge / weld / prismatic are configurations of locked vs free DOFs.
- **LIMITED** — a *speculative (β=1) one-sided* velocity constraint: `bias =
  (pos − bound)/h`, clamp one-sided. Slider end-stops and joint ROM.
- **MOTOR** — `bias = −targetVelocity`, clamp `±maxForce·h`. Powered doors,
  wheel drive, pistons.
- **SPRING** — a regularised (compliant) row: `γ = 1/(h·(c+h·k))`, `bias =
  (k/denom)·C`, softened mass `1/(K+γ)`, plus the `+γ·λ_accum` term. Suspension,
  bungees, soft-return.

Adding each mode was a localised change to the row-parameter setup; the
warm-start and the iteration loop never changed shape. SPRING added exactly one
term (`+γ·λ_accum`, γ defaulting to 0), so LOCKED/LIMITED/MOTOR stayed
bit-for-bit identical.

**Frame-basis everything.** Both linear and angular rows resolve in frame A's
basis axes, clearing the early world-axis linear debt — the solver is fully
frame-relative.

**Cone-twist (swing-twist).** Opt-in `Joint.swingTwist` switches the angular
position measure from the per-axis small-angle vector to a swing-twist
decomposition (X = twist, Y/Z = swing) with *exact* angles, reusing the LIMITED
/ SPRING rows. Wide ragdoll ROM now holds at the true angle.

**Raycast vehicle.** `vehicle/RaycastVehicle.js` — a chassis body + raycast
wheels, a controller on the public `raycast` + `applyForceAt`/`applyImpulseAt`
API. Suspension (spring+damper along the contact normal) + tyre friction
(lateral grip + drive/brake clamped to a friction circle μ·N) + steering. The
6-DOF spring+motor is the alternative "simulated wheel" path.

Coverage ~720 → **732 physics tests**, all green. Joint tests 9 → 19; vehicle
tests +6; one swing-twist micro-bench (`test.skip`).

---

## Hard-won lessons

1. **A limit must not bounce — velocity-Baumgarte injects energy.** The first
   LIMITED cut waited for the DOF to cross a stop, then pushed back with a
   Baumgarte bias. On a spin-into-stop with no opposing load the restorative
   *velocity* it added didn't dissipate — the DOF coasted back through the whole
   range. This is the same energy injection the contact solver avoids by using
   split-impulse instead of velocity-Baumgarte. The fix: a **speculative (β=1)
   one-sided velocity constraint** that removes exactly the approach velocity so
   the DOF *lands on* the stop (zero penetration → zero rebound), self-gates to
   a no-op away from the bound, and — because only the push-out side of the bias
   is clamped — eases a teleport out instead of yanking. A loaded stop (gravity
   on a slider) rests cleanly: per-substep load and per-substep warm-start
   cancel, the resting-contact invariant again.

2. **One row, many modes — but only because the *fixed point* is shared.**
   Folding LOCKED and LIMITED (and later MOTOR/SPRING) into one solve worked
   because they differ only in `(bias, clamp, γ)`. The proof it was safe was
   mechanical: LOCKED's prior tests stayed green untouched after each
   generalisation. Resisting the urge to special-case kept the loop tiny and
   made every later mode a few lines.

3. **Spring = regularisation, and it reproduces the analytic equilibrium.** The
   SPOOK soft constraint (`γ` compliance + `(k/denom)·C` bias + softened mass)
   isn't an approximation to dial in — a vertical strut settles at *exactly*
   `m·g/k`, an undamped one oscillates and conserves energy, a stiff one stays
   stable at 4 substeps. Getting the discrete coefficients right (Catto's
   implicit-Euler form) made it behave like a real spring on the first run.

4. **Swing-twist: exact position, first-order Jacobian.** The win is measuring
   the limit *position* as a true angle (a 1.2 rad swing stops at 1.2, not the
   small-angle proxy's ~1.287). The velocity Jacobian stays the cheap `ω·axis`
   first-order one — PhysX/Jolt do the same. Keeping it opt-in means welds and
   tight hinges don't pay the `atan2`/`sqrt` cost.

5. **Benchmark before inlining — but inline when it's 5×.** Rather than assume,
   the swing-twist decomposition was measured inlined (allocation-free scalar
   math) vs the object-based `Quaternion.computeSwingAndTwist`: ~5× faster than
   the method with reused out-params, ~10× vs naive fresh allocation (property
   getters + `normalize` + a quaternion multiply + GC). In a per-substep
   per-joint hot loop that margin justifies the duplicated math; the Quaternion
   method stays for general callers. Making all three variants produce identical
   results (matching sinks) was what made the comparison trustworthy.

6. **The raycast vehicle is a force controller, and its failure modes are
   physical.** Too much drive on a light, high-CG car wheelies and flips; too
   much speed into a hard steer rolls it. These aren't solver bugs — they're the
   model being honest. The tests use a low, heavy car and moderate inputs so the
   *controller* (suspension equilibrium, grip, drive, steer) is what's under
   test, not the operator. Spotting "the car launched, went airborne, then slept
   on its roof" from `contacts=0, y=0.29, sleep=1` saved chasing a non-bug.

7. **An external per-frame controller can't fully honour the substep
   invariant.** Suspension is applied once per frame as a `dt`-force, but gravity
   is per-substep — so a resting chassis carries a ~`g·h` end-of-step
   velocity-sample artifact (it hovers position-stable to sub-cm, but the
   sampled `v_y` isn't zero the way a resting *contact* body's is). Contacts
   avoid this because their warm-start cancels each substep's gravity inside the
   loop; a frame-level controller has no such hook. Documented, not hidden.

---

## Deviations from the plan (and why)

- **LIMITED is speculative-velocity, not split-impulse.** The plan sketched
  limits as bounded rows; the bounce lesson pushed it to a β=1 speculative
  velocity constraint, which needs no separate position pass and rests cleanly.
- **Swing-twist is inlined, not `Quaternion.computeSwingAndTwist`.** The note
  said "reuse the Quaternion method"; the benchmark said inline (lesson 5). The
  method stays for non-hot callers.
- **Swing-twist is opt-in (`swingTwist` flag), not the default measure.** Keeps
  the cheaper small-angle path for welds/hinges and zero regression risk; the
  flag is a genuine angular-mode choice (PhysX D6 / Jolt SwingTwist), not an
  auto/override sentinel.
- **Vehicle is a standalone controller, not an ECS component+system.** It rides
  the public query+force API exactly as the plan framed it ("on top of the
  queries"), which keeps it decoupled and matches Bullet/Godot usage.

---

## Deferred (with rationale)

- **Raycast narrowphase refinement** — the suspension ray (and every shape
  query) currently resolves to the leaf's inflated AABB: exact for axis-aligned
  box ground, coarse for spheres/capsules/rotated boxes/meshes. This is the next
  item; it sharpens vehicle ride height and all query accuracy. Tracked
  separately.
- **Vehicle extras** — anti-roll bars, engine/gearbox curves, configurable
  weight transfer / anti-squat, wheel-collision (vs raycast-only). The core is
  in; these are tuning layers.
- **Elliptical/conical swing limit** — the cone is currently box-shaped
  (independent Y/Z swing limits). A single combined swing-magnitude row would
  give a circular/elliptical cone.
- **Per-substep joint warm-start for the vehicle** — would remove the `g·h`
  velocity-sample artifact, at the cost of running the controller inside the
  substep loop.
- Carried from REVIEW_002: convex-hull / few-hull decomposition for production
  dynamic concave; per-island parallel solve; shape-cast CCD.

---

## Bottom line

The 6-DOF joint now spans the whole taxonomy from one row solve — ball-socket,
hinge, weld, prismatic, slider/ROM end-stops, powered doors/wheels, springs/
suspension, and accurate ragdoll cone-twist — and the raycast vehicle sits on
top of it and the query API. The costliest lessons were again about *not
injecting energy* (the limit bounce, mirroring the contact split-impulse
rationale) and *measuring before optimising* (the swing-twist inline bench). The
constraints plan (phases 1–7) is complete; the highest-leverage next step is
raycast narrowphase, which every query — and the vehicle — would sharpen.
