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 ba = bodies.get(&self.body_a);
529        let bb = bodies.get(&self.body_b);
530        let im_a = ba.map(|b| b.inv_mass).unwrap_or(0.0);
531        let im_b = bb.map(|b| b.inv_mass).unwrap_or(0.0);
532        let d = im_a + im_b;
533        if d < 1e-10 { 0.0 } else { 1.0 / d }
534    }
535
536    /// Solve the position constraint (anchor match) and return correction impulse.
537    fn solve_anchor(&self, bodies: &mut HashMap<BodyHandle, BodyState>, beta: f32, dt: f32) {
538        let (pa, pb) = self.world_anchors(bodies);
539        let delta = pb - pa;
540        if delta.length_squared() < 1e-12 { return; }
541        let len = delta.length();
542        let dir = delta / len;
543        let em = self.positional_effective_mass(bodies);
544        if em < 1e-10 { return; }
545        let lambda = -beta / dt * len * em;
546        let impulse = dir * lambda;
547        if let Some(b) = bodies.get_mut(&self.body_a) {
548            b.apply_impulse(-impulse, pa - b.position);
549        }
550        if let Some(b) = bodies.get_mut(&self.body_b) {
551            b.apply_impulse(impulse, pb - b.position);
552        }
553    }
554
555    /// Solve the limit constraint (angular).
556    fn solve_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
557        let Some((min, max)) = self.limits else { return };
558        let rel_angle = self.relative_angle(bodies);
559        let violation = if rel_angle < min {
560            rel_angle - min
561        } else if rel_angle > max {
562            rel_angle - max
563        } else {
564            return;
565        };
566        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
567        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
568        let denom = ii_a + ii_b + self.compliance / (dt * dt);
569        if denom < 1e-10 { return; }
570        let lambda = -violation / denom;
571        let prev = self.accum_limit;
572        let new_acc = if rel_angle < min {
573            (prev + lambda).max(0.0)
574        } else {
575            (prev + lambda).min(0.0)
576        };
577        let actual = new_acc - prev;
578        self.accum_limit = new_acc;
579        if let Some(b) = bodies.get_mut(&self.body_a) {
580            b.apply_angular_impulse(-actual);
581        }
582        if let Some(b) = bodies.get_mut(&self.body_b) {
583            b.apply_angular_impulse(actual);
584        }
585    }
586
587    /// Solve the motor constraint.
588    fn solve_motor(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
589        let Some((target_vel, max_torque)) = self.motor else { return };
590        let omega_a = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
591        let omega_b = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
592        let rel_omega = omega_b - omega_a;
593        let error = rel_omega - target_vel;
594        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
595        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
596        let denom = ii_a + ii_b;
597        if denom < 1e-10 { return; }
598        let lambda = -error / denom;
599        let prev = self.accum_motor;
600        let new_acc = (prev + lambda).clamp(-max_torque, max_torque);
601        let actual = new_acc - prev;
602        self.accum_motor = new_acc;
603        if let Some(b) = bodies.get_mut(&self.body_a) {
604            b.apply_angular_impulse(-actual);
605        }
606        if let Some(b) = bodies.get_mut(&self.body_b) {
607            b.apply_angular_impulse(actual);
608        }
609    }
610}
611
612impl Constraint for HingeConstraint {
613    fn prepare(&mut self, _bodies: &HashMap<BodyHandle, BodyState>, _dt: f32) {
614        // Warm-start: accumulators are kept from the previous frame
615    }
616
617    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
618        let (pa, pb) = self.world_anchors(bodies);
619        let ba = bodies.get(&self.body_a);
620        let bb = bodies.get(&self.body_b);
621        let va = ba.map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
622        let vb = bb.map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
623        (vb - va).length()
624    }
625
626    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
627        let (pa, pb) = self.world_anchors(bodies);
628        (pb - pa).length()
629    }
630
631    fn get_compliance(&self) -> f32 { self.compliance }
632
633    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
634        self.positional_effective_mass(bodies)
635    }
636
637    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
638        let (pa, pb) = self.world_anchors(bodies);
639        let delta = pb - pa;
640        let len = delta.length();
641        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
642        let impulse = dir * lambda;
643        if let Some(b) = bodies.get_mut(&self.body_a) {
644            b.apply_impulse(-impulse, pa - b.position);
645        }
646        if let Some(b) = bodies.get_mut(&self.body_b) {
647            b.apply_impulse(impulse, pb - b.position);
648        }
649    }
650
651    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
652        // Positional anchor constraint
653        let em = self.effective_mass(bodies);
654        if em > 1e-10 {
655            let cdot = self.compute_cdot(bodies);
656            let bias = self.bias(bodies, dt);
657            let delta = -(cdot + bias) * em;
658            let prev = self.accum_x;
659            let new_acc = prev + delta;
660            let actual = new_acc - prev;
661            self.accum_x = new_acc;
662            if actual.abs() > 1e-14 {
663                self.apply_impulse(bodies, actual);
664            }
665        }
666        // Limit and motor
667        self.solve_limit(bodies, dt);
668        self.solve_motor(bodies);
669    }
670
671    fn solve_position(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
672        self.solve_anchor(bodies, 0.2, dt);
673    }
674
675    fn accumulated_impulse(&self) -> f32 { self.accum_x }
676    fn reset_accumulated(&mut self) {
677        self.accum_x = 0.0; self.accum_y = 0.0;
678        self.accum_limit = 0.0; self.accum_motor = 0.0;
679    }
680    fn add_accumulated(&mut self, d: f32) { self.accum_x += d; }
681    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
682}
683
684// ── BallSocketConstraint ─────────────────────────────────────────────────────
685
686/// Ball-and-socket joint: 3-DOF rotation, fixed relative position.
687/// Optionally limits the cone angle between the two body Z-axes.
688#[derive(Debug, Clone)]
689pub struct BallSocketConstraint {
690    pub body_a:       BodyHandle,
691    pub body_b:       BodyHandle,
692    pub anchor_a:     Vec2,
693    pub anchor_b:     Vec2,
694    /// Max cone half-angle in radians (None = unlimited).
695    pub cone_limit:   Option<f32>,
696    pub compliance:   f32,
697    accumulated:      f32,
698    accum_cone:       f32,
699}
700
701impl BallSocketConstraint {
702    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2) -> Self {
703        Self { body_a, body_b, anchor_a, anchor_b, cone_limit: None, compliance: 0.0, accumulated: 0.0, accum_cone: 0.0 }
704    }
705
706    pub fn with_cone_limit(mut self, angle: f32) -> Self { self.cone_limit = Some(angle); self }
707    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
708
709    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
710        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
711        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
712        (pa, pb)
713    }
714
715    fn solve_cone_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
716        let Some(max_angle) = self.cone_limit else { return };
717        let angle_a = bodies.get(&self.body_a).map(|b| b.angle).unwrap_or(0.0);
718        let angle_b = bodies.get(&self.body_b).map(|b| b.angle).unwrap_or(0.0);
719        let rel = wrap_angle(angle_b - angle_a);
720        if rel.abs() <= max_angle { return; }
721        let violation = if rel > max_angle { rel - max_angle } else { rel + max_angle };
722        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
723        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
724        let denom = ii_a + ii_b;
725        if denom < 1e-10 { return; }
726        let lambda = -violation / denom;
727        let prev = self.accum_cone;
728        let new_acc = if rel > max_angle { (prev + lambda).min(0.0) } else { (prev + lambda).max(0.0) };
729        let actual = new_acc - prev;
730        self.accum_cone = new_acc;
731        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(-actual); }
732        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(actual); }
733    }
734}
735
736impl Constraint for BallSocketConstraint {
737    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
738        let (pa, pb) = self.world_anchors(bodies);
739        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
740        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
741        (vb - va).length()
742    }
743
744    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
745        let (pa, pb) = self.world_anchors(bodies);
746        (pb - pa).length()
747    }
748
749    fn get_compliance(&self) -> f32 { self.compliance }
750
751    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
752        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
753        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
754        let d = im_a + im_b;
755        if d < 1e-10 { 0.0 } else { 1.0 / d }
756    }
757
758    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
759        let (pa, pb) = self.world_anchors(bodies);
760        let delta = pb - pa;
761        let len = delta.length();
762        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
763        let impulse = dir * lambda;
764        if let Some(b) = bodies.get_mut(&self.body_a) {
765            b.apply_impulse(-impulse, pa - b.position);
766        }
767        if let Some(b) = bodies.get_mut(&self.body_b) {
768            b.apply_impulse(impulse, pb - b.position);
769        }
770    }
771
772    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
773        let em = self.effective_mass(bodies);
774        if em > 1e-10 {
775            let cdot = self.compute_cdot(bodies);
776            let bias = self.bias(bodies, dt);
777            let delta = -(cdot + bias) * em;
778            self.accumulated += delta;
779            self.apply_impulse(bodies, delta);
780        }
781        self.solve_cone_limit(bodies);
782    }
783
784    fn accumulated_impulse(&self) -> f32 { self.accumulated }
785    fn reset_accumulated(&mut self) { self.accumulated = 0.0; self.accum_cone = 0.0; }
786    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
787    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
788}
789
790// ── SliderConstraint ──────────────────────────────────────────────────────────
791
792/// Prismatic (slider) joint: allows translation along one axis only.
793/// Supports translation limits and a linear motor.
794#[derive(Debug, Clone)]
795pub struct SliderConstraint {
796    pub body_a:     BodyHandle,
797    pub body_b:     BodyHandle,
798    pub anchor_a:   Vec2,
799    pub anchor_b:   Vec2,
800    /// Slide axis in world space (unit vector).
801    pub axis:       Vec2,
802    /// Translation limits (min, max) in world units.
803    pub limits:     Option<(f32, f32)>,
804    /// Motor: (target_velocity, max_force).
805    pub motor:      Option<(f32, f32)>,
806    pub compliance: f32,
807    accum_perp:     f32,  // perpendicular constraint
808    accum_limit:    f32,
809    accum_motor:    f32,
810}
811
812impl SliderConstraint {
813    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, axis: Vec2) -> Self {
814        let axis = axis.normalize_or_zero();
815        Self {
816            body_a, body_b, anchor_a, anchor_b, axis,
817            limits: None, motor: None, compliance: 0.0,
818            accum_perp: 0.0, accum_limit: 0.0, accum_motor: 0.0,
819        }
820    }
821
822    pub fn with_limits(mut self, min: f32, max: f32) -> Self { self.limits = Some((min, max)); self }
823    pub fn with_motor(mut self, vel: f32, max_force: f32) -> Self { self.motor = Some((vel, max_force)); self }
824    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
825
826    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
827        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
828        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
829        (pa, pb)
830    }
831
832    fn slide_offset(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
833        let (pa, pb) = self.world_anchors(bodies);
834        (pb - pa).dot(self.axis)
835    }
836
837    fn perp_offset(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
838        let (pa, pb) = self.world_anchors(bodies);
839        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
840        (pb - pa).dot(perp_axis)
841    }
842
843    fn solve_perp(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, beta: f32, dt: f32) {
844        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
845        let c = self.perp_offset(bodies);
846        if c.abs() < 1e-8 { return; }
847        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
848        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
849        let (pa, pb) = self.world_anchors(bodies);
850        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
851        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
852        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
853        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
854        let rda = cross2(ra, perp_axis);
855        let rdb = cross2(rb, perp_axis);
856        let denom = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb;
857        if denom < 1e-10 { return; }
858        let lambda = -beta / dt * c / denom;
859        let impulse = perp_axis * lambda;
860        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
861        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
862    }
863
864    fn solve_translation_limit(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
865        let Some((min, max)) = self.limits else { return };
866        let offset = self.slide_offset(bodies);
867        let violation = if offset < min { offset - min } else if offset > max { offset - max } else { return };
868        let (pa, pb) = self.world_anchors(bodies);
869        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
870        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
871        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
872        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
873        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
874        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
875        let rda = cross2(ra, self.axis);
876        let rdb = cross2(rb, self.axis);
877        let denom = im_a + im_b + ii_a * rda * rda + ii_b * rdb * rdb + self.compliance / (dt * dt);
878        if denom < 1e-10 { return; }
879        let lambda = -violation / denom;
880        let prev = self.accum_limit;
881        let new_acc = if offset < min { (prev + lambda).max(0.0) } else { (prev + lambda).min(0.0) };
882        let actual = new_acc - prev;
883        self.accum_limit = new_acc;
884        let impulse = self.axis * actual;
885        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
886        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
887    }
888
889    fn solve_motor_constraint(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>) {
890        let Some((target_vel, max_force)) = self.motor else { return };
891        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
892        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
893        let rel_vel = (vb - va).dot(self.axis);
894        let error = rel_vel - target_vel;
895        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
896        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
897        let denom = im_a + im_b;
898        if denom < 1e-10 { return; }
899        let lambda = -error / denom;
900        let prev = self.accum_motor;
901        let new_acc = (prev + lambda).clamp(-max_force, max_force);
902        let actual = new_acc - prev;
903        self.accum_motor = new_acc;
904        let impulse = self.axis * actual;
905        let (pa, pb) = self.world_anchors(bodies);
906        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
907        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
908        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
909        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
910    }
911}
912
913impl Constraint for SliderConstraint {
914    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
915        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
916        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
917        (vb - va).dot(Vec2::new(-self.axis.y, self.axis.x))
918    }
919
920    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
921        self.perp_offset(bodies)
922    }
923
924    fn get_compliance(&self) -> f32 { self.compliance }
925
926    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
927        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
928        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
929        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
930        let d = im_a + im_b;
931        if d < 1e-10 { 0.0 } else { 1.0 / d }
932    }
933
934    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
935        let perp_axis = Vec2::new(-self.axis.y, self.axis.x);
936        let impulse = perp_axis * lambda;
937        let (pa, pb) = self.world_anchors(bodies);
938        let ra = bodies.get(&self.body_a).map(|b| pa - b.position).unwrap_or(Vec2::ZERO);
939        let rb = bodies.get(&self.body_b).map(|b| pb - b.position).unwrap_or(Vec2::ZERO);
940        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, ra); }
941        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse, rb); }
942    }
943
944    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
945        self.solve_perp(bodies, 0.1, dt);
946        self.solve_translation_limit(bodies, dt);
947        self.solve_motor_constraint(bodies);
948    }
949
950    fn solve_position(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
951        self.solve_perp(bodies, 0.2, dt);
952    }
953
954    fn accumulated_impulse(&self) -> f32 { self.accum_perp }
955    fn reset_accumulated(&mut self) { self.accum_perp = 0.0; self.accum_limit = 0.0; self.accum_motor = 0.0; }
956    fn add_accumulated(&mut self, d: f32) { self.accum_perp += d; }
957    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
958}
959
960// ── WeldConstraint ────────────────────────────────────────────────────────────
961
962/// Weld joint: fully rigid 6-DOF lock. Fixes both relative position and angle.
963#[derive(Debug, Clone)]
964pub struct WeldConstraint {
965    pub body_a:          BodyHandle,
966    pub body_b:          BodyHandle,
967    pub anchor_a:        Vec2,
968    pub anchor_b:        Vec2,
969    /// Reference relative angle (captured at creation time).
970    pub ref_angle:       f32,
971    /// Angular compliance (0 = perfectly rigid).
972    pub angular_compliance: f32,
973    /// Linear compliance.
974    pub linear_compliance: f32,
975    accum_lin:           f32,
976    accum_ang:           f32,
977}
978
979impl WeldConstraint {
980    pub fn new(body_a: BodyHandle, anchor_a: Vec2, body_b: BodyHandle, anchor_b: Vec2, ref_angle: f32) -> Self {
981        Self {
982            body_a, body_b, anchor_a, anchor_b, ref_angle,
983            angular_compliance: 0.0, linear_compliance: 0.0,
984            accum_lin: 0.0, accum_ang: 0.0,
985        }
986    }
987
988    pub fn with_angular_compliance(mut self, c: f32) -> Self { self.angular_compliance = c; self }
989    pub fn with_linear_compliance(mut self, c: f32) -> Self { self.linear_compliance = c; self }
990
991    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
992        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
993        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
994        (pa, pb)
995    }
996
997    fn angle_error(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
998        let angle_a = bodies.get(&self.body_a).map(|b| b.angle).unwrap_or(0.0);
999        let angle_b = bodies.get(&self.body_b).map(|b| b.angle).unwrap_or(0.0);
1000        wrap_angle(angle_b - angle_a - self.ref_angle)
1001    }
1002}
1003
1004impl Constraint for WeldConstraint {
1005    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1006        let (pa, pb) = self.world_anchors(bodies);
1007        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(pa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1008        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(pb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1009        (vb - va).length()
1010    }
1011
1012    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1013        let (pa, pb) = self.world_anchors(bodies);
1014        (pb - pa).length()
1015    }
1016
1017    fn get_compliance(&self) -> f32 { self.linear_compliance }
1018
1019    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1020        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1021        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1022        let d = im_a + im_b;
1023        if d < 1e-10 { 0.0 } else { 1.0 / d }
1024    }
1025
1026    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1027        let (pa, pb) = self.world_anchors(bodies);
1028        let delta = pb - pa;
1029        let len = delta.length();
1030        let dir = if len > 1e-7 { delta / len } else { Vec2::X };
1031        let impulse = dir * lambda;
1032        if let Some(b) = bodies.get_mut(&self.body_a) {
1033            b.apply_impulse(-impulse, pa - b.position);
1034        }
1035        if let Some(b) = bodies.get_mut(&self.body_b) {
1036            b.apply_impulse(impulse, pb - b.position);
1037        }
1038    }
1039
1040    fn solve_velocity(&mut self, bodies: &mut HashMap<BodyHandle, BodyState>, dt: f32) {
1041        // Linear
1042        let em_lin = self.effective_mass(bodies);
1043        if em_lin > 1e-10 {
1044            let cdot = self.compute_cdot(bodies);
1045            let bias = self.bias(bodies, dt);
1046            let delta = -(cdot + bias) * em_lin;
1047            self.accum_lin += delta;
1048            self.apply_impulse(bodies, delta);
1049        }
1050        // Angular
1051        let err = self.angle_error(bodies);
1052        let omega_a = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
1053        let omega_b = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
1054        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1055        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1056        let denom = ii_a + ii_b + self.angular_compliance / (dt * dt);
1057        if denom > 1e-10 {
1058            let ang_bias = -0.1 / dt * err;
1059            let rel_omega = omega_b - omega_a;
1060            let lambda = -(rel_omega + ang_bias) / denom;
1061            self.accum_ang += lambda;
1062            if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(-lambda); }
1063            if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(lambda); }
1064        }
1065    }
1066
1067    fn accumulated_impulse(&self) -> f32 { self.accum_lin }
1068    fn reset_accumulated(&mut self) { self.accum_lin = 0.0; self.accum_ang = 0.0; }
1069    fn add_accumulated(&mut self, d: f32) { self.accum_lin += d; }
1070    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1071}
1072
1073// ── PulleyConstraint ──────────────────────────────────────────────────────────
1074
1075/// Pulley constraint: body_a and body_b are connected via a rope over two
1076/// fixed pulley points. Maintains: len_a + ratio * len_b = constant.
1077#[derive(Debug, Clone)]
1078pub struct PulleyConstraint {
1079    pub body_a:       BodyHandle,
1080    pub body_b:       BodyHandle,
1081    pub anchor_a:     Vec2,      // local anchor on body A
1082    pub anchor_b:     Vec2,      // local anchor on body B
1083    pub pulley_a:     Vec2,      // fixed world-space pulley point for A
1084    pub pulley_b:     Vec2,      // fixed world-space pulley point for B
1085    pub ratio:        f32,       // pulley ratio
1086    pub total_length: f32,       // len_a + ratio * len_b at rest
1087    accumulated:      f32,
1088}
1089
1090impl PulleyConstraint {
1091    pub fn new(
1092        body_a: BodyHandle, anchor_a: Vec2, pulley_a: Vec2,
1093        body_b: BodyHandle, anchor_b: Vec2, pulley_b: Vec2,
1094        ratio: f32, total_length: f32,
1095    ) -> Self {
1096        Self { body_a, body_b, anchor_a, anchor_b, pulley_a, pulley_b, ratio, total_length, accumulated: 0.0 }
1097    }
1098
1099    fn world_anchor_a(&self, bodies: &HashMap<BodyHandle, BodyState>) -> Vec2 {
1100        bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO)
1101    }
1102    fn world_anchor_b(&self, bodies: &HashMap<BodyHandle, BodyState>) -> Vec2 {
1103        bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO)
1104    }
1105
1106    fn lengths(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (f32, f32) {
1107        let wa = self.world_anchor_a(bodies);
1108        let wb = self.world_anchor_b(bodies);
1109        let la = (wa - self.pulley_a).length();
1110        let lb = (wb - self.pulley_b).length();
1111        (la, lb)
1112    }
1113
1114    fn dirs(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
1115        let wa = self.world_anchor_a(bodies);
1116        let wb = self.world_anchor_b(bodies);
1117        let da = (wa - self.pulley_a);
1118        let db = (wb - self.pulley_b);
1119        let la = da.length();
1120        let lb = db.length();
1121        (
1122            if la > 1e-7 { da / la } else { Vec2::X },
1123            if lb > 1e-7 { db / lb } else { Vec2::X },
1124        )
1125    }
1126}
1127
1128impl Constraint for PulleyConstraint {
1129    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1130        let (la, lb) = self.lengths(bodies);
1131        la + self.ratio * lb - self.total_length
1132    }
1133
1134    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1135        let wa = self.world_anchor_a(bodies);
1136        let wb = self.world_anchor_b(bodies);
1137        let (da, db) = self.dirs(bodies);
1138        let va = bodies.get(&self.body_a).map(|b| b.velocity + perp(wa - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1139        let vb = bodies.get(&self.body_b).map(|b| b.velocity + perp(wb - b.position) * b.angular_velocity).unwrap_or(Vec2::ZERO);
1140        va.dot(da) + self.ratio * vb.dot(db)
1141    }
1142
1143    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1144        let wa = self.world_anchor_a(bodies);
1145        let wb = self.world_anchor_b(bodies);
1146        let (da, db) = self.dirs(bodies);
1147        let ra = bodies.get(&self.body_a).map(|b| wa - b.position).unwrap_or(Vec2::ZERO);
1148        let rb = bodies.get(&self.body_b).map(|b| wb - b.position).unwrap_or(Vec2::ZERO);
1149        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1150        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1151        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1152        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1153        let rda = cross2(ra, da);
1154        let rdb = cross2(rb, db);
1155        let denom = im_a + ii_a * rda * rda + self.ratio * self.ratio * (im_b + ii_b * rdb * rdb);
1156        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1157    }
1158
1159    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1160        let wa = self.world_anchor_a(bodies);
1161        let wb = self.world_anchor_b(bodies);
1162        let (da, db) = self.dirs(bodies);
1163        let ra = bodies.get(&self.body_a).map(|b| wa - b.position).unwrap_or(Vec2::ZERO);
1164        let rb = bodies.get(&self.body_b).map(|b| wb - b.position).unwrap_or(Vec2::ZERO);
1165        if let Some(b) = bodies.get_mut(&self.body_a) {
1166            b.apply_impulse(-da * lambda, ra);
1167        }
1168        if let Some(b) = bodies.get_mut(&self.body_b) {
1169            b.apply_impulse(-db * (lambda * self.ratio), rb);
1170        }
1171    }
1172
1173    fn impulse_bounds(&self) -> (f32, f32) { (0.0, f32::INFINITY) }
1174    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1175    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1176    fn add_accumulated(&mut self, d: f32) { self.accumulated = (self.accumulated + d).max(0.0); }
1177    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1178}
1179
1180// ── SpringConstraint ──────────────────────────────────────────────────────────
1181
1182/// A spring-damper between two anchor points (Hooke's law).
1183#[derive(Debug, Clone)]
1184pub struct SpringConstraint {
1185    pub body_a:      BodyHandle,
1186    pub body_b:      BodyHandle,
1187    pub anchor_a:    Vec2,
1188    pub anchor_b:    Vec2,
1189    pub rest_length: f32,
1190    pub stiffness:   f32,
1191    pub damping:     f32,
1192    accumulated:     f32,
1193}
1194
1195impl SpringConstraint {
1196    pub fn new(a: BodyHandle, aa: Vec2, b: BodyHandle, ab: Vec2, rest: f32, k: f32, d: f32) -> Self {
1197        Self { body_a: a, body_b: b, anchor_a: aa, anchor_b: ab, rest_length: rest, stiffness: k, damping: d, accumulated: 0.0 }
1198    }
1199
1200    fn world_anchors(&self, bodies: &HashMap<BodyHandle, BodyState>) -> (Vec2, Vec2) {
1201        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1202        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1203        (pa, pb)
1204    }
1205}
1206
1207impl Constraint for SpringConstraint {
1208    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1209        let (pa, pb) = self.world_anchors(bodies);
1210        (pb - pa).length() - self.rest_length
1211    }
1212
1213    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1214        let (pa, pb) = self.world_anchors(bodies);
1215        let delta = pb - pa;
1216        let len = delta.length().max(1e-7);
1217        let dir = delta / len;
1218        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1219        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1220        (vb - va).dot(dir)
1221    }
1222
1223    fn get_compliance(&self) -> f32 { if self.stiffness > 1e-6 { 1.0 / self.stiffness } else { 0.0 } }
1224
1225    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1226        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1227        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1228        let denom = im_a + im_b + self.get_compliance();
1229        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1230    }
1231
1232    fn bias(&self, bodies: &HashMap<BodyHandle, BodyState>, dt: f32) -> f32 {
1233        -self.stiffness * self.compute_c(bodies) / dt
1234    }
1235
1236    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1237        let (pa, pb) = self.world_anchors(bodies);
1238        let delta = pb - pa;
1239        let len = delta.length().max(1e-7);
1240        let dir = delta / len;
1241        let impulse = dir * lambda;
1242        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-impulse, Vec2::ZERO); }
1243        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(impulse,  Vec2::ZERO); }
1244    }
1245
1246    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1247    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1248    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1249    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1250}
1251
1252// ── PinConstraint ─────────────────────────────────────────────────────────────
1253
1254/// Pin a body's local anchor to a fixed world-space point.
1255#[derive(Debug, Clone)]
1256pub struct PinConstraint {
1257    pub body:         BodyHandle,
1258    pub local_anchor: Vec2,
1259    pub world_target: Vec2,
1260    pub compliance:   f32,
1261    accumulated:      f32,
1262}
1263
1264impl PinConstraint {
1265    pub fn new(body: BodyHandle, local_anchor: Vec2, world_target: Vec2) -> Self {
1266        Self { body, local_anchor, world_target, compliance: 0.0, accumulated: 0.0 }
1267    }
1268    pub fn with_compliance(mut self, c: f32) -> Self { self.compliance = c; self }
1269}
1270
1271impl Constraint for PinConstraint {
1272    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1273        let pa = bodies.get(&self.body)
1274            .map(|b| b.position + rotate2(self.local_anchor, b.angle))
1275            .unwrap_or(Vec2::ZERO);
1276        (pa - self.world_target).length()
1277    }
1278
1279    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1280        let b = match bodies.get(&self.body) { Some(b) => b, None => return 0.0 };
1281        let pa = b.position + rotate2(self.local_anchor, b.angle);
1282        let dir = (pa - self.world_target).normalize_or_zero();
1283        (b.velocity + perp(pa - b.position) * b.angular_velocity).dot(dir)
1284    }
1285
1286    fn get_compliance(&self) -> f32 { self.compliance }
1287
1288    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1289        let b = match bodies.get(&self.body) { Some(b) => b, None => return 0.0 };
1290        let pa = b.position + rotate2(self.local_anchor, b.angle);
1291        let r = pa - b.position;
1292        let dir = (pa - self.world_target).normalize_or_zero();
1293        let rd = cross2(r, dir);
1294        let denom = b.inv_mass + b.inv_inertia * rd * rd;
1295        if denom < 1e-10 { 0.0 } else { 1.0 / denom }
1296    }
1297
1298    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1299        let b = match bodies.get_mut(&self.body) { Some(b) => b, None => return };
1300        let pa = b.position + rotate2(self.local_anchor, b.angle);
1301        let dir = (pa - self.world_target).normalize_or_zero();
1302        b.apply_impulse(-dir * lambda, pa - b.position);
1303    }
1304
1305    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1306    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1307    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1308    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body] }
1309}
1310
1311// ── MotorConstraint ───────────────────────────────────────────────────────────
1312
1313/// Drive a body at a target angular velocity. Acts as a torque motor.
1314#[derive(Debug, Clone)]
1315pub struct MotorConstraint {
1316    pub body:            BodyHandle,
1317    pub target_velocity: f32,
1318    pub max_torque:      f32,
1319    accumulated:         f32,
1320}
1321
1322impl MotorConstraint {
1323    pub fn new(body: BodyHandle, target_velocity: f32, max_torque: f32) -> Self {
1324        Self { body, target_velocity, max_torque, accumulated: 0.0 }
1325    }
1326}
1327
1328impl Constraint for MotorConstraint {
1329    fn compute_c(&self, _: &HashMap<BodyHandle, BodyState>) -> f32 { 0.0 }
1330
1331    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1332        bodies.get(&self.body).map(|b| b.angular_velocity).unwrap_or(0.0) - self.target_velocity
1333    }
1334
1335    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1336        let ii = bodies.get(&self.body).map(|b| b.inv_inertia).unwrap_or(0.0);
1337        if ii < 1e-10 { 0.0 } else { 1.0 / ii }
1338    }
1339
1340    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1341        if let Some(b) = bodies.get_mut(&self.body) {
1342            b.angular_velocity -= lambda * b.inv_inertia;
1343        }
1344    }
1345
1346    fn impulse_bounds(&self) -> (f32, f32) { (-self.max_torque, self.max_torque) }
1347    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1348    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1349    fn add_accumulated(&mut self, d: f32) {
1350        self.accumulated = (self.accumulated + d).clamp(-self.max_torque, self.max_torque);
1351    }
1352    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body] }
1353}
1354
1355// ── GearConstraint ────────────────────────────────────────────────────────────
1356
1357/// Couple the angular velocities of two bodies: omega_a + ratio * omega_b = 0.
1358#[derive(Debug, Clone)]
1359pub struct GearConstraint {
1360    pub body_a:  BodyHandle,
1361    pub body_b:  BodyHandle,
1362    pub ratio:   f32,
1363    accumulated: f32,
1364}
1365
1366impl GearConstraint {
1367    pub fn new(body_a: BodyHandle, body_b: BodyHandle, ratio: f32) -> Self {
1368        Self { body_a, body_b, ratio, accumulated: 0.0 }
1369    }
1370}
1371
1372impl Constraint for GearConstraint {
1373    fn compute_c(&self, _: &HashMap<BodyHandle, BodyState>) -> f32 { 0.0 }
1374
1375    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1376        let oa = bodies.get(&self.body_a).map(|b| b.angular_velocity).unwrap_or(0.0);
1377        let ob = bodies.get(&self.body_b).map(|b| b.angular_velocity).unwrap_or(0.0);
1378        oa + self.ratio * ob
1379    }
1380
1381    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1382        let ii_a = bodies.get(&self.body_a).map(|b| b.inv_inertia).unwrap_or(0.0);
1383        let ii_b = bodies.get(&self.body_b).map(|b| b.inv_inertia).unwrap_or(0.0);
1384        let d = ii_a + self.ratio * self.ratio * ii_b;
1385        if d < 1e-10 { 0.0 } else { 1.0 / d }
1386    }
1387
1388    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1389        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_angular_impulse(-lambda); }
1390        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_angular_impulse(-lambda * self.ratio); }
1391    }
1392
1393    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1394    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1395    fn add_accumulated(&mut self, d: f32) { self.accumulated += d; }
1396    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1397}
1398
1399// ── MaxDistanceConstraint ─────────────────────────────────────────────────────
1400
1401/// Soft rope: only constrains when distance exceeds max_distance.
1402#[derive(Debug, Clone)]
1403pub struct MaxDistanceConstraint {
1404    pub body_a:       BodyHandle,
1405    pub body_b:       BodyHandle,
1406    pub anchor_a:     Vec2,
1407    pub anchor_b:     Vec2,
1408    pub max_distance: f32,
1409    pub compliance:   f32,
1410    accumulated:      f32,
1411}
1412
1413impl MaxDistanceConstraint {
1414    pub fn new(a: BodyHandle, aa: Vec2, b: BodyHandle, ab: Vec2, max_dist: f32) -> Self {
1415        Self { body_a: a, body_b: b, anchor_a: aa, anchor_b: ab, max_distance: max_dist, compliance: 0.0, accumulated: 0.0 }
1416    }
1417
1418    fn current_length(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1419        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1420        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1421        (pb - pa).length()
1422    }
1423}
1424
1425impl Constraint for MaxDistanceConstraint {
1426    fn compute_c(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1427        (self.current_length(bodies) - self.max_distance).max(0.0)
1428    }
1429
1430    fn compute_cdot(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1431        if self.compute_c(bodies) <= 0.0 { return 0.0; }
1432        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1433        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1434        let delta = pb - pa;
1435        let len = delta.length().max(1e-7);
1436        let dir = delta / len;
1437        let va = bodies.get(&self.body_a).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1438        let vb = bodies.get(&self.body_b).map(|b| b.velocity).unwrap_or(Vec2::ZERO);
1439        (vb - va).dot(dir)
1440    }
1441
1442    fn get_compliance(&self) -> f32 { self.compliance }
1443
1444    fn effective_mass(&self, bodies: &HashMap<BodyHandle, BodyState>) -> f32 {
1445        let im_a = bodies.get(&self.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
1446        let im_b = bodies.get(&self.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
1447        let d = im_a + im_b;
1448        if d < 1e-10 { 0.0 } else { 1.0 / d }
1449    }
1450
1451    fn apply_impulse(&self, bodies: &mut HashMap<BodyHandle, BodyState>, lambda: f32) {
1452        let pa = bodies.get(&self.body_a).map(|b| b.position + rotate2(self.anchor_a, b.angle)).unwrap_or(Vec2::ZERO);
1453        let pb = bodies.get(&self.body_b).map(|b| b.position + rotate2(self.anchor_b, b.angle)).unwrap_or(Vec2::ZERO);
1454        let delta = pb - pa;
1455        let len = delta.length().max(1e-7);
1456        let dir = delta / len;
1457        let imp = dir * lambda;
1458        if let Some(b) = bodies.get_mut(&self.body_a) { b.apply_impulse(-imp, Vec2::ZERO); }
1459        if let Some(b) = bodies.get_mut(&self.body_b) { b.apply_impulse(imp,  Vec2::ZERO); }
1460    }
1461
1462    fn impulse_bounds(&self) -> (f32, f32) { (0.0, f32::INFINITY) }
1463    fn accumulated_impulse(&self) -> f32 { self.accumulated }
1464    fn reset_accumulated(&mut self) { self.accumulated = 0.0; }
1465    fn add_accumulated(&mut self, d: f32) { self.accumulated = (self.accumulated + d).max(0.0); }
1466    fn body_handles(&self) -> Vec<BodyHandle> { vec![self.body_a, self.body_b] }
1467}
1468
1469// ── ConstraintSolver ──────────────────────────────────────────────────────────
1470
1471/// Sequential-impulse + XPBD constraint solver.
1472///
1473/// Call `solve()` once per physics step. Internally iterates
1474/// `iteration_count` times applying velocity and/or position corrections.
1475pub struct ConstraintSolver {
1476    pub iteration_count:  u32,
1477    pub baumgarte_factor: f32,
1478    pub warm_start:       bool,
1479    /// Number of XPBD substeps (for solve_xpbd).
1480    pub substep_count:    u32,
1481}
1482
1483impl Default for ConstraintSolver {
1484    fn default() -> Self {
1485        Self { iteration_count: 10, baumgarte_factor: 0.2, warm_start: true, substep_count: 4 }
1486    }
1487}
1488
1489impl ConstraintSolver {
1490    pub fn new(iterations: u32) -> Self {
1491        Self { iteration_count: iterations, ..Default::default() }
1492    }
1493
1494    /// Solve all constraints using sequential impulses (velocity level).
1495    pub fn solve(
1496        &self,
1497        bodies:      &mut HashMap<BodyHandle, BodyState>,
1498        constraints: &mut [Box<dyn Constraint>],
1499        dt:          f32,
1500    ) {
1501        if dt < 1e-8 { return; }
1502
1503        // Warm starting: apply accumulated impulses from last frame
1504        if self.warm_start {
1505            for c in constraints.iter() {
1506                let lambda = c.accumulated_impulse();
1507                if lambda.abs() > 1e-10 {
1508                    c.apply_impulse(bodies, lambda * 0.8); // scale down slightly
1509                }
1510            }
1511        } else {
1512            for c in constraints.iter_mut() { c.reset_accumulated(); }
1513        }
1514
1515        // Prepare constraints (compute cached values, apply warm start)
1516        for c in constraints.iter_mut() {
1517            c.prepare(bodies, dt);
1518        }
1519
1520        // Iterative velocity solve
1521        for _ in 0..self.iteration_count {
1522            for c in constraints.iter_mut() {
1523                c.solve_velocity(bodies, dt);
1524            }
1525        }
1526    }
1527
1528    /// Apply position correction (post-solve) using Baumgarte stabilization.
1529    pub fn solve_positions(
1530        &self,
1531        bodies:      &mut HashMap<BodyHandle, BodyState>,
1532        constraints: &mut [Box<dyn Constraint>],
1533        dt:          f32,
1534    ) {
1535        let alpha = self.baumgarte_factor / dt.max(1e-6);
1536        for c in constraints.iter_mut() {
1537            let em = c.effective_mass(bodies);
1538            if em < 1e-10 { continue; }
1539            let c_val = c.compute_c(bodies);
1540            if c_val.abs() < 1e-5 { continue; }
1541            let lambda = -alpha * c_val * em;
1542            c.apply_impulse(bodies, lambda);
1543        }
1544    }
1545
1546    /// Solve using XPBD with substep integration.
1547    pub fn solve_xpbd(
1548        &self,
1549        bodies:      &mut HashMap<BodyHandle, BodyState>,
1550        constraints: &mut [Box<dyn Constraint>],
1551        dt:          f32,
1552    ) {
1553        if dt < 1e-8 { return; }
1554        let sub_dt = dt / self.substep_count as f32;
1555
1556        for _ in 0..self.substep_count {
1557            // Reset accumulated lambdas for XPBD
1558            for c in constraints.iter_mut() { c.reset_accumulated(); }
1559
1560            // Integrate velocities
1561            for body in bodies.values_mut() {
1562                if !body.is_static() {
1563                    body.position += body.velocity * sub_dt;
1564                    body.angle    += body.angular_velocity * sub_dt;
1565                }
1566            }
1567
1568            // Position-level constraint solve
1569            for _ in 0..self.iteration_count {
1570                for c in constraints.iter_mut() {
1571                    c.solve_position(bodies, sub_dt);
1572                }
1573            }
1574
1575            // Derive velocities from position corrections
1576            // (velocities are updated by apply_impulse inside solve_position)
1577        }
1578    }
1579}
1580
1581// ── Constraint Island Detection ───────────────────────────────────────────────
1582
1583/// A group of bodies and constraints that are connected and should be solved together.
1584#[derive(Debug, Clone)]
1585pub struct ConstraintIsland {
1586    pub body_handles:      Vec<BodyHandle>,
1587    pub constraint_indices: Vec<usize>,
1588}
1589
1590/// Detect constraint islands using union-find.
1591pub fn detect_islands(
1592    bodies: &HashMap<BodyHandle, BodyState>,
1593    constraints: &[Box<dyn Constraint>],
1594) -> Vec<ConstraintIsland> {
1595    let body_list: Vec<BodyHandle> = bodies.keys().copied().collect();
1596    let n = body_list.len();
1597    if n == 0 { return Vec::new(); }
1598
1599    // Map handle -> index
1600    let mut handle_to_idx: HashMap<BodyHandle, usize> = HashMap::new();
1601    for (i, h) in body_list.iter().enumerate() {
1602        handle_to_idx.insert(*h, i);
1603    }
1604
1605    // Union-Find
1606    let mut parent: Vec<usize> = (0..n).collect();
1607    let mut rank: Vec<usize> = vec![0; n];
1608
1609    fn find(parent: &mut Vec<usize>, x: usize) -> usize {
1610        if parent[x] != x { parent[x] = find(parent, parent[x]); }
1611        parent[x]
1612    }
1613
1614    fn union(parent: &mut Vec<usize>, rank: &mut Vec<usize>, x: usize, y: usize) {
1615        let rx = find(parent, x);
1616        let ry = find(parent, y);
1617        if rx == ry { return; }
1618        if rank[rx] < rank[ry] { parent[rx] = ry; }
1619        else if rank[rx] > rank[ry] { parent[ry] = rx; }
1620        else { parent[ry] = rx; rank[rx] += 1; }
1621    }
1622
1623    // Union bodies connected by constraints
1624    for c in constraints.iter() {
1625        let handles = c.body_handles();
1626        if handles.len() < 2 { continue; }
1627        for i in 1..handles.len() {
1628            if let (Some(&ia), Some(&ib)) = (handle_to_idx.get(&handles[0]), handle_to_idx.get(&handles[i])) {
1629                union(&mut parent, &mut rank, ia, ib);
1630            }
1631        }
1632    }
1633
1634    // Group by root
1635    let mut island_map: HashMap<usize, usize> = HashMap::new();
1636    let mut islands: Vec<ConstraintIsland> = Vec::new();
1637
1638    for (i, h) in body_list.iter().enumerate() {
1639        let root = find(&mut parent, i);
1640        let island_idx = *island_map.entry(root).or_insert_with(|| {
1641            let idx = islands.len();
1642            islands.push(ConstraintIsland { body_handles: Vec::new(), constraint_indices: Vec::new() });
1643            idx
1644        });
1645        islands[island_idx].body_handles.push(*h);
1646    }
1647
1648    // Assign constraints to islands
1649    for (ci, c) in constraints.iter().enumerate() {
1650        let handles = c.body_handles();
1651        if handles.is_empty() { continue; }
1652        let first = handles[0];
1653        if let Some(&idx) = handle_to_idx.get(&first) {
1654            let root = find(&mut parent, idx);
1655            if let Some(&island_idx) = island_map.get(&root) {
1656                islands[island_idx].constraint_indices.push(ci);
1657            }
1658        }
1659    }
1660
1661    islands
1662}
1663
1664// ── ConstraintWorld ───────────────────────────────────────────────────────────
1665
1666/// Manages all bodies and constraints in one place.
1667pub struct ConstraintWorld {
1668    pub bodies:      HashMap<BodyHandle, BodyState>,
1669    pub constraints: Vec<Box<dyn Constraint>>,
1670    pub solver:      ConstraintSolver,
1671    pub gravity:     Vec2,
1672    next_body_id:    u32,
1673    next_con_id:     u32,
1674    /// Whether to use island-based solving.
1675    pub use_islands: bool,
1676    /// Whether to use XPBD substep integration.
1677    pub use_xpbd:    bool,
1678}
1679
1680impl ConstraintWorld {
1681    pub fn new() -> Self {
1682        Self {
1683            bodies:        HashMap::new(),
1684            constraints:   Vec::new(),
1685            solver:        ConstraintSolver::default(),
1686            gravity:       Vec2::new(0.0, -9.81),
1687            next_body_id:  1,
1688            next_con_id:   1,
1689            use_islands:   false,
1690            use_xpbd:      false,
1691        }
1692    }
1693
1694    pub fn add_body(&mut self, state: BodyState) -> BodyHandle {
1695        let id = BodyHandle(self.next_body_id);
1696        self.next_body_id += 1;
1697        self.bodies.insert(id, state);
1698        id
1699    }
1700
1701    pub fn add_constraint(&mut self, c: Box<dyn Constraint>) -> ConstraintId {
1702        let id = ConstraintId(self.next_con_id);
1703        self.next_con_id += 1;
1704        self.constraints.push(c);
1705        id
1706    }
1707
1708    pub fn remove_body(&mut self, handle: BodyHandle) {
1709        self.bodies.remove(&handle);
1710    }
1711
1712    pub fn step(&mut self, dt: f32) {
1713        if dt < 1e-8 { return; }
1714
1715        // Apply gravity
1716        for body in self.bodies.values_mut() {
1717            if !body.is_static() {
1718                body.velocity += self.gravity * dt;
1719            }
1720        }
1721
1722        if self.use_xpbd {
1723            // XPBD path: integrate then position-solve
1724            for body in self.bodies.values_mut() {
1725                if !body.is_static() {
1726                    body.position += body.velocity * dt;
1727                    body.angle    += body.angular_velocity * dt;
1728                }
1729            }
1730            self.solver.solve_xpbd(&mut self.bodies, &mut self.constraints, dt);
1731        } else {
1732            // SI path
1733            // Integrate velocities to positions
1734            for body in self.bodies.values_mut() {
1735                if !body.is_static() {
1736                    body.position += body.velocity * dt;
1737                    body.angle    += body.angular_velocity * dt;
1738                }
1739            }
1740
1741            self.solver.solve(&mut self.bodies, &mut self.constraints, dt);
1742            self.solver.solve_positions(&mut self.bodies, &mut self.constraints, dt);
1743        }
1744    }
1745
1746    pub fn body(&self, h: BodyHandle) -> Option<&BodyState> { self.bodies.get(&h) }
1747    pub fn body_mut(&mut self, h: BodyHandle) -> Option<&mut BodyState> { self.bodies.get_mut(&h) }
1748
1749    pub fn body_count(&self) -> usize { self.bodies.len() }
1750    pub fn constraint_count(&self) -> usize { self.constraints.len() }
1751
1752    /// Compute total kinetic energy.
1753    pub fn total_kinetic_energy(&self) -> f32 {
1754        self.bodies.values().map(|b| b.kinetic_energy()).sum()
1755    }
1756
1757    /// Remove all sleeping bodies from the simulation.
1758    pub fn remove_sleeping(&mut self, threshold: f32) {
1759        let sleeping: Vec<BodyHandle> = self.bodies.iter()
1760            .filter(|(_, b)| b.kinetic_energy() < threshold)
1761            .map(|(h, _)| *h)
1762            .collect();
1763        for h in sleeping { self.bodies.remove(&h); }
1764    }
1765}
1766
1767impl Default for ConstraintWorld {
1768    fn default() -> Self { Self::new() }
1769}
1770
1771// ── Tests ─────────────────────────────────────────────────────────────────────
1772
1773#[cfg(test)]
1774mod tests {
1775    use super::*;
1776    use std::f32::consts::PI;
1777
1778    fn make_two_bodies(world: &mut ConstraintWorld, mass: f32, pos_a: Vec2, pos_b: Vec2) -> (BodyHandle, BodyHandle) {
1779        let a = world.add_body(BodyState::new(pos_a, mass, mass * 0.1));
1780        let b = world.add_body(BodyState::new(pos_b, mass, mass * 0.1));
1781        (a, b)
1782    }
1783
1784    #[test]
1785    fn distance_constraint_rigid_rod() {
1786        let mut world = ConstraintWorld::new();
1787        world.gravity = Vec2::ZERO;
1788        let (a, b) = make_two_bodies(&mut world, 1.0, Vec2::ZERO, Vec2::new(3.0, 0.0));
1789        let c = Box::new(DistanceConstraint::rigid_rod(a, Vec2::ZERO, b, Vec2::ZERO, 2.0));
1790        world.add_constraint(c);
1791        for _ in 0..60 {
1792            world.step(1.0 / 60.0);
1793        }
1794        let pa = world.body(a).unwrap().position;
1795        let pb = world.body(b).unwrap().position;
1796        let dist = (pb - pa).length();
1797        assert!((dist - 2.0).abs() < 0.1, "rod should maintain 2m distance, got {dist}");
1798    }
1799
1800    #[test]
1801    fn distance_constraint_soft_spring() {
1802        let mut world = ConstraintWorld::new();
1803        world.gravity = Vec2::ZERO;
1804        let (a, b) = make_two_bodies(&mut world, 1.0, Vec2::ZERO, Vec2::new(5.0, 0.0));
1805        let c = Box::new(DistanceConstraint::soft(a, Vec2::ZERO, b, Vec2::ZERO, 2.0, 0.01));
1806        world.add_constraint(c);
1807        world.step(1.0 / 60.0);
1808        // Compliance means it gives a bit — just ensure it ran without panic
1809        let pb = world.body(b).unwrap().position;
1810        assert!(pb.x.is_finite());
1811    }
1812
1813    #[test]
1814    fn hinge_constraint_limits() {
1815        let mut world = ConstraintWorld::new();
1816        world.gravity = Vec2::ZERO;
1817        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1818        let b = world.add_body({
1819            let mut s = BodyState::new(Vec2::new(0.0, 1.0), 1.0, 0.1);
1820            s.angular_velocity = 10.0;
1821            s
1822        });
1823        let c = Box::new(
1824            HingeConstraint::new(a, Vec2::ZERO, b, Vec2::new(0.0, -1.0))
1825                .with_limits(-PI / 4.0, PI / 4.0)
1826        );
1827        world.add_constraint(c);
1828        for _ in 0..120 {
1829            world.step(1.0 / 60.0);
1830        }
1831        let angle = world.body(b).unwrap().angle;
1832        assert!(angle.abs() <= PI / 4.0 + 0.1, "angle {} should be within limits", angle);
1833    }
1834
1835    #[test]
1836    fn hinge_constraint_motor() {
1837        let mut world = ConstraintWorld::new();
1838        world.gravity = Vec2::ZERO;
1839        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1840        let b = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 1.0));
1841        let c = Box::new(
1842            HingeConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO)
1843                .with_motor(2.0 * PI, 100.0)
1844        );
1845        world.add_constraint(c);
1846        for _ in 0..60 {
1847            world.step(1.0 / 60.0);
1848        }
1849        let omega = world.body(b).unwrap().angular_velocity;
1850        assert!(omega > 0.0, "motor should drive positive angular velocity, got {omega}");
1851    }
1852
1853    #[test]
1854    fn ball_socket_cone_limit() {
1855        let mut world = ConstraintWorld::new();
1856        world.gravity = Vec2::ZERO;
1857        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1858        let b = world.add_body({
1859            let mut s = BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1);
1860            s.angular_velocity = 5.0;
1861            s
1862        });
1863        let c = Box::new(
1864            BallSocketConstraint::new(a, Vec2::ZERO, b, Vec2::new(-1.0, 0.0))
1865                .with_cone_limit(PI / 6.0)
1866        );
1867        world.add_constraint(c);
1868        for _ in 0..60 {
1869            world.step(1.0 / 60.0);
1870        }
1871        let angle = world.body(b).unwrap().angle;
1872        assert!(angle.abs() <= PI / 6.0 + 0.2, "cone angle exceeded");
1873    }
1874
1875    #[test]
1876    fn slider_constraint_limits() {
1877        let mut world = ConstraintWorld::new();
1878        world.gravity = Vec2::ZERO;
1879        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1880        let b = world.add_body({
1881            let mut s = BodyState::new(Vec2::new(0.5, 0.0), 1.0, 0.1);
1882            s.velocity = Vec2::new(5.0, 0.0);
1883            s
1884        });
1885        let c = Box::new(
1886            SliderConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, Vec2::X)
1887                .with_limits(-1.0, 1.0)
1888        );
1889        world.add_constraint(c);
1890        for _ in 0..120 {
1891            world.step(1.0 / 60.0);
1892        }
1893        let pos_x = world.body(b).unwrap().position.x;
1894        assert!(pos_x <= 1.5, "slider should be limited, got x={pos_x}");
1895    }
1896
1897    #[test]
1898    fn weld_constraint_holds() {
1899        let mut world = ConstraintWorld::new();
1900        world.gravity = Vec2::new(0.0, -9.81);
1901        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1902        let b = world.add_body(BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1));
1903        let c = Box::new(WeldConstraint::new(a, Vec2::new(1.0, 0.0), b, Vec2::ZERO, 0.0));
1904        world.add_constraint(c);
1905        for _ in 0..60 {
1906            world.step(1.0 / 60.0);
1907        }
1908        let pos = world.body(b).unwrap().position;
1909        assert!((pos - Vec2::new(1.0, 0.0)).length() < 0.5, "weld should hold, drift={}", (pos - Vec2::new(1.0, 0.0)).length());
1910    }
1911
1912    #[test]
1913    fn pulley_constraint_ratio() {
1914        let mut world = ConstraintWorld::new();
1915        world.gravity = Vec2::ZERO;
1916        let a = world.add_body(BodyState::new(Vec2::new(-3.0, 0.0), 1.0, 0.1));
1917        let b = world.add_body(BodyState::new(Vec2::new( 3.0, 0.0), 1.0, 0.1));
1918        let init_la = (Vec2::new(-3.0, 0.0) - Vec2::new(-1.0, 2.0)).length();
1919        let init_lb = (Vec2::new( 3.0, 0.0) - Vec2::new( 1.0, 2.0)).length();
1920        let total = init_la + init_lb;
1921        let c = Box::new(PulleyConstraint::new(
1922            a, Vec2::ZERO, Vec2::new(-1.0, 2.0),
1923            b, Vec2::ZERO, Vec2::new( 1.0, 2.0),
1924            1.0, total,
1925        ));
1926        world.add_constraint(c);
1927        world.step(1.0 / 60.0);
1928        let pa = world.body(a).unwrap().position;
1929        assert!(pa.is_finite(), "pulley should not produce NaN");
1930    }
1931
1932    #[test]
1933    fn spring_constraint_oscillates() {
1934        let mut world = ConstraintWorld::new();
1935        world.gravity = Vec2::ZERO;
1936        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1937        let b = world.add_body(BodyState::new(Vec2::new(3.0, 0.0), 1.0, 0.1));
1938        let c = Box::new(SpringConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 1.0, 50.0, 0.5));
1939        world.add_constraint(c);
1940        let pos_before = world.body(b).unwrap().position.x;
1941        world.step(0.016);
1942        let pos_after = world.body(b).unwrap().position.x;
1943        assert!(pos_after < pos_before, "spring should pull body toward rest length");
1944    }
1945
1946    #[test]
1947    fn motor_constraint_drives_rotation() {
1948        let mut world = ConstraintWorld::new();
1949        world.gravity = Vec2::ZERO;
1950        let a = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 1.0));
1951        let c = Box::new(MotorConstraint::new(a, 5.0, 100.0));
1952        world.add_constraint(c);
1953        for _ in 0..30 {
1954            world.step(1.0 / 60.0);
1955        }
1956        let omega = world.body(a).unwrap().angular_velocity;
1957        assert!(omega > 0.0, "motor should spin body, got {omega}");
1958    }
1959
1960    #[test]
1961    fn gear_constraint_couples_bodies() {
1962        let mut world = ConstraintWorld::new();
1963        world.gravity = Vec2::ZERO;
1964        let a = world.add_body({
1965            let mut s = BodyState::new(Vec2::ZERO, 1.0, 1.0);
1966            s.angular_velocity = 2.0;
1967            s
1968        });
1969        let b = world.add_body(BodyState::new(Vec2::new(2.0, 0.0), 1.0, 1.0));
1970        let c = Box::new(GearConstraint::new(a, b, 2.0));
1971        world.add_constraint(c);
1972        for _ in 0..30 {
1973            world.step(1.0 / 60.0);
1974        }
1975        let oa = world.body(a).unwrap().angular_velocity;
1976        let ob = world.body(b).unwrap().angular_velocity;
1977        // omega_a + 2 * omega_b ~ 0
1978        let coupling_err = (oa + 2.0 * ob).abs();
1979        assert!(coupling_err < 1.0, "gear coupling error too large: {coupling_err}");
1980    }
1981
1982    #[test]
1983    fn max_distance_constraint_rope() {
1984        let mut world = ConstraintWorld::new();
1985        world.gravity = Vec2::ZERO;
1986        let a = world.add_body(BodyState::static_body(Vec2::ZERO));
1987        let b = world.add_body({
1988            let mut s = BodyState::new(Vec2::new(3.0, 0.0), 1.0, 0.1);
1989            s.velocity = Vec2::new(10.0, 0.0);
1990            s
1991        });
1992        let c = Box::new(MaxDistanceConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 2.0));
1993        world.add_constraint(c);
1994        for _ in 0..60 {
1995            world.step(1.0 / 60.0);
1996        }
1997        let dist = world.body(b).unwrap().position.length();
1998        assert!(dist <= 2.5, "rope should limit distance, got {dist}");
1999    }
2000
2001    #[test]
2002    fn pin_constraint_holds_at_world_point() {
2003        let mut world = ConstraintWorld::new();
2004        world.gravity = Vec2::new(0.0, -9.81);
2005        let a = world.add_body(BodyState::new(Vec2::new(0.0, 5.0), 1.0, 0.1));
2006        let target = Vec2::new(0.0, 5.0);
2007        let c = Box::new(PinConstraint::new(a, Vec2::ZERO, target));
2008        world.add_constraint(c);
2009        for _ in 0..60 {
2010            world.step(1.0 / 60.0);
2011        }
2012        let pos = world.body(a).unwrap().position;
2013        let err = (pos - target).length();
2014        assert!(err < 0.5, "pin should hold, error={err}");
2015    }
2016
2017    #[test]
2018    fn constraint_world_gravity_applied() {
2019        let mut world = ConstraintWorld::new();
2020        let a = world.add_body(BodyState::new(Vec2::new(0.0, 10.0), 1.0, 0.1));
2021        world.step(1.0);
2022        let pos = world.body(a).unwrap().position;
2023        assert!(pos.y < 10.0, "gravity should pull body down");
2024    }
2025
2026    #[test]
2027    fn island_detection_finds_connected_components() {
2028        let mut world = ConstraintWorld::new();
2029        let a = world.add_body(BodyState::new(Vec2::ZERO, 1.0, 0.1));
2030        let b = world.add_body(BodyState::new(Vec2::new(1.0, 0.0), 1.0, 0.1));
2031        let c = world.add_body(BodyState::new(Vec2::new(5.0, 0.0), 1.0, 0.1));  // isolated
2032        world.add_constraint(Box::new(DistanceConstraint::new(a, Vec2::ZERO, b, Vec2::ZERO, 1.0)));
2033        let islands = detect_islands(&world.bodies, &world.constraints);
2034        // Should find 2 islands: {a,b} and {c}
2035        assert_eq!(islands.len(), 2, "should detect 2 islands");
2036    }
2037
2038    #[test]
2039    fn contact_constraint_non_penetration() {
2040        let mut bodies: HashMap<BodyHandle, BodyState> = HashMap::new();
2041        let h_a = BodyHandle(1);
2042        let h_b = BodyHandle(2);
2043        let mut ba = BodyState::new(Vec2::ZERO, 1.0, 0.1);
2044        let mut bb = BodyState::new(Vec2::new(0.5, 0.0), 1.0, 0.1);
2045        ba.velocity = Vec2::new(1.0, 0.0);
2046        bb.velocity = Vec2::new(-1.0, 0.0);
2047        bodies.insert(h_a, ba);
2048        bodies.insert(h_b, bb);
2049        let mut c = ContactConstraint::new(
2050            h_a, h_b,
2051            Vec2::new(0.25, 0.0), Vec2::X, 0.5,
2052            0.3, 0.5,
2053        );
2054        c.prepare(&bodies, 1.0 / 60.0);
2055        c.solve_contact(&mut bodies, 1.0 / 60.0);
2056        let va = bodies[&h_a].velocity.x;
2057        let vb = bodies[&h_b].velocity.x;
2058        assert!(vb >= va - 0.01, "relative velocity after impulse should be non-negative");
2059    }
2060
2061    #[test]
2062    fn total_kinetic_energy_is_finite() {
2063        let mut world = ConstraintWorld::new();
2064        for i in 0..10 {
2065            let s = BodyState::new(Vec2::new(i as f32, 0.0), 1.0, 0.1);
2066            world.add_body(s);
2067        }
2068        for _ in 0..60 {
2069            world.step(1.0 / 60.0);
2070        }
2071        let ke = world.total_kinetic_energy();
2072        assert!(ke.is_finite(), "KE should be finite");
2073    }
2074}