Skip to main content

proof_engine/physics/
constraints.rs

1//! XPBD + Sequential-Impulse constraint solver for 2D/3D rigid bodies.
2//!
3//! Implements both:
4//! - Sequential Impulse (SI) solver — velocity-level constraint resolution
5//!   following Erin Catto / Box2D style iterative impulses.
6//! - Extended Position-Based Dynamics (XPBD) — position-level constraints
7//!   with compliance, substep integration, and constraint islands.
8//!
9//! ## Constraint types
10//! - `ContactConstraint`      — normal + friction, restitution, Baumgarte, warm-start
11//! - `DistanceConstraint`     — rigid rod (exact) and soft spring (compliance)
12//! - `HingeConstraint`        — single-axis rotation with limits and motor
13//! - `BallSocketConstraint`   — 3-DOF free rotation with cone limit
14//! - `SliderConstraint`       — prismatic joint with translation limits and motor
15//! - `WeldConstraint`         — fully rigid 6-DOF lock
16//! - `PulleyConstraint`       — two bodies through a fixed pulley ratio
17//! - `MotorConstraint`        — angular velocity drive
18//! - `MaxDistanceConstraint`  — soft rope
19//! - `GearConstraint`         — ratio-based angular coupling
20
21use glam::{Vec2, Vec3, Mat2};
22use std::collections::HashMap;
23
24// ── BodyHandle ────────────────────────────────────────────────────────────────
25
26/// Reference to a rigid body in the solver.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct BodyHandle(pub u32);
29
30/// Sentinel for world-space anchors (infinite mass, immovable).
31pub const WORLD: BodyHandle = BodyHandle(u32::MAX);
32
33// ── BodyState ─────────────────────────────────────────────────────────────────
34
35/// Minimal state the solver needs from a rigid body.
36#[derive(Debug, Clone)]
37pub struct BodyState {
38    pub position:         Vec2,
39    pub velocity:         Vec2,
40    pub angle:            f32,
41    pub angular_velocity: f32,
42    /// Inverse mass (0.0 for static bodies).
43    pub inv_mass:         f32,
44    /// Inverse moment of inertia (0.0 for static).
45    pub inv_inertia:      f32,
46    /// Accumulated corrective impulse from constraints.
47    pub impulse_accum:    Vec2,
48    pub torque_accum:     f32,
49}
50
51impl BodyState {
52    pub fn new(position: Vec2, mass: f32, inertia: f32) -> Self {
53        Self {
54            position,
55            velocity: Vec2::ZERO,
56            angle: 0.0,
57            angular_velocity: 0.0,
58            inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
59            inv_inertia: if inertia > 0.0 { 1.0 / inertia } else { 0.0 },
60            impulse_accum: Vec2::ZERO,
61            torque_accum: 0.0,
62        }
63    }
64
65    pub fn static_body(position: Vec2) -> Self {
66        Self::new(position, 0.0, 0.0)
67    }
68
69    pub fn apply_impulse(&mut self, impulse: Vec2, contact_arm: Vec2) {
70        self.velocity         += impulse * self.inv_mass;
71        self.angular_velocity += cross2(contact_arm, impulse) * self.inv_inertia;
72    }
73
74    pub fn apply_angular_impulse(&mut self, ang_impulse: f32) {
75        self.angular_velocity += ang_impulse * self.inv_inertia;
76    }
77
78    pub fn is_static(&self) -> bool { self.inv_mass < 1e-12 }
79
80    pub fn kinetic_energy(&self) -> f32 {
81        let m = if self.inv_mass > 1e-12 { 1.0 / self.inv_mass } else { 0.0 };
82        let i = if self.inv_inertia > 1e-12 { 1.0 / self.inv_inertia } else { 0.0 };
83        0.5 * m * self.velocity.length_squared() + 0.5 * i * self.angular_velocity * self.angular_velocity
84    }
85}
86
87// ── Math helpers ──────────────────────────────────────────────────────────────
88
89/// 2D cross product (scalar result): a.x*b.y - a.y*b.x
90#[inline]
91pub fn cross2(a: Vec2, b: Vec2) -> f32 { a.x * b.y - a.y * b.x }
92
93/// Perpendicular of a 2D vector: (-y, x)
94#[inline]
95pub fn perp(v: Vec2) -> Vec2 { Vec2::new(-v.y, v.x) }
96
97/// Rotate a Vec2 by angle theta.
98#[inline]
99pub fn rotate2(v: Vec2, theta: f32) -> Vec2 {
100    let (s, c) = theta.sin_cos();
101    Vec2::new(c * v.x - s * v.y, s * v.x + c * v.y)
102}
103
104/// Clamp an angle to [-PI, PI].
105#[inline]
106fn wrap_angle(a: f32) -> f32 {
107    use std::f32::consts::PI;
108    let mut r = a % (2.0 * PI);
109    if r > PI  { r -= 2.0 * PI; }
110    if r < -PI { r += 2.0 * PI; }
111    r
112}
113
114// ── Constraint ID ─────────────────────────────────────────────────────────────
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub struct ConstraintId(pub u32);
118
119// ── Constraint trait ──────────────────────────────────────────────────────────
120
121/// A constraint imposes a restriction on body state.
122pub trait Constraint: std::fmt::Debug {
123    /// Compute the velocity constraint violation (Cdot = J * v).
124    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32;
125    /// Compute the position constraint violation (C).
126    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32;
127    /// Apply the corrective impulse to body states.
128    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32);
129    /// Compute effective constraint mass (1 / (J M^-1 J^T)).
130    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32;
131    /// XPBD compliance (inverse stiffness). 0 = rigid.
132    fn get_compliance(&self) -> f32 { 0.0 }
133    /// Baumgarte position bias.
134    fn bias(&self, bodies: &HashMap<BodyHandle, BodyState>, dt: f32) -> f32 {
135        let beta = 0.1;
136        beta / dt * self.compute_c(bodies)
137    }
138    /// Prepare the constraint for the current step (pre-compute cached values).
139    fn prepare(&mut self, _bodies: &HashMap<BodyHandle, BodyState>, _dt: f32) {}
140    /// Solve velocity constraint (one iteration).
141    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
142        let em = self.effective_mass(bodies);
143        if em < 1e-10 { return; }
144        let cdot = self.compute_cdot(bodies);
145        let bias = self.bias(bodies, dt);
146        let delta = -(cdot + bias) * em;
147        let (lo, hi) = self.impulse_bounds();
148        let prev = self.accumulated_impulse();
149        let new_acc = (prev + delta).clamp(lo, hi);
150        let actual = new_acc - prev;
151        self.add_accumulated(actual);
152        if actual.abs() > 1e-14 {
153            self.apply_impulse(bodies, actual);
154        }
155    }
156    /// Solve position constraint using XPBD (one sub-step).
157    fn solve_position(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
158        let compliance = self.get_compliance();
159        let em = self.effective_mass(bodies);
160        let c = self.compute_c(bodies);
161        let denom = em + compliance / (dt * dt);
162        if denom < 1e-10 { return; }
163        let lambda = -c / denom;
164        self.apply_impulse(bodies, lambda);
165    }
166    /// Accumulated impulse for warm starting.
167    fn accumulated_impulse(&self) -> f32;
168    fn reset_accumulated(&mut self);
169    fn add_accumulated(&mut self, delta: f32);
170    /// Whether this constraint has an upper/lower impulse clamp.
171    fn impulse_bounds(&self) -> (f32, f32) { (f32::NEG_INFINITY, f32::INFINITY) }
172    /// Bodies involved in this constraint.
173    fn body_handles(&self) -> Vec<BodyHandle> { Vec::new() }
174}
175
176// ── ContactConstraint ─────────────────────────────────────────────────────────
177
178/// Contact constraint: normal impulse (non-penetration) + friction impulse.
179///
180/// Uses Baumgarte stabilization and warm-starting of cached impulses.
181#[derive(Debug, Clone)]
182pub struct ContactConstraint {
183    pub body_a:         BodyHandle,
184    pub body_b:         BodyHandle,
185    /// Contact point in world space.
186    pub contact_point:  Vec2,
187    /// Contact normal (from A to B).
188    pub normal:         Vec2,
189    /// Penetration depth (positive = overlap).
190    pub penetration:    f32,
191    /// Restitution (bounciness).
192    pub restitution:    f32,
193    /// Friction coefficient.
194    pub friction:       f32,
195    /// Baumgarte bias factor.
196    pub baumgarte:      f32,
197    /// Penetration slop — small overlaps are allowed.
198    pub slop:           f32,
199    /// Warm-started normal impulse.
200    pub cached_normal:  f32,
201    /// Warm-started friction impulse.
202    pub cached_tangent: f32,
203    /// Effective mass along normal (cached in prepare).
204    eff_mass_n:         f32,
205    /// Effective mass along tangent (cached in prepare).
206    eff_mass_t:         f32,
207    /// Velocity bias (restitution + Baumgarte).
208    bias:               f32,
209    /// Contact arm A (world anchor - body A center).
210    ra:                 Vec2,
211    /// Contact arm B (world anchor - body B center).
212    rb:                 Vec2,
213}
214
215impl ContactConstraint {
216    pub fn new(
217        body_a: BodyHandle, body_b: BodyHandle,
218        contact_point: Vec2, normal: Vec2, penetration: f32,
219        restitution: f32, friction: f32,
220    ) -> Self {
221        Self {
222            body_a, body_b, contact_point, normal, penetration,
223            restitution, friction,
224            baumgarte: 0.2, slop: 0.005,
225            cached_normal: 0.0, cached_tangent: 0.0,
226            eff_mass_n: 0.0, eff_mass_t: 0.0,
227            bias: 0.0, ra: Vec2::ZERO, rb: Vec2::ZERO,
228        }
229    }
230
231    fn tangent(&self) -> Vec2 {
232        Vec2::new(-self.normal.y, self.normal.x)
233    }
234
235    fn compute_effective_mass_along(
236        &self, dir: Vec2, bodies: &HashMap<BodyHandle, BodyState>,
237    ) -> f32 {
238        let ba = bodies.get(&self.body_a);
239        let bb = bodies.get(&self.body_b);
240        let im_a = ba.map(|b| b.inv_mass).unwrap_or(0.0);
241        let im_b = bb.map(|b| b.inv_mass).unwrap_or(0.0);
242        let ii_a = ba.map(|b| b.inv_inertia).unwrap_or(0.0);
243        let ii_b = bb.map(|b| b.inv_inertia).unwrap_or(0.0);
244        let rda = cross2(self.ra, dir);
245        let rdb = cross2(self.rb, dir);
246        let d = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb;
247        if d < 1e-10 { 0.0 } else { 1.0 / d }
248    }
249
250    fn relative_velocity_at_contact(&self, bodies: &HashMap<BodyHandle, BodyState>) -> Vec2 {
251        let va = bodies.get(&self.body_a)
252            .map(|b| b.velocity + perp(self.ra) * b.angular_velocity)
253            .unwrap_or(Vec2::ZERO);
254        let vb = bodies.get(&self.body_b)
255            .map(|b| b.velocity + perp(self.rb) * b.angular_velocity)
256            .unwrap_or(Vec2::ZERO);
257        vb - va
258    }
259
260    /// Apply normal impulse only.
261    fn apply_normal_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
262        let impulse = self.normal * lambda;
263        if let Some(b) = bodies.get_mut(&self.body_a) {
264            b.apply_impulse(-impulse, self.ra);
265        }
266        if let Some(b) = bodies.get_mut(&self.body_b) {
267            b.apply_impulse(impulse, self.rb);
268        }
269    }
270
271    /// Apply tangent (friction) impulse.
272    fn apply_tangent_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
273        let tangent = self.tangent();
274        let impulse = tangent * lambda;
275        if let Some(b) = bodies.get_mut(&self.body_a) {
276            b.apply_impulse(-impulse, self.ra);
277        }
278        if let Some(b) = bodies.get_mut(&self.body_b) {
279            b.apply_impulse(impulse, self.rb);
280        }
281    }
282
283    /// Solve both normal and friction in one call, handling warm-starting.
284    pub fn solve_contact(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
285        // Normal
286        let rel_vel = self.relative_velocity_at_contact(bodies);
287        let vn = rel_vel.dot(self.normal);
288        let lambda_n = -(vn - self.bias) * self.eff_mass_n;
289        let prev_n = self.cached_normal;
290        let new_n = (prev_n + lambda_n).max(0.0);
291        let actual_n = new_n - prev_n;
292        self.cached_normal = new_n;
293        self.apply_normal_impulse(bodies, actual_n);
294
295        // Friction
296        let rel_vel2 = self.relative_velocity_at_contact(bodies);
297        let tangent = self.tangent();
298        let vt = rel_vel2.dot(tangent);
299        let lambda_t = -vt * self.eff_mass_t;
300        let max_friction = self.friction * self.cached_normal;
301        let prev_t = self.cached_tangent;
302        let new_t = (prev_t + lambda_t).clamp(-max_friction, max_friction);
303        let actual_t = new_t - prev_t;
304        self.cached_tangent = new_t;
305        self.apply_tangent_impulse(bodies, actual_t);
306    }
307}
308
309impl Constraint for ContactConstraint {
310    fn prepare(&mut self, bodies: &HashMap<BodyHandle, BodyState>, dt: f32) {
311        let ba = bodies.get(&self.body_a);
312        let bb = bodies.get(&self.body_b);
313        self.ra = ba.map(|b| self.contact_point - b.position).unwrap_or(Vec2::ZERO);
314        self.rb = bb.map(|b| self.contact_point - b.position).unwrap_or(Vec2::ZERO);
315
316        self.eff_mass_n = self.compute_effective_mass_along(self.normal, bodies);
317        self.eff_mass_t = self.compute_effective_mass_along(self.tangent(), bodies);
318
319        // Restitution bias: only if separating velocity is significant
320        let rel_vel = self.relative_velocity_at_contact(bodies);
321        let vn = rel_vel.dot(self.normal);
322        let restitution_bias = if vn < -1.0 { -self.restitution * vn } else { 0.0 };
323
324        // Baumgarte bias
325        let baumgarte_bias = self.baumgarte / dt * (self.penetration - self.slop).max(0.0);
326
327        self.bias = restitution_bias + baumgarte_bias;
328        // Note: warm-start impulse application happens in solve_velocity (bodies is &mut there)
329    }
330
331    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
332        self.relative_velocity_at_contact(bodies).dot(self.normal)
333    }
334
335    fn compute_c(&self, _bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
336        -self.penetration
337    }
338
339    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
340        self.compute_effective_mass_along(self.normal, bodies)
341    }
342
343    fn bias(&self, _bodies: &HashMap<BodyHandle, BodyState>, _dt: f32) -> f32 {
344        self.bias
345    }
346
347    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
348        self.apply_normal_impulse(bodies, lambda);
349    }
350
351    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
352        self.solve_contact(bodies, dt);
353    }
354
355    fn impulse_bounds(&self) -> (f32, f32) { (0.0, f32::INFINITY) }
356    fn accumulated_impulse(&self) -> f32 { self.cached_normal }
357    fn reset_accumulated(&mut self) { self.cached_normal = 0.0; self.cached_tangent = 0.0; }
358    fn add_accumulated(&mut self, d: f32) { self.cached_normal = (self.cached_normal + d).max(0.0); }
359    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
360}
361
362// ── DistanceConstraint ────────────────────────────────────────────────────────
363
364/// Maintain a fixed distance between two local anchor points on two bodies.
365/// With compliance = 0 this is a rigid rod; compliance > 0 gives spring behavior.
366#[derive(Debug, Clone)]
367pub struct DistanceConstraint {
368    pub body_a:      BodyHandle,
369    pub body_b:      BodyHandle,
370    pub anchor_a:    Vec2,
371    pub anchor_b:    Vec2,
372    pub rest_length: f32,
373    /// 0.0 = rigid rod, >0 = soft spring (XPBD compliance).
374    pub compliance:  f32,
375    accumulated:     f32,
376}
377
378impl DistanceConstraint {
379    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, rest_length: f32) -> Self {
380        Self { body_a, body_b, anchor_a, anchor_b, rest_length, compliance: 0.0, accumulated: 0.0 }
381    }
382
383    /// Create a rigid rod (exact distance, zero compliance).
384    pub fn rigid_rod(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, rest_length: f32) -> Self {
385        Self::new(body_a, anchor_a, body_b, anchor_b, rest_length)
386    }
387
388    /// Create a soft spring (positive compliance = 1/k).
389    pub fn soft(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, rest_length: f32, compliance: f32) -> Self {
390        Self { body_a, body_b, anchor_a, anchor_b, rest_length, compliance, accumulated: 0.0 }
391    }
392
393    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
394
395    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
396        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
397        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
398        (pa, pb)
399    }
400
401    fn constraint_dir(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, f32) {
402        let (pa, pb) = self.world_anchors(bodies);
403        let delta = pb - pa;
404        let len = delta.length();
405        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
406        (dir, len)
407    }
408}
409
410impl Constraint for DistanceConstraint {
411    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
412        let (pa, pb) = self.world_anchors(bodies);
413        let (dir, _) = self.constraint_dir(bodies);
414        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
415        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
416        (vb - va).dot(dir)
417    }
418
419    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
420        let (_, len) = self.constraint_dir(bodies);
421        len - self.rest_length
422    }
423
424    fn get_compliance(&self) -> f32 { self.compliance }
425
426    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
427        let (pa, pb) = self.world_anchors(bodies);
428        let (dir, _) = self.constraint_dir(bodies);
429        let ba = bodies.get(&self.body_a);
430        let bb = bodies.get(&self.body_b);
431        let ra = ba.map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
432        let rb = bb.map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
433        let im_a = ba.map(|b| b.inv_mass).unwrap_or(0.0);
434        let im_b = bb.map(|b| b.inv_mass).unwrap_or(0.0);
435        let ii_a = ba.map(|b| b.inv_inertia).unwrap_or(0.0);
436        let ii_b = bb.map(|b| b.inv_inertia).unwrap_or(0.0);
437        let rda = cross2(ra, dir);
438        let rdb = cross2(rb, dir);
439        let denom = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb;
440        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
441    }
442
443    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
444        let (pa, pb) = {
445            let ba = bodies.get(&self.body_a);
446            let bb = bodies.get(&self.body_b);
447            (
448                ba.map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO),
449                bb.map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO),
450            )
451        };
452        let delta = pb - pa;
453        let len = delta.length();
454        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
455        let impulse = dir * lambda;
456        if let Some(b) = bodies.get_mut(&self.body_a) {
457            let r = pa - b.position;
458            b.apply_impulse(-impulse, r);
459        }
460        if let Some(b) = bodies.get_mut(&self.body_b) {
461            let r = pb - b.position;
462            b.apply_impulse(impulse, r);
463        }
464    }
465
466    fn accumulated_impulse(&self) -> f32 { self.accumulated }
467    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
468    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
469    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
470}
471
472// ── HingeConstraint ───────────────────────────────────────────────────────────
473
474/// Hinge (revolute) joint: two bodies share a common anchor. Allows rotation
475/// around the Z-axis. Supports angular limits and a velocity motor.
476#[derive(Debug, Clone)]
477pub struct HingeConstraint {
478    pub body_a:      BodyHandle,
479    pub body_b:      BodyHandle,
480    pub anchor_a:    Vec2,
481    pub anchor_b:    Vec2,
482    /// Angular limits: (min, max) relative angle in radians.
483    pub limits:      Option<(f32, f32)>,
484    /// Motor: (target_velocity, max_torque).
485    pub motor:       Option<(f32, f32)>,
486    /// Compliance for soft limits (0 = rigid).
487    pub compliance:  f32,
488    accum_x:         f32,
489    accum_y:         f32,
490    accum_limit:     f32,
491    accum_motor:     f32,
492}
493
494impl HingeConstraint {
495    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2) -> Self {
496        Self {
497            body_a, body_b, anchor_a, anchor_b,
498            limits: None, motor: None, compliance: 0.0,
499            accum_x: 0.0, accum_y: 0.0, accum_limit: 0.0, accum_motor: 0.0,
500        }
501    }
502
503    /// Add angular limits in radians (relative angle of B w.r.t. A).
504    pub fn with_limits(mut self, min: f32, max: f32) -> Self {
505        self.limits = Some((min, max)); self
506    }
507
508    /// Add a rotational motor.
509    pub fn with_motor(mut self, target_velocity: f32, max_torque: f32) -> Self {
510        self.motor = Some((target_velocity, max_torque)); self
511    }
512
513    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
514
515    fn relative_angle(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
516        let angle_a = bodies.get(&self.body_a).map(|b| b.angle).unwrap_or(0.0);
517        let angle_b = bodies.get(&self.body_b).map(|b| b.angle).unwrap_or(0.0);
518        wrap_angle(angle_b - angle_a)
519    }
520
521    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
522        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
523        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
524        (pa, pb)
525    }
526
527    fn positional_effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
528        let (pa, pb) = self.world_anchors(bodies);
529        let delta = pb - pa;
530        let len = delta.length().max(1e-7);
531        let dir = delta / len;
532        let ba = bodies.get(&self.body_a);
533        let bb = bodies.get(&self.body_b);
534        let im_a = ba.map(|b| b.inv_mass).unwrap_or(0.0);
535        let im_b = bb.map(|b| b.inv_mass).unwrap_or(0.0);
536        let ii_a = ba.map(|b| b.inv_inertia).unwrap_or(0.0);
537        let ii_b = bb.map(|b| b.inv_inertia).unwrap_or(0.0);
538        let ra = ba.map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
539        let rb = bb.map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
540        let rda = cross2(ra, dir);
541        let rdb = cross2(rb, dir);
542        let d = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb;
543        if d < 1e-10 { 0.0 } else { 1.0 / d }
544    }
545
546    /// Solve the position constraint (anchor match) and return correction impulse.
547    fn solve_anchor(&self, bodies: &mut HashMap<BodyHandle, BodyState>, beta: f32, dt: f32) {
548        let (pa, pb) = self.world_anchors(bodies);
549        let delta = pb - pa;
550        if delta.length_squared() < 1e-12 { return; }
551        let len = delta.length();
552        let dir = delta / len;
553        let em = self.positional_effective_mass(bodies);
554        if em < 1e-10 { return; }
555        let lambda = -beta / dt * len * em;
556        let impulse = dir * lambda;
557        if let Some(b) = bodies.get_mut(&self.body_a) {
558            b.apply_impulse(-impulse, pa - b.position);
559        }
560        if let Some(b) = bodies.get_mut(&self.body_b) {
561            b.apply_impulse(impulse, pb - b.position);
562        }
563    }
564
565    /// Solve the limit constraint (angular).
566    fn solve_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
567        let Some((min, max)) = self.limits else { return };
568        let rel_angle = self.relative_angle(bodies);
569        let violation = if rel_angle < min {
570            rel_angle - min
571        } else if rel_angle > max {
572            rel_angle - max
573        } else {
574            return;
575        };
576        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
577        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
578        let denom = ii_a + ii_b + self.compliance / (dt * dt);
579        if denom < 1e-10 { return; }
580        let lambda = -violation / denom;
581        let prev = self.accum_limit;
582        let new_acc = if rel_angle < min {
583            (prev + lambda).max(0.0)
584        } else {
585            (prev + lambda).min(0.0)
586        };
587        let actual = new_acc - prev;
588        self.accum_limit = new_acc;
589        if let Some(b) = bodies.get_mut(&self.body_a) {
590            b.apply_angular_impulse(-actual);
591        }
592        if let Some(b) = bodies.get_mut(&self.body_b) {
593            b.apply_angular_impulse(actual);
594        }
595    }
596
597    /// Solve the motor constraint.
598    fn solve_motor(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
599        let Some((target_vel, max_torque)) = self.motor else { return };
600        let omega_a = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
601        let omega_b = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
602        let rel_omega = omega_b - omega_a;
603        let error = rel_omega - target_vel;
604        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
605        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
606        let denom = ii_a + ii_b;
607        if denom < 1e-10 { return; }
608        let lambda = -error / denom;
609        let prev = self.accum_motor;
610        let new_acc = (prev + lambda).clamp(-max_torque, max_torque);
611        let actual = new_acc - prev;
612        self.accum_motor = new_acc;
613        if let Some(b) = bodies.get_mut(&self.body_a) {
614            b.apply_angular_impulse(-actual);
615        }
616        if let Some(b) = bodies.get_mut(&self.body_b) {
617            b.apply_angular_impulse(actual);
618        }
619    }
620}
621
622impl Constraint for HingeConstraint {
623    fn prepare(&mut self, _bodies: &HashMap<BodyHandle, BodyState>, _dt: f32) {
624        // Warm-start: accumulators are kept from the previous frame
625    }
626
627    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
628        let (pa, pb) = self.world_anchors(bodies);
629        let delta = pb - pa;
630        let len = delta.length().max(1e-7);
631        let dir = delta / len;
632        let ba = bodies.get(&self.body_a);
633        let bb = bodies.get(&self.body_b);
634        let va = ba.map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
635        let vb = bb.map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
636        (vb - va).dot(dir)
637    }
638
639    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
640        let (pa, pb) = self.world_anchors(bodies);
641        (pb - pa).length()
642    }
643
644    fn get_compliance(&self) -> f32 { self.compliance }
645
646    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
647        self.positional_effective_mass(bodies)
648    }
649
650    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
651        let (pa, pb) = self.world_anchors(bodies);
652        let delta = pb - pa;
653        let len = delta.length();
654        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
655        let impulse = dir * lambda;
656        if let Some(b) = bodies.get_mut(&self.body_a) {
657            b.apply_impulse(-impulse, pa - b.position);
658        }
659        if let Some(b) = bodies.get_mut(&self.body_b) {
660            b.apply_impulse(impulse, pb - b.position);
661        }
662    }
663
664    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
665        // Positional anchor constraint
666        let em = self.effective_mass(bodies);
667        if em > 1e-10 {
668            let cdot = self.compute_cdot(bodies);
669            let bias = self.bias(bodies, dt);
670            let delta = -(cdot + bias) * em;
671            let prev = self.accum_x;
672            let new_acc = prev + delta;
673            let actual = new_acc - prev;
674            self.accum_x = new_acc;
675            if actual.abs() > 1e-14 {
676                self.apply_impulse(bodies, actual);
677            }
678        }
679        // Limit and motor
680        self.solve_limit(bodies, dt);
681        self.solve_motor(bodies);
682    }
683
684    fn solve_position(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
685        self.solve_anchor(bodies, 0.2, dt);
686    }
687
688    fn accumulated_impulse(&self) -> f32 { self.accum_x }
689    fn reset_accumulated(&mut self) {
690        self.accum_x = 0.0; self.accum_y = 0.0;
691        self.accum_limit = 0.0; self.accum_motor = 0.0;
692    }
693    fn add_accumulated(&mut self, d: f32) { self.accum_x += d; }
694    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
695}
696
697// ── BallSocketConstraint ─────────────────────────────────────────────────────
698
699/// Ball-and-socket joint: 3-DOF rotation, fixed relative position.
700/// Optionally limits the cone angle between the two body Z-axes.
701#[derive(Debug, Clone)]
702pub struct BallSocketConstraint {
703    pub body_a:       BodyHandle,
704    pub body_b:       BodyHandle,
705    pub anchor_a:     Vec2,
706    pub anchor_b:     Vec2,
707    /// Max cone half-angle in radians (None = unlimited).
708    pub cone_limit:   Option<f32>,
709    pub compliance:   f32,
710    accumulated:      f32,
711    accum_cone:       f32,
712}
713
714impl BallSocketConstraint {
715    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2) -> Self {
716        Self { body_a, body_b, anchor_a, anchor_b, cone_limit: None, compliance: 0.0, accumulated: 0.0, accum_cone: 0.0 }
717    }
718
719    pub fn with_cone_limit(mut self, angle: f32) -> Self { self.cone_limit = Some(angle); self }
720    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
721
722    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
723        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
724        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
725        (pa, pb)
726    }
727
728    fn solve_cone_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
729        let Some(max_angle) = self.cone_limit else { return };
730        let angle_a = bodies.get(&self.body_a).map(|b| b.angle).unwrap_or(0.0);
731        let angle_b = bodies.get(&self.body_b).map(|b| b.angle).unwrap_or(0.0);
732        let rel = wrap_angle(angle_b - angle_a);
733        if rel.abs() <= max_angle { return; }
734        let violation = if rel > max_angle { rel - max_angle } else { rel + max_angle };
735        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
736        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
737        let denom = ii_a + ii_b;
738        if denom < 1e-10 { return; }
739        let lambda = -violation / denom;
740        let prev = self.accum_cone;
741        let new_acc = if rel > max_angle { (prev + lambda).min(0.0) } else { (prev + lambda).max(0.0) };
742        let actual = new_acc - prev;
743        self.accum_cone = new_acc;
744        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(-actual); }
745        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(actual); }
746    }
747}
748
749impl Constraint for BallSocketConstraint {
750    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
751        let (pa, pb) = self.world_anchors(bodies);
752        let delta = pb - pa;
753        let len = delta.length().max(1e-7);
754        let dir = delta / len;
755        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
756        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
757        (vb - va).dot(dir)
758    }
759
760    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
761        let (pa, pb) = self.world_anchors(bodies);
762        (pb - pa).length()
763    }
764
765    fn get_compliance(&self) -> f32 { self.compliance }
766
767    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
768        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
769        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
770        let d = im_a + im_b;
771        if d < 1e-10 { 0.0 } else { 1.0 / d }
772    }
773
774    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
775        let (pa, pb) = self.world_anchors(bodies);
776        let delta = pb - pa;
777        let len = delta.length();
778        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
779        let impulse = dir * lambda;
780        if let Some(b) = bodies.get_mut(&self.body_a) {
781            b.apply_impulse(-impulse, pa - b.position);
782        }
783        if let Some(b) = bodies.get_mut(&self.body_b) {
784            b.apply_impulse(impulse, pb - b.position);
785        }
786    }
787
788    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
789        let em = self.effective_mass(bodies);
790        if em > 1e-10 {
791            let cdot = self.compute_cdot(bodies);
792            let bias = self.bias(bodies, dt);
793            let delta = -(cdot + bias) * em;
794            self.accumulated += delta;
795            self.apply_impulse(bodies, delta);
796        }
797        self.solve_cone_limit(bodies);
798    }
799
800    fn accumulated_impulse(&self) -> f32 { self.accumulated }
801    fn reset_accumulated(&mut self) { self.accumulated = 0.0; self.accum_cone = 0.0; }
802    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
803    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
804}
805
806// ── SliderConstraint ──────────────────────────────────────────────────────────
807
808/// Prismatic (slider) joint: allows translation along one axis only.
809/// Supports translation limits and a linear motor.
810#[derive(Debug, Clone)]
811pub struct SliderConstraint {
812    pub body_a:     BodyHandle,
813    pub body_b:     BodyHandle,
814    pub anchor_a:   Vec2,
815    pub anchor_b:   Vec2,
816    /// Slide axis in world space (unit vector).
817    pub axis:       Vec2,
818    /// Translation limits (min, max) in world units.
819    pub limits:     Option<(f32, f32)>,
820    /// Motor: (target_velocity, max_force).
821    pub motor:      Option<(f32, f32)>,
822    pub compliance: f32,
823    accum_perp:     f32,  // perpendicular constraint
824    accum_limit:    f32,
825    accum_motor:    f32,
826}
827
828impl SliderConstraint {
829    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, axis: Vec2) -> Self {
830        let axis = axis.normalize_or_zero();
831        Self {
832            body_a, body_b, anchor_a, anchor_b, axis,
833            limits: None, motor: None, compliance: 0.0,
834            accum_perp: 0.0, accum_limit: 0.0, accum_motor: 0.0,
835        }
836    }
837
838    pub fn with_limits(mut self, min: f32, max: f32) -> Self { self.limits = Some((min, max)); self }
839    pub fn with_motor(mut self, vel: f32, max_force: f32) -> Self { self.motor = Some((vel, max_force)); self }
840    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
841
842    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
843        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
844        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
845        (pa, pb)
846    }
847
848    fn slide_offset(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
849        let (pa, pb) = self.world_anchors(bodies);
850        (pb - pa).dot(self.axis)
851    }
852
853    fn perp_offset(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
854        let (pa, pb) = self.world_anchors(bodies);
855        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
856        (pb - pa).dot(perp_axis)
857    }
858
859    fn solve_perp(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, beta: f32, dt: f32) {
860        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
861        let c = self.perp_offset(bodies);
862        if c.abs() < 1e-8 { return; }
863        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
864        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
865        let (pa, pb) = self.world_anchors(bodies);
866        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
867        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
868        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
869        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
870        let rda = cross2(ra, perp_axis);
871        let rdb = cross2(rb, perp_axis);
872        let denom = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb;
873        if denom < 1e-10 { return; }
874        let lambda = -beta / dt * c / denom;
875        let impulse = perp_axis * lambda;
876        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
877        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
878    }
879
880    fn solve_translation_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
881        let Some((min, max)) = self.limits else { return };
882        let offset = self.slide_offset(bodies);
883        let violation = if offset < min { offset - min } else if offset > max { offset - max } else { return };
884        let (pa, pb) = self.world_anchors(bodies);
885        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
886        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
887        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
888        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
889        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
890        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
891        let rda = cross2(ra, self.axis);
892        let rdb = cross2(rb, self.axis);
893        let denom = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb + self.compliance / (dt * dt);
894        if denom < 1e-10 { return; }
895        let lambda = -violation / denom;
896        let prev = self.accum_limit;
897        let new_acc = if offset < min { (prev + lambda).max(0.0) } else { (prev + lambda).min(0.0) };
898        let actual = new_acc - prev;
899        self.accum_limit = new_acc;
900        let impulse = self.axis * actual;
901        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
902        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
903    }
904
905    fn solve_motor_constraint(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
906        let Some((target_vel, max_force)) = self.motor else { return };
907        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
908        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
909        let rel_vel = (vb - va).dot(self.axis);
910        let error = rel_vel - target_vel;
911        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
912        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
913        let denom = im_a + im_b;
914        if denom < 1e-10 { return; }
915        let lambda = -error / denom;
916        let prev = self.accum_motor;
917        let new_acc = (prev + lambda).clamp(-max_force, max_force);
918        let actual = new_acc - prev;
919        self.accum_motor = new_acc;
920        let impulse = self.axis * actual;
921        let (pa, pb) = self.world_anchors(bodies);
922        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
923        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
924        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
925        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
926    }
927}
928
929impl Constraint for SliderConstraint {
930    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
931        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
932        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
933        (vb - va).dot(Vec2::new(-self.axis.y, self.axis.x))
934    }
935
936    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
937        self.perp_offset(bodies)
938    }
939
940    fn get_compliance(&self) -> f32 { self.compliance }
941
942    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
943        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
944        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
945        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
946        let d = im_a + im_b;
947        if d < 1e-10 { 0.0 } else { 1.0 / d }
948    }
949
950    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
951        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
952        let impulse = perp_axis * lambda;
953        let (pa, pb) = self.world_anchors(bodies);
954        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
955        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
956        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
957        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
958    }
959
960    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
961        self.solve_perp(bodies, 0.1, dt);
962        self.solve_translation_limit(bodies, dt);
963        self.solve_motor_constraint(bodies);
964    }
965
966    fn solve_position(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
967        self.solve_perp(bodies, 0.2, dt);
968    }
969
970    fn accumulated_impulse(&self) -> f32 { self.accum_perp }
971    fn reset_accumulated(&mut self) { self.accum_perp = 0.0; self.accum_limit = 0.0; self.accum_motor = 0.0; }
972    fn add_accumulated(&mut self, d: f32) { self.accum_perp += d; }
973    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
974}
975
976// ── WeldConstraint ────────────────────────────────────────────────────────────
977
978/// Weld joint: fully rigid 6-DOF lock. Fixes both relative position and angle.
979#[derive(Debug, Clone)]
980pub struct WeldConstraint {
981    pub body_a:          BodyHandle,
982    pub body_b:          BodyHandle,
983    pub anchor_a:        Vec2,
984    pub anchor_b:        Vec2,
985    /// Reference relative angle (captured at creation time).
986    pub ref_angle:       f32,
987    /// Angular compliance (0 = perfectly rigid).
988    pub angular_compliance: f32,
989    /// Linear compliance.
990    pub linear_compliance: f32,
991    accum_lin:           f32,
992    accum_ang:           f32,
993}
994
995impl WeldConstraint {
996    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, ref_angle: f32) -> Self {
997        Self {
998            body_a, body_b, anchor_a, anchor_b, ref_angle,
999            angular_compliance: 0.0, linear_compliance: 0.0,
1000            accum_lin: 0.0, accum_ang: 0.0,
1001        }
1002    }
1003
1004    pub fn with_angular_compliance(mut self, c: f32) -> Self { self.angular_compliance = c; self }
1005    pub fn with_linear_compliance(mut self, c: f32) -> Self { self.linear_compliance = c; self }
1006
1007    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
1008        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1009        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1010        (pa, pb)
1011    }
1012
1013    fn angle_error(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1014        let angle_a = bodies.get(&self.body_a).map(|b| b.angle).unwrap_or(0.0);
1015        let angle_b = bodies.get(&self.body_b).map(|b| b.angle).unwrap_or(0.0);
1016        wrap_angle(angle_b - angle_a - self.ref_angle)
1017    }
1018}
1019
1020impl Constraint for WeldConstraint {
1021    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1022        let (pa, pb) = self.world_anchors(bodies);
1023        let delta = pb - pa;
1024        let len = delta.length().max(1e-7);
1025        let dir = delta / len;
1026        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1027        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1028        (vb - va).dot(dir)
1029    }
1030
1031    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1032        let (pa, pb) = self.world_anchors(bodies);
1033        (pb - pa).length()
1034    }
1035
1036    fn get_compliance(&self) -> f32 { self.linear_compliance }
1037
1038    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1039        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1040        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1041        let d = im_a + im_b;
1042        if d < 1e-10 { 0.0 } else { 1.0 / d }
1043    }
1044
1045    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1046        let (pa, pb) = self.world_anchors(bodies);
1047        let delta = pb - pa;
1048        let len = delta.length();
1049        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
1050        let impulse = dir * lambda;
1051        if let Some(b) = bodies.get_mut(&self.body_a) {
1052            b.apply_impulse(-impulse, pa - b.position);
1053        }
1054        if let Some(b) = bodies.get_mut(&self.body_b) {
1055            b.apply_impulse(impulse, pb - b.position);
1056        }
1057    }
1058
1059    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
1060        // Linear
1061        let em_lin = self.effective_mass(bodies);
1062        if em_lin > 1e-10 {
1063            let cdot = self.compute_cdot(bodies);
1064            let bias = self.bias(bodies, dt);
1065            let delta = -(cdot + bias) * em_lin;
1066            self.accum_lin += delta;
1067            self.apply_impulse(bodies, delta);
1068        }
1069        // Angular
1070        let err = self.angle_error(bodies);
1071        let omega_a = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
1072        let omega_b = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
1073        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1074        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1075        let denom = ii_a + ii_b + self.angular_compliance / (dt * dt);
1076        if denom > 1e-10 {
1077            let ang_bias = -0.1 / dt * err;
1078            let rel_omega = omega_b - omega_a;
1079            let lambda = -(rel_omega + ang_bias) / denom;
1080            self.accum_ang += lambda;
1081            if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(-lambda); }
1082            if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(lambda); }
1083        }
1084    }
1085
1086    fn accumulated_impulse(&self) -> f32 { self.accum_lin }
1087    fn reset_accumulated(&mut self) { self.accum_lin = 0.0; self.accum_ang = 0.0; }
1088    fn add_accumulated(&mut self, d: f32) { self.accum_lin += d; }
1089    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1090}
1091
1092// ── PulleyConstraint ──────────────────────────────────────────────────────────
1093
1094/// Pulley constraint: body_a and body_b are connected via a rope over two
1095/// fixed pulley points. Maintains: len_a + ratio * len_b = constant.
1096#[derive(Debug, Clone)]
1097pub struct PulleyConstraint {
1098    pub body_a:       BodyHandle,
1099    pub body_b:       BodyHandle,
1100    pub anchor_a:     Vec2,      // local anchor on body A
1101    pub anchor_b:     Vec2,      // local anchor on body B
1102    pub pulley_a:     Vec2,      // fixed world-space pulley point for A
1103    pub pulley_b:     Vec2,      // fixed world-space pulley point for B
1104    pub ratio:        f32,       // pulley ratio
1105    pub total_length: f32,       // len_a + ratio * len_b at rest
1106    accumulated:      f32,
1107}
1108
1109impl PulleyConstraint {
1110    pub fn new(
1111        body_a: BodyHandle, anchor_a: Vec2, pulley_a: Vec2,
1112        body_b: BodyHandle, anchor_b: Vec2, pulley_b: Vec2,
1113        ratio: f32, total_length: f32,
1114    ) -> Self {
1115        Self { body_a, body_b, anchor_a, anchor_b, pulley_a, pulley_b, ratio, total_length, accumulated: 0.0 }
1116    }
1117
1118    fn world_anchor_a(&self, bodies: &HashMap<BodyHandle, BodyState>) -> Vec2 {
1119        bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO)
1120    }
1121    fn world_anchor_b(&self, bodies: &HashMap<BodyHandle, BodyState>) -> Vec2 {
1122        bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO)
1123    }
1124
1125    fn lengths(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (f32, f32) {
1126        let wa = self.world_anchor_a(bodies);
1127        let wb = self.world_anchor_b(bodies);
1128        let la = (wa - self.pulley_a).length();
1129        let lb = (wb - self.pulley_b).length();
1130        (la, lb)
1131    }
1132
1133    fn dirs(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
1134        let wa = self.world_anchor_a(bodies);
1135        let wb = self.world_anchor_b(bodies);
1136        let da = (wa - self.pulley_a);
1137        let db = (wb - self.pulley_b);
1138        let la = da.length();
1139        let lb = db.length();
1140        (
1141            if la > 1e-7 { da / la } else { Vec2::X },
1142            if lb > 1e-7 { db / lb } else { Vec2::X },
1143        )
1144    }
1145}
1146
1147impl Constraint for PulleyConstraint {
1148    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1149        let (la, lb) = self.lengths(bodies);
1150        la + self.ratio * lb - self.total_length
1151    }
1152
1153    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1154        let wa = self.world_anchor_a(bodies);
1155        let wb = self.world_anchor_b(bodies);
1156        let (da, db) = self.dirs(bodies);
1157        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(wa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1158        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(wb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1159        va.dot(da) + self.ratio * vb.dot(db)
1160    }
1161
1162    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1163        let wa = self.world_anchor_a(bodies);
1164        let wb = self.world_anchor_b(bodies);
1165        let (da, db) = self.dirs(bodies);
1166        let ra = bodies.get(&self.body_a).map(|b| wa - b.position).unwrap_or(Vec2::ZERO);
1167        let rb = bodies.get(&self.body_b).map(|b| wb - b.position).unwrap_or(Vec2::ZERO);
1168        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1169        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1170        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1171        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1172        let rda = cross2(ra, da);
1173        let rdb = cross2(rb, db);
1174        let denom = im_a + ii_a * rda * rda + self.ratio * self.ratio * (im_b + ii_b * rdb * rdb);
1175        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1176    }
1177
1178    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1179        let wa = self.world_anchor_a(bodies);
1180        let wb = self.world_anchor_b(bodies);
1181        let (da, db) = self.dirs(bodies);
1182        let ra = bodies.get(&self.body_a).map(|b| wa - b.position).unwrap_or(Vec2::ZERO);
1183        let rb = bodies.get(&self.body_b).map(|b| wb - b.position).unwrap_or(Vec2::ZERO);
1184        if let Some(b) = bodies.get_mut(&self.body_a) {
1185            b.apply_impulse(-da * lambda, ra);
1186        }
1187        if let Some(b) = bodies.get_mut(&self.body_b) {
1188            b.apply_impulse(-db * (lambda * self.ratio), rb);
1189        }
1190    }
1191
1192    fn impulse_bounds(&self) -> (f32, f32) { (0.0, f32::INFINITY) }
1193    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1194    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1195    fn add_accumulated(&mut self, d: f32) { self.accumulated = (self.accumulated + d).max(0.0); }
1196    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1197}
1198
1199// ── SpringConstraint ──────────────────────────────────────────────────────────
1200
1201/// A spring-damper between two anchor points (Hooke's law).
1202#[derive(Debug, Clone)]
1203pub struct SpringConstraint {
1204    pub body_a:      BodyHandle,
1205    pub body_b:      BodyHandle,
1206    pub anchor_a:    Vec2,
1207    pub anchor_b:    Vec2,
1208    pub rest_length: f32,
1209    pub stiffness:   f32,
1210    pub damping:     f32,
1211    accumulated:     f32,
1212}
1213
1214impl SpringConstraint {
1215    pub fn new(a: BodyHandle, aa: Vec2, b: BodyHandle, ab: Vec2, rest: f32, k: f32, d: f32) -> Self {
1216        Self { body_a: a, body_b: b, anchor_a: aa, anchor_b: ab, rest_length: rest, stiffness: k, damping: d, accumulated: 0.0 }
1217    }
1218
1219    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
1220        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1221        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1222        (pa, pb)
1223    }
1224}
1225
1226impl Constraint for SpringConstraint {
1227    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1228        let (pa, pb) = self.world_anchors(bodies);
1229        (pb - pa).length() - self.rest_length
1230    }
1231
1232    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1233        let (pa, pb) = self.world_anchors(bodies);
1234        let delta = pb - pa;
1235        let len = delta.length().max(1e-7);
1236        let dir = delta / len;
1237        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1238        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1239        (vb - va).dot(dir)
1240    }
1241
1242    fn get_compliance(&self) -> f32 { if self.stiffness > 1e-6 { 1.0 / self.stiffness } else { 0.0 } }
1243
1244    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1245        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1246        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1247        let denom = im_a + im_b + self.get_compliance();
1248        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1249    }
1250
1251    fn bias(&self, bodies: &HashMap<BodyHandle, BodyState>, dt: f32) -> f32 {
1252        self.stiffness * self.compute_c(bodies) / dt
1253    }
1254
1255    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1256        let (pa, pb) = self.world_anchors(bodies);
1257        let delta = pb - pa;
1258        let len = delta.length().max(1e-7);
1259        let dir = delta / len;
1260        let impulse = dir * lambda;
1261        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, Vec2::ZERO); }
1262        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse,  Vec2::ZERO); }
1263    }
1264
1265    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1266    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1267    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1268    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1269}
1270
1271// ── PinConstraint ─────────────────────────────────────────────────────────────
1272
1273/// Pin a body's local anchor to a fixed world-space point.
1274#[derive(Debug, Clone)]
1275pub struct PinConstraint {
1276    pub body:         BodyHandle,
1277    pub local_anchor: Vec2,
1278    pub world_target: Vec2,
1279    pub compliance:   f32,
1280    accumulated:      f32,
1281}
1282
1283impl PinConstraint {
1284    pub fn new(body: BodyHandle, local_anchor: Vec2, world_target: Vec2) -> Self {
1285        Self { body, local_anchor, world_target, compliance: 0.0, accumulated: 0.0 }
1286    }
1287    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
1288}
1289
1290impl Constraint for PinConstraint {
1291    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1292        let pa = bodies.get(&self.body)
1293            .map(|b| b.position + rotate2(self.local_anchor, b.angle))
1294            .unwrap_or(Vec2::ZERO);
1295        (pa - self.world_target).length()
1296    }
1297
1298    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1299        let b = match bodies.get(&self.body) { Some(b) => b, None => return 0.0 };
1300        let pa = b.position + rotate2(self.local_anchor, b.angle);
1301        let dir = (pa - self.world_target).normalize_or_zero();
1302        (b.velocity + perp(pa - b.position) * b.angular_velocity).dot(dir)
1303    }
1304
1305    fn get_compliance(&self) -> f32 { self.compliance }
1306
1307    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1308        let b = match bodies.get(&self.body) { Some(b) => b, None => return 0.0 };
1309        let pa = b.position + rotate2(self.local_anchor, b.angle);
1310        let r = pa - b.position;
1311        let dir = (pa - self.world_target).normalize_or_zero();
1312        let rd = cross2(r, dir);
1313        let denom = b.inv_mass + b.inv_inertia * rd * rd;
1314        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1315    }
1316
1317    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1318        let b = match bodies.get_mut(&self.body) { Some(b) => b, None => return };
1319        let pa = b.position + rotate2(self.local_anchor, b.angle);
1320        let dir = (pa - self.world_target).normalize_or_zero();
1321        b.apply_impulse(dir * lambda, pa - b.position);
1322    }
1323
1324    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1325    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1326    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1327    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body] }
1328}
1329
1330// ── MotorConstraint ───────────────────────────────────────────────────────────
1331
1332/// Drive a body at a target angular velocity. Acts as a torque motor.
1333#[derive(Debug, Clone)]
1334pub struct MotorConstraint {
1335    pub body:            BodyHandle,
1336    pub target_velocity: f32,
1337    pub max_torque:      f32,
1338    accumulated:         f32,
1339}
1340
1341impl MotorConstraint {
1342    pub fn new(body: BodyHandle, target_velocity: f32, max_torque: f32) -> Self {
1343        Self { body, target_velocity, max_torque, accumulated: 0.0 }
1344    }
1345}
1346
1347impl Constraint for MotorConstraint {
1348    fn compute_c(&self, _: &HashMap<BodyHandle, BodyState>) -> f32 { 0.0 }
1349
1350    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1351        bodies.get(&self.body).map(|b| b.angular_velocity).unwrap_or(0.0) - self.target_velocity
1352    }
1353
1354    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1355        let ii = bodies.get(&self.body).map(|b| b.inv_inertia).unwrap_or(0.0);
1356        if ii < 1e-10 { 0.0 } else { 1.0 / ii }
1357    }
1358
1359    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1360        if let Some(b) = bodies.get_mut(&self.body) {
1361            b.angular_velocity += lambda * b.inv_inertia;
1362        }
1363    }
1364
1365    fn impulse_bounds(&self) -> (f32, f32) { (-self.max_torque, self.max_torque) }
1366    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1367    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1368    fn add_accumulated(&mut self, d: f32) {
1369        self.accumulated = (self.accumulated + d).clamp(-self.max_torque, self.max_torque);
1370    }
1371    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body] }
1372}
1373
1374// ── GearConstraint ────────────────────────────────────────────────────────────
1375
1376/// Couple the angular velocities of two bodies: omega_a + ratio * omega_b = 0.
1377#[derive(Debug, Clone)]
1378pub struct GearConstraint {
1379    pub body_a:  BodyHandle,
1380    pub body_b:  BodyHandle,
1381    pub ratio:   f32,
1382    accumulated: f32,
1383}
1384
1385impl GearConstraint {
1386    pub fn new(body_a: BodyHandle, body_b: BodyHandle, ratio: f32) -> Self {
1387        Self { body_a, body_b, ratio, accumulated: 0.0 }
1388    }
1389}
1390
1391impl Constraint for GearConstraint {
1392    fn compute_c(&self, _: &HashMap<BodyHandle, BodyState>) -> f32 { 0.0 }
1393
1394    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1395        let oa = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
1396        let ob = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
1397        oa + self.ratio * ob
1398    }
1399
1400    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1401        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1402        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1403        let d = ii_a + self.ratio * self.ratio * ii_b;
1404        if d < 1e-10 { 0.0 } else { 1.0 / d }
1405    }
1406
1407    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1408        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(lambda); }
1409        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(lambda * self.ratio); }
1410    }
1411
1412    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1413    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1414    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1415    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1416}
1417
1418// ── MaxDistanceConstraint ─────────────────────────────────────────────────────
1419
1420/// Soft rope: only constrains when distance exceeds max_distance.
1421#[derive(Debug, Clone)]
1422pub struct MaxDistanceConstraint {
1423    pub body_a:       BodyHandle,
1424    pub body_b:       BodyHandle,
1425    pub anchor_a:     Vec2,
1426    pub anchor_b:     Vec2,
1427    pub max_distance: f32,
1428    pub compliance:   f32,
1429    accumulated:      f32,
1430}
1431
1432impl MaxDistanceConstraint {
1433    pub fn new(a: BodyHandle, aa: Vec2, b: BodyHandle, ab: Vec2, max_dist: f32) -> Self {
1434        Self { body_a: a, body_b: b, anchor_a: aa, anchor_b: ab, max_distance: max_dist, compliance: 0.0, accumulated: 0.0 }
1435    }
1436
1437    fn current_length(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1438        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1439        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1440        (pb - pa).length()
1441    }
1442}
1443
1444impl Constraint for MaxDistanceConstraint {
1445    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1446        (self.current_length(bodies) - self.max_distance).max(0.0)
1447    }
1448
1449    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1450        if self.compute_c(bodies) <= 0.0 { return 0.0; }
1451        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1452        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1453        let delta = pb - pa;
1454        let len = delta.length().max(1e-7);
1455        let dir = delta / len;
1456        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1457        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1458        (vb - va).dot(dir)
1459    }
1460
1461    fn get_compliance(&self) -> f32 { self.compliance }
1462
1463    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1464        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1465        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1466        let d = im_a + im_b;
1467        if d < 1e-10 { 0.0 } else { 1.0 / d }
1468    }
1469
1470    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1471        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1472        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1473        let delta = pb - pa;
1474        let len = delta.length().max(1e-7);
1475        let dir = delta / len;
1476        let imp = dir * lambda;
1477        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-imp, Vec2::ZERO); }
1478        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(imp,  Vec2::ZERO); }
1479    }
1480
1481    fn impulse_bounds(&self) -> (f32, f32) { (0.0, f32::INFINITY) }
1482    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1483    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1484    fn add_accumulated(&mut self, d: f32) { self.accumulated = (self.accumulated + d).max(0.0); }
1485    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1486}
1487
1488// ── ConstraintSolver ──────────────────────────────────────────────────────────
1489
1490/// Sequential-impulse + XPBD constraint solver.
1491///
1492/// Call `solve()` once per physics step. Internally iterates
1493/// `iteration_count` times applying velocity and/or position corrections.
1494pub struct ConstraintSolver {
1495    pub iteration_count:  u32,
1496    pub baumgarte_factor: f32,
1497    pub warm_start:       bool,
1498    /// Number of XPBD substeps (for solve_xpbd).
1499    pub substep_count:    u32,
1500}
1501
1502impl Default for ConstraintSolver {
1503    fn default() -> Self {
1504        Self { iteration_count: 10, baumgarte_factor: 0.2, warm_start: true, substep_count: 4 }
1505    }
1506}
1507
1508impl ConstraintSolver {
1509    pub fn new(iterations: u32) -> Self {
1510        Self { iteration_count: iterations, ..Default::default() }
1511    }
1512
1513    /// Solve all constraints using sequential impulses (velocity level).
1514    pub fn solve(
1515        &self,
1516        bodies:      &mut HashMap<BodyHandle, BodyState>,
1517        constraints: &mut [Box<dyn Constraint>],
1518        dt:          f32,
1519    ) {
1520        if dt < 1e-8 { return; }
1521
1522        // Warm starting: apply accumulated impulses from last frame
1523        if self.warm_start {
1524            for c in constraints.iter() {
1525                let lambda = c.accumulated_impulse();
1526                if lambda.abs() > 1e-10 {
1527                    c.apply_impulse(bodies, lambda * 0.8); // scale down slightly
1528                }
1529            }
1530        } else {
1531            for c in constraints.iter_mut() { c.reset_accumulated(); }
1532        }
1533
1534        // Prepare constraints (compute cached values, apply warm start)
1535        for c in constraints.iter_mut() {
1536            c.prepare(bodies, dt);
1537        }
1538
1539        // Iterative velocity solve
1540        for _ in 0..self.iteration_count {
1541            for c in constraints.iter_mut() {
1542                c.solve_velocity(bodies, dt);
1543            }
1544        }
1545    }
1546
1547    /// Apply position correction (post-solve) using Baumgarte stabilization.
1548    pub fn solve_positions(
1549        &self,
1550        bodies:      &mut HashMap<BodyHandle, BodyState>,
1551        constraints: &mut [Box<dyn Constraint>],
1552        dt:          f32,
1553    ) {
1554        let alpha = self.baumgarte_factor / dt.max(1e-6);
1555        for c in constraints.iter_mut() {
1556            let em = c.effective_mass(bodies);
1557            if em < 1e-10 { continue; }
1558            let c_val = c.compute_c(bodies);
1559            if c_val.abs() < 1e-5 { continue; }
1560            let lambda = -alpha * c_val * em;
1561            c.apply_impulse(bodies, lambda);
1562        }
1563    }
1564
1565    /// Solve using XPBD with substep integration.
1566    pub fn solve_xpbd(
1567        &self,
1568        bodies:      &mut HashMap<BodyHandle, BodyState>,
1569        constraints: &mut [Box<dyn Constraint>],
1570        dt:          f32,
1571    ) {
1572        if dt < 1e-8 { return; }
1573        let sub_dt = dt / self.substep_count as f32;
1574
1575        for _ in 0..self.substep_count {
1576            // Reset accumulated lambdas for XPBD
1577            for c in constraints.iter_mut() { c.reset_accumulated(); }
1578
1579            // Integrate velocities
1580            for body in bodies.values_mut() {
1581                if !body.is_static() {
1582                    body.position += body.velocity * sub_dt;
1583                    body.angle    += body.angular_velocity * sub_dt;
1584                }
1585            }
1586
1587            // Position-level constraint solve
1588            for _ in 0..self.iteration_count {
1589                for c in constraints.iter_mut() {
1590                    c.solve_position(bodies, sub_dt);
1591                }
1592            }
1593
1594            // Derive velocities from position corrections
1595            // (velocities are updated by apply_impulse inside solve_position)
1596        }
1597    }
1598}
1599
1600// ── Constraint Island Detection ───────────────────────────────────────────────
1601
1602/// A group of bodies and constraints that are connected and should be solved together.
1603#[derive(Debug, Clone)]
1604pub struct ConstraintIsland {
1605    pub body_handles:      Vec<BodyHandle>,
1606    pub constraint_indices: Vec<usize>,
1607}
1608
1609/// Detect constraint islands using union-find.
1610pub fn detect_islands(
1611    bodies: &HashMap<BodyHandle, BodyState>,
1612    constraints: &[Box<dyn Constraint>],
1613) -> Vec<ConstraintIsland> {
1614    let body_list: Vec<BodyHandle> = bodies.keys().copied().collect();
1615    let n = body_list.len();
1616    if n == 0 { return Vec::new(); }
1617
1618    // Map handle -> index
1619    let mut handle_to_idx: HashMap<BodyHandle, usize> = HashMap::new();
1620    for (i, h) in body_list.iter().enumerate() {
1621        handle_to_idx.insert(*h, i);
1622    }
1623
1624    // Union-Find
1625    let mut parent: Vec<usize> = (0..n).collect();
1626    let mut rank: Vec<usize> = vec![0; n];
1627
1628    fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1629        if parent[x] != x { parent[x] = find(parent, parent[x]); }
1630        parent[x]
1631    }
1632
1633    fn union(parent: &mut Vec<usize>, rank: &mut Vec<usize>, x: usize, y: usize) {
1634        let rx = find(parent, x);
1635        let ry = find(parent, y);
1636        if rx == ry { return; }
1637        if rank[rx] < rank[ry] { parent[rx] = ry; }
1638        else if rank[rx] > rank[ry] { parent[ry] = rx; }
1639        else { parent[ry] = rx; rank[rx] += 1; }
1640    }
1641
1642    // Union bodies connected by constraints
1643    for c in constraints.iter() {
1644        let handles = c.body_handles();
1645        if handles.len() < 2 { continue; }
1646        for i in 1..handles.len() {
1647            if let (Some(&ia), Some(&ib)) = (handle_to_idx.get(&handles[0]), handle_to_idx.get(&handles[i])) {
1648                union(&mut parent, &mut rank, ia, ib);
1649            }
1650        }
1651    }
1652
1653    // Group by root
1654    let mut island_map: HashMap<usize, usize> = HashMap::new();
1655    let mut islands: Vec<ConstraintIsland> = Vec::new();
1656
1657    for (i, h) in body_list.iter().enumerate() {
1658        let root = find(&mut parent, i);
1659        let island_idx = *island_map.entry(root).or_insert_with(|| {
1660            let idx = islands.len();
1661            islands.push(ConstraintIsland { body_handles: Vec::new(), constraint_indices: Vec::new() });
1662            idx
1663        });
1664        islands[island_idx].body_handles.push(*h);
1665    }
1666
1667    // Assign constraints to islands
1668    for (ci, c) in constraints.iter().enumerate() {
1669        let handles = c.body_handles();
1670        if handles.is_empty() { continue; }
1671        let first = handles[0];
1672        if let Some(&idx) = handle_to_idx.get(&first) {
1673            let root = find(&mut parent, idx);
1674            if let Some(&island_idx) = island_map.get(&root) {
1675                islands[island_idx].constraint_indices.push(ci);
1676            }
1677        }
1678    }
1679
1680    islands
1681}
1682
1683// ── ConstraintWorld ───────────────────────────────────────────────────────────
1684
1685/// Manages all bodies and constraints in one place.
1686pub struct ConstraintWorld {
1687    pub bodies:      HashMap<BodyHandle, BodyState>,
1688    pub constraints: Vec<Box<dyn Constraint>>,
1689    pub solver:      ConstraintSolver,
1690    pub gravity:     Vec2,
1691    next_body_id:    u32,
1692    next_con_id:     u32,
1693    /// Whether to use island-based solving.
1694    pub use_islands: bool,
1695    /// Whether to use XPBD substep integration.
1696    pub use_xpbd:    bool,
1697}
1698
1699impl ConstraintWorld {
1700    pub fn new() -> Self {
1701        Self {
1702            bodies:        HashMap::new(),
1703            constraints:   Vec::new(),
1704            solver:        ConstraintSolver::default(),
1705            gravity:       Vec2::new(0.0, -9.81),
1706            next_body_id:  1,
1707            next_con_id:   1,
1708            use_islands:   false,
1709            use_xpbd:      false,
1710        }
1711    }
1712
1713    pub fn add_body(&mut self, state: BodyState) -> BodyHandle {
1714        let id = BodyHandle(self.next_body_id);
1715        self.next_body_id += 1;
1716        self.bodies.insert(id, state);
1717        id
1718    }
1719
1720    pub fn add_constraint(&mut self, c: Box<dyn Constraint>) -> ConstraintId {
1721        let id = ConstraintId(self.next_con_id);
1722        self.next_con_id += 1;
1723        self.constraints.push(c);
1724        id
1725    }
1726
1727    pub fn remove_body(&mut self, handle: BodyHandle) {
1728        self.bodies.remove(&handle);
1729    }
1730
1731    pub fn step(&mut self, dt: f32) {
1732        if dt < 1e-8 { return; }
1733
1734        // Apply gravity
1735        for body in self.bodies.values_mut() {
1736            if !body.is_static() {
1737                body.velocity += self.gravity * dt;
1738            }
1739        }
1740
1741        if self.use_xpbd {
1742            // XPBD path: integrate then position-solve
1743            for body in self.bodies.values_mut() {
1744                if !body.is_static() {
1745                    body.position += body.velocity * dt;
1746                    body.angle    += body.angular_velocity * dt;
1747                }
1748            }
1749            self.solver.solve_xpbd(&mut self.bodies, &mut self.constraints, dt);
1750        } else {
1751            // SI path
1752            self.solver.solve(&mut self.bodies, &mut self.constraints, dt);
1753            self.solver.solve_positions(&mut self.bodies, &mut self.constraints, dt);
1754
1755            // Integrate velocities to positions
1756            for body in self.bodies.values_mut() {
1757                if !body.is_static() {
1758                    body.position += body.velocity * dt;
1759                    body.angle    += body.angular_velocity * dt;
1760                }
1761            }
1762        }
1763    }
1764
1765    pub fn body(&self, h: BodyHandle) -> Option<&BodyState> { self.bodies.get(&h) }
1766    pub fn body_mut(&mut self, h: BodyHandle) -> Option<&mut BodyState> { self.bodies.get_mut(&h) }
1767
1768    pub fn body_count(&self) -> usize { self.bodies.len() }
1769    pub fn constraint_count(&self) -> usize { self.constraints.len() }
1770
1771    /// Compute total kinetic energy.
1772    pub fn total_kinetic_energy(&self) -> f32 {
1773        self.bodies.values().map(|b| b.kinetic_energy()).sum()
1774    }
1775
1776    /// Remove all sleeping bodies from the simulation.
1777    pub fn remove_sleeping(&mut self, threshold: f32) {
1778        let sleeping: Vec<BodyHandle> = self.bodies.iter()
1779            .filter(|(_, b)| b.kinetic_energy() < threshold)
1780            .map(|(h, _)| *h)
1781            .collect();
1782        for h in sleeping { self.bodies.remove(&h); }
1783    }
1784}
1785
1786impl Default for ConstraintWorld {
1787    fn default() -> Self { Self::new() }
1788}
1789
1790// ── Tests ─────────────────────────────────────────────────────────────────────
1791
1792#[cfg(test)]
1793mod tests {
1794    use super::*;
1795    use std::f32::consts::PI;
1796
1797    fn make_two_bodies(world: &mut ConstraintWorld, mass: f32, pos_a: Vec2, pos_b: Vec2) -> (BodyHandle, BodyHandle) {
1798        let a = world.add_body(BodyState::new(pos_a, mass, mass * 0.1));
1799        let b = world.add_body(BodyState::new(pos_b, mass, mass * 0.1));
1800        (a, b)
1801    }
1802
1803    #[test]
1804    fn distance_constraint_rigid_rod() {
1805        let mut world = ConstraintWorld::new();
1806        world.gravity = Vec2::ZERO;
1807        let (a, b) = make_two_bodies(&mut world, 1.0, Vec2::ZERO, Vec2::new(3.0, 0.0));
1808        let c = Box::new(DistanceConstraint::rigid_rod(a, Vec2::ZERO, b, Vec2::ZERO, 2.0));
1809        world.add_constraint(c);
1810        for _ in 0..60 {
1811            world.step(1.0 / 60.0);
1812        }
1813        let pa = world.body(a).unwrap().position;
1814        let pb = world.body(b).unwrap().position;
1815        let dist = (pb - pa).length();
1816        assert!((dist - 2.0).abs() < 0.1, "rod should maintain 2m distance, got {dist}");
1817    }
1818
1819    #[test]
1820    fn distance_constraint_soft_spring() {
1821        let mut world = ConstraintWorld::new();
1822        world.gravity = Vec2::ZERO;
1823        let (a, b) = make_two_bodies(&mut world, 1.0, Vec2::ZERO, Vec2::new(5.0, 0.0));
1824        let c = Box::new(DistanceConstraint::soft(a, Vec2::ZERO, b, Vec2::ZERO, 2.0, 0.01));
1825        world.add_constraint(c);
1826        world.step(1.0 / 60.0);
1827        // Compliance means it gives a bit — just ensure it ran without panic
1828        let pb = world.body(b).unwrap().position;
1829        assert!(pb.x.is_finite());
1830    }
1831
1832    #[test]
1833    fn hinge_constraint_limits() {
1834        let mut world = ConstraintWorld::new();
1835        world.gravity = Vec2::ZERO;
1836        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1837        let b = world.add_body({
1838            let mut s = BodyState::new(Vec2::new(0.0, 1.0), 1.0, 0.1);
1839            s.angular_velocity = 10.0;
1840            s
1841        });
1842        let c = Box::new(
1843            HingeConstraint::new(a, Vec2::ZERO, b, Vec2::new(0.0, -1.0))
1844                .with_limits(-PI / 4.0, PI / 4.0)
1845        );
1846        world.add_constraint(c);
1847        for _ in 0..120 {
1848            world.step(1.0 / 60.0);
1849        }
1850        let angle = world.body(b).unwrap().angle;
1851        assert!(angle.abs() <= PI / 4.0 + 0.1, "angle {} should be within limits", angle);
1852    }
1853
1854    #[test]
1855    fn hinge_constraint_motor() {
1856        let mut world = ConstraintWorld::new();
1857        world.gravity = Vec2::ZERO;
1858        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1859        let b = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 1.0));
1860        let c = Box::new(
1861            HingeConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO)
1862                .with_motor(2.0 * PI, 100.0)
1863        );
1864        world.add_constraint(c);
1865        for _ in 0..60 {
1866            world.step(1.0 / 60.0);
1867        }
1868        let omega = world.body(b).unwrap().angular_velocity;
1869        assert!(omega > 0.0, "motor should drive positive angular velocity, got {omega}");
1870    }
1871
1872    #[test]
1873    fn ball_socket_cone_limit() {
1874        let mut world = ConstraintWorld::new();
1875        world.gravity = Vec2::ZERO;
1876        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1877        let b = world.add_body({
1878            let mut s = BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1);
1879            s.angular_velocity = 5.0;
1880            s
1881        });
1882        let c = Box::new(
1883            BallSocketConstraint::new(a, Vec2::ZERO, b, Vec2::new(-1.0, 0.0))
1884                .with_cone_limit(PI / 6.0)
1885        );
1886        world.add_constraint(c);
1887        for _ in 0..60 {
1888            world.step(1.0 / 60.0);
1889        }
1890        let angle = world.body(b).unwrap().angle;
1891        assert!(angle.abs() <= PI / 6.0 + 0.2, "cone angle exceeded");
1892    }
1893
1894    #[test]
1895    fn slider_constraint_limits() {
1896        let mut world = ConstraintWorld::new();
1897        world.gravity = Vec2::ZERO;
1898        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1899        let b = world.add_body({
1900            let mut s = BodyState::new(Vec2::new(0.5, 0.0), 1.0, 0.1);
1901            s.velocity = Vec2::new(5.0, 0.0);
1902            s
1903        });
1904        let c = Box::new(
1905            SliderConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, Vec2::X)
1906                .with_limits(-1.0, 1.0)
1907        );
1908        world.add_constraint(c);
1909        for _ in 0..120 {
1910            world.step(1.0 / 60.0);
1911        }
1912        let pos_x = world.body(b).unwrap().position.x;
1913        assert!(pos_x <= 1.5, "slider should be limited, got x={pos_x}");
1914    }
1915
1916    #[test]
1917    fn weld_constraint_holds() {
1918        let mut world = ConstraintWorld::new();
1919        world.gravity = Vec2::new(0.0, -9.81);
1920        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1921        let b = world.add_body(BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1));
1922        let c = Box::new(WeldConstraint::new(a, Vec2::new(1.0, 0.0), b, Vec2::ZERO, 0.0));
1923        world.add_constraint(c);
1924        for _ in 0..60 {
1925            world.step(1.0 / 60.0);
1926        }
1927        let pos = world.body(b).unwrap().position;
1928        assert!((pos - Vec2::new(1.0, 0.0)).length() < 0.5, "weld should hold, drift={}", (pos - Vec2::new(1.0, 0.0)).length());
1929    }
1930
1931    #[test]
1932    fn pulley_constraint_ratio() {
1933        let mut world = ConstraintWorld::new();
1934        world.gravity = Vec2::ZERO;
1935        let a = world.add_body(BodyState::new(Vec2::new(-3.0, 0.0), 1.0, 0.1));
1936        let b = world.add_body(BodyState::new(Vec2::new( 3.0, 0.0), 1.0, 0.1));
1937        let init_la = (Vec2::new(-3.0, 0.0) - Vec2::new(-1.0, 2.0)).length();
1938        let init_lb = (Vec2::new( 3.0, 0.0) - Vec2::new( 1.0, 2.0)).length();
1939        let total = init_la + init_lb;
1940        let c = Box::new(PulleyConstraint::new(
1941            a, Vec2::ZERO, Vec2::new(-1.0, 2.0),
1942            b, Vec2::ZERO, Vec2::new( 1.0, 2.0),
1943            1.0, total,
1944        ));
1945        world.add_constraint(c);
1946        world.step(1.0 / 60.0);
1947        let pa = world.body(a).unwrap().position;
1948        assert!(pa.is_finite(), "pulley should not produce NaN");
1949    }
1950
1951    #[test]
1952    fn spring_constraint_oscillates() {
1953        let mut world = ConstraintWorld::new();
1954        world.gravity = Vec2::ZERO;
1955        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1956        let b = world.add_body(BodyState::new(Vec2::new(3.0, 0.0), 1.0, 0.1));
1957        let c = Box::new(SpringConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 1.0, 50.0, 0.5));
1958        world.add_constraint(c);
1959        let pos_before = world.body(b).unwrap().position.x;
1960        world.step(0.016);
1961        let pos_after = world.body(b).unwrap().position.x;
1962        assert!(pos_after < pos_before, "spring should pull body toward rest length");
1963    }
1964
1965    #[test]
1966    fn motor_constraint_drives_rotation() {
1967        let mut world = ConstraintWorld::new();
1968        world.gravity = Vec2::ZERO;
1969        let a = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 1.0));
1970        let c = Box::new(MotorConstraint::new(a, 5.0, 100.0));
1971        world.add_constraint(c);
1972        for _ in 0..30 {
1973            world.step(1.0 / 60.0);
1974        }
1975        let omega = world.body(a).unwrap().angular_velocity;
1976        assert!(omega > 0.0, "motor should spin body, got {omega}");
1977    }
1978
1979    #[test]
1980    fn gear_constraint_couples_bodies() {
1981        let mut world = ConstraintWorld::new();
1982        world.gravity = Vec2::ZERO;
1983        let a = world.add_body({
1984            let mut s = BodyState::new(Vec2::ZERO, 1.0, 1.0);
1985            s.angular_velocity = 2.0;
1986            s
1987        });
1988        let b = world.add_body(BodyState::new(Vec2::new(2.0, 0.0), 1.0, 1.0));
1989        let c = Box::new(GearConstraint::new(a, b, 2.0));
1990        world.add_constraint(c);
1991        for _ in 0..30 {
1992            world.step(1.0 / 60.0);
1993        }
1994        let oa = world.body(a).unwrap().angular_velocity;
1995        let ob = world.body(b).unwrap().angular_velocity;
1996        // omega_a + 2 * omega_b ~ 0
1997        let coupling_err = (oa + 2.0 * ob).abs();
1998        assert!(coupling_err < 1.0, "gear coupling error too large: {coupling_err}");
1999    }
2000
2001    #[test]
2002    fn max_distance_constraint_rope() {
2003        let mut world = ConstraintWorld::new();
2004        world.gravity = Vec2::ZERO;
2005        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
2006        let b = world.add_body({
2007            let mut s = BodyState::new(Vec2::new(3.0, 0.0), 1.0, 0.1);
2008            s.velocity = Vec2::new(10.0, 0.0);
2009            s
2010        });
2011        let c = Box::new(MaxDistanceConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 2.0));
2012        world.add_constraint(c);
2013        for _ in 0..60 {
2014            world.step(1.0 / 60.0);
2015        }
2016        let dist = world.body(b).unwrap().position.length();
2017        assert!(dist <= 2.5, "rope should limit distance, got {dist}");
2018    }
2019
2020    #[test]
2021    fn pin_constraint_holds_at_world_point() {
2022        let mut world = ConstraintWorld::new();
2023        world.gravity = Vec2::new(0.0, -9.81);
2024        let a = world.add_body(BodyState::new(Vec2::new(0.0, 5.0), 1.0, 0.1));
2025        let target = Vec2::new(0.0, 5.0);
2026        let c = Box::new(PinConstraint::new(a, Vec2::ZERO, target));
2027        world.add_constraint(c);
2028        for _ in 0..60 {
2029            world.step(1.0 / 60.0);
2030        }
2031        let pos = world.body(a).unwrap().position;
2032        let err = (pos - target).length();
2033        assert!(err < 0.5, "pin should hold, error={err}");
2034    }
2035
2036    #[test]
2037    fn constraint_world_gravity_applied() {
2038        let mut world = ConstraintWorld::new();
2039        let a = world.add_body(BodyState::new(Vec2::new(0.0, 10.0), 1.0, 0.1));
2040        world.step(1.0);
2041        let pos = world.body(a).unwrap().position;
2042        assert!(pos.y < 10.0, "gravity should pull body down");
2043    }
2044
2045    #[test]
2046    fn island_detection_finds_connected_components() {
2047        let mut world = ConstraintWorld::new();
2048        let a = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 0.1));
2049        let b = world.add_body(BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1));
2050        let c = world.add_body(BodyState::new(Vec2::new(5.0, 0.0), 1.0, 0.1));  // isolated
2051        world.add_constraint(Box::new(DistanceConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 1.0)));
2052        let islands = detect_islands(&world.bodies, &world.constraints);
2053        // Should find 2 islands: {a,b} and {c}
2054        assert_eq!(islands.len(), 2, "should detect 2 islands");
2055    }
2056
2057    #[test]
2058    fn contact_constraint_non_penetration() {
2059        let mut bodies: HashMap<BodyHandle, BodyState> = HashMap::new();
2060        let h_a = BodyHandle(1);
2061        let h_b = BodyHandle(2);
2062        let mut ba = BodyState::new(Vec2::ZERO, 1.0, 0.1);
2063        let mut bb = BodyState::new(Vec2::new(0.5, 0.0), 1.0, 0.1);
2064        ba.velocity = Vec2::new(1.0, 0.0);
2065        bb.velocity = Vec2::new(-1.0, 0.0);
2066        bodies.insert(h_a, ba);
2067        bodies.insert(h_b, bb);
2068        let mut c = ContactConstraint::new(
2069            h_a, h_b,
2070            Vec2::new(0.25, 0.0), Vec2::X, 0.5,
2071            0.3, 0.5,
2072        );
2073        c.prepare(&bodies, 1.0 / 60.0);
2074        c.solve_contact(&mut bodies, 1.0 / 60.0);
2075        let va = bodies[&h_a].velocity.x;
2076        let vb = bodies[&h_b].velocity.x;
2077        assert!(vb >= va - 0.01, "relative velocity after impulse should be non-negative");
2078    }
2079
2080    #[test]
2081    fn total_kinetic_energy_is_finite() {
2082        let mut world = ConstraintWorld::new();
2083        for i in 0..10 {
2084            let s = BodyState::new(Vec2::new(i as f32, 0.0), 1.0, 0.1);
2085            world.add_body(s);
2086        }
2087        for _ in 0..60 {
2088            world.step(1.0 / 60.0);
2089        }
2090        let ke = world.total_kinetic_energy();
2091        assert!(ke.is_finite(), "KE should be finite");
2092    }
2093}