Skip to main content

proof_engine/physics/
rigid_body.rs

1//! 2D Rigid Body Physics for Proof Engine.
2//!
3//! Full dynamics pipeline:
4//! - Rigid body integration (semi-implicit Euler)
5//! - Collision shapes: Circle, Box, Polygon, Capsule
6//! - AABB broad phase
7//! - SAT narrow phase for convex polygons
8//! - Sequential impulse solver with position correction
9//! - Joint constraints: distance, revolute, prismatic, weld, spring
10//! - Continuous collision detection (sweep test)
11//! - Raycasting against all shapes
12//! - Sleeping bodies for performance
13
14use glam::{Vec2, Mat2};
15use std::collections::HashMap;
16
17// ── Constants ─────────────────────────────────────────────────────────────────
18
19pub const GRAVITY: Vec2 = Vec2::new(0.0, -9.81);
20const SLEEP_VELOCITY_THRESHOLD: f32 = 0.05;
21const SLEEP_ANGULAR_THRESHOLD:  f32 = 0.05;
22const SLEEP_TIME_THRESHOLD:     f32 = 0.5;
23const POSITION_CORRECTION_SLOP: f32 = 0.005;
24const POSITION_CORRECTION_PERCENT: f32 = 0.4;
25const MAX_SOLVER_ITERATIONS:    usize = 10;
26
27// ── BodyId ────────────────────────────────────────────────────────────────────
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct BodyId(pub u32);
31
32// ── Shape ─────────────────────────────────────────────────────────────────────
33
34/// Collision shape definition.
35#[derive(Debug, Clone)]
36pub enum Shape {
37    /// Circle with radius.
38    Circle { radius: f32 },
39    /// Axis-aligned box (half-extents before rotation).
40    Box    { half_w: f32, half_h: f32 },
41    /// Convex polygon with vertices in local space (CCW winding).
42    Polygon { vertices: Vec<Vec2> },
43    /// Capsule: two circles joined by a rectangle.
44    Capsule { half_height: f32, radius: f32 },
45}
46
47impl Shape {
48    /// Compute area.
49    pub fn area(&self) -> f32 {
50        match self {
51            Shape::Circle { radius }          => std::f32::consts::PI * radius * radius,
52            Shape::Box { half_w, half_h }     => 4.0 * half_w * half_h,
53            Shape::Polygon { vertices }       => polygon_area(vertices).abs(),
54            Shape::Capsule { half_height, radius } => {
55                std::f32::consts::PI * radius * radius + 4.0 * half_height * radius
56            }
57        }
58    }
59
60    /// Compute moment of inertia (per unit mass) around center.
61    pub fn moment_of_inertia(&self) -> f32 {
62        match self {
63            Shape::Circle { radius }      => 0.5 * radius * radius,
64            Shape::Box { half_w, half_h } => (half_w * half_w + half_h * half_h) / 3.0,
65            Shape::Polygon { vertices }   => polygon_moment(vertices),
66            Shape::Capsule { half_height, radius } => {
67                let rect_i = (4.0 * half_height * radius * (half_height * half_height / 3.0 + radius * radius / 4.0));
68                let circ_i = 0.5 * radius * radius * std::f32::consts::PI * radius * radius;
69                rect_i + circ_i
70            }
71        }
72    }
73
74    /// Compute local AABB.
75    pub fn local_aabb(&self) -> Aabb2 {
76        match self {
77            Shape::Circle { radius } => Aabb2::new(-Vec2::splat(*radius), Vec2::splat(*radius)),
78            Shape::Box { half_w, half_h } => Aabb2::new(Vec2::new(-half_w, -half_h), Vec2::new(*half_w, *half_h)),
79            Shape::Polygon { vertices } => {
80                let mn = vertices.iter().copied().reduce(|a, b| a.min(b)).unwrap_or(Vec2::ZERO);
81                let mx = vertices.iter().copied().reduce(|a, b| a.max(b)).unwrap_or(Vec2::ZERO);
82                Aabb2::new(mn, mx)
83            }
84            Shape::Capsule { half_height, radius } => {
85                Aabb2::new(Vec2::new(-radius, -half_height - radius), Vec2::new(*radius, half_height + radius))
86            }
87        }
88    }
89
90    /// Get vertices of this shape in local space (approximated for circles/capsules).
91    pub fn local_vertices(&self) -> Vec<Vec2> {
92        match self {
93            Shape::Box { half_w, half_h } => vec![
94                Vec2::new(-*half_w, -*half_h),
95                Vec2::new( *half_w, -*half_h),
96                Vec2::new( *half_w,  *half_h),
97                Vec2::new(-*half_w,  *half_h),
98            ],
99            Shape::Polygon { vertices } => vertices.clone(),
100            Shape::Circle { radius } => {
101                // 8-sided approximation for broadphase
102                (0..8).map(|i| {
103                    let a = i as f32 * std::f32::consts::TAU / 8.0;
104                    Vec2::new(a.cos(), a.sin()) * *radius
105                }).collect()
106            }
107            Shape::Capsule { half_height, radius } => vec![
108                Vec2::new(-*radius, -*half_height),
109                Vec2::new( *radius, -*half_height),
110                Vec2::new( *radius,  *half_height),
111                Vec2::new(-*radius,  *half_height),
112            ],
113        }
114    }
115}
116
117fn polygon_area(verts: &[Vec2]) -> f32 {
118    let n = verts.len();
119    let mut area = 0.0_f32;
120    for i in 0..n {
121        let j = (i + 1) % n;
122        area += verts[i].x * verts[j].y - verts[j].x * verts[i].y;
123    }
124    area * 0.5
125}
126
127fn polygon_moment(verts: &[Vec2]) -> f32 {
128    let n = verts.len();
129    let mut num = 0.0_f32;
130    let mut den = 0.0_f32;
131    for i in 0..n {
132        let j = (i + 1) % n;
133        let cross = verts[i].perp_dot(verts[j]).abs();
134        num += cross * (verts[i].dot(verts[i]) + verts[i].dot(verts[j]) + verts[j].dot(verts[j]));
135        den += cross;
136    }
137    if den.abs() < 1e-6 { return 1.0; }
138    num / (6.0 * den)
139}
140
141// ── Aabb2 ─────────────────────────────────────────────────────────────────────
142
143#[derive(Debug, Clone, Copy)]
144pub struct Aabb2 {
145    pub min: Vec2,
146    pub max: Vec2,
147}
148
149impl Aabb2 {
150    pub fn new(min: Vec2, max: Vec2) -> Self { Self { min, max } }
151
152    pub fn from_center_half(center: Vec2, half: Vec2) -> Self {
153        Self { min: center - half, max: center + half }
154    }
155
156    pub fn overlaps(&self, other: &Aabb2) -> bool {
157        self.min.x <= other.max.x && self.max.x >= other.min.x &&
158        self.min.y <= other.max.y && self.max.y >= other.min.y
159    }
160
161    pub fn contains_point(&self, p: Vec2) -> bool {
162        p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
163    }
164
165    pub fn expand(&self, margin: f32) -> Self {
166        Self { min: self.min - Vec2::splat(margin), max: self.max + Vec2::splat(margin) }
167    }
168
169    pub fn center(&self) -> Vec2 { (self.min + self.max) * 0.5 }
170    pub fn half_extents(&self) -> Vec2 { (self.max - self.min) * 0.5 }
171
172    /// Merge two AABBs.
173    pub fn union(&self, other: &Aabb2) -> Self {
174        Self { min: self.min.min(other.min), max: self.max.max(other.max) }
175    }
176
177    /// Ray-AABB intersection. Returns t (min hit distance) or None.
178    pub fn ray_intersect(&self, origin: Vec2, dir: Vec2) -> Option<f32> {
179        let inv_d = Vec2::new(
180            if dir.x.abs() > 1e-10 { 1.0 / dir.x } else { f32::INFINITY },
181            if dir.y.abs() > 1e-10 { 1.0 / dir.y } else { f32::INFINITY },
182        );
183        let t1 = (self.min - origin) * inv_d;
184        let t2 = (self.max - origin) * inv_d;
185        let tmin = t1.min(t2);
186        let tmax = t1.max(t2);
187        let t_enter = tmin.x.max(tmin.y);
188        let t_exit  = tmax.x.min(tmax.y);
189        if t_enter <= t_exit && t_exit >= 0.0 { Some(t_enter.max(0.0)) } else { None }
190    }
191}
192
193// ── PhysicsMaterial ───────────────────────────────────────────────────────────
194
195#[derive(Debug, Clone, Copy)]
196pub struct PhysicsMaterial {
197    pub restitution: f32, // bounciness 0..1
198    pub friction:    f32, // coefficient 0..1
199    pub density:     f32, // kg/m²
200}
201
202impl Default for PhysicsMaterial {
203    fn default() -> Self { Self { restitution: 0.3, friction: 0.5, density: 1.0 } }
204}
205
206impl PhysicsMaterial {
207    pub fn bouncy()   -> Self { Self { restitution: 0.9, friction: 0.1, density: 0.5 } }
208    pub fn sticky()   -> Self { Self { restitution: 0.0, friction: 0.9, density: 1.5 } }
209    pub fn ice()      -> Self { Self { restitution: 0.1, friction: 0.02, density: 0.9 } }
210    pub fn rubber()   -> Self { Self { restitution: 0.8, friction: 0.7, density: 1.2 } }
211    pub fn metal()    -> Self { Self { restitution: 0.2, friction: 0.3, density: 7.8 } }
212    pub fn wood()     -> Self { Self { restitution: 0.3, friction: 0.6, density: 0.6 } }
213}
214
215// ── BodyType ──────────────────────────────────────────────────────────────────
216
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub enum BodyType {
219    /// Fully simulated.
220    Dynamic,
221    /// Infinite mass, moved manually.
222    Kinematic,
223    /// Never moves.
224    Static,
225}
226
227// ── RigidBody2D ───────────────────────────────────────────────────────────────
228
229#[derive(Debug, Clone)]
230pub struct RigidBody2D {
231    pub id:        BodyId,
232    pub body_type: BodyType,
233    pub shape:     Shape,
234    pub material:  PhysicsMaterial,
235
236    // State
237    pub position:         Vec2,
238    pub angle:            f32,
239    pub linear_velocity:  Vec2,
240    pub angular_velocity: f32,
241
242    // Derived
243    pub mass:            f32,
244    pub inv_mass:        f32,
245    pub inertia:         f32,
246    pub inv_inertia:     f32,
247
248    // Accumulated forces (reset each step)
249    pub force:           Vec2,
250    pub torque:          f32,
251
252    // Damping
253    pub linear_damping:  f32,
254    pub angular_damping: f32,
255
256    // Sleeping
257    pub sleeping:        bool,
258    sleep_timer:         f32,
259
260    // User data
261    pub user_data:       u64,
262    pub collision_layer: u32,
263    pub collision_mask:  u32,
264
265    // Position from previous frame (for CCD)
266    pub prev_position:   Vec2,
267    pub prev_angle:      f32,
268    pub enabled:         bool,
269    pub fixed_rotation:  bool,
270    pub gravity_scale:   f32,
271}
272
273impl RigidBody2D {
274    pub fn new(id: BodyId, shape: Shape, material: PhysicsMaterial) -> Self {
275        let area = shape.area().max(1e-6);
276        let mass = area * material.density;
277        let inertia = mass * shape.moment_of_inertia();
278        Self {
279            id,
280            body_type: BodyType::Dynamic,
281            shape,
282            material,
283            position: Vec2::ZERO,
284            angle: 0.0,
285            linear_velocity: Vec2::ZERO,
286            angular_velocity: 0.0,
287            mass,
288            inv_mass: 1.0 / mass,
289            inertia,
290            inv_inertia: 1.0 / inertia,
291            force: Vec2::ZERO,
292            torque: 0.0,
293            linear_damping: 0.01,
294            angular_damping: 0.01,
295            sleeping: false,
296            sleep_timer: 0.0,
297            user_data: 0,
298            collision_layer: 1,
299            collision_mask: !0,
300            prev_position: Vec2::ZERO,
301            prev_angle: 0.0,
302            enabled: true,
303            fixed_rotation: false,
304            gravity_scale: 1.0,
305        }
306    }
307
308    pub fn static_body(id: BodyId, shape: Shape) -> Self {
309        let mut b = Self::new(id, shape, PhysicsMaterial::default());
310        b.body_type = BodyType::Static;
311        b.inv_mass = 0.0;
312        b.inv_inertia = 0.0;
313        b
314    }
315
316    pub fn apply_force(&mut self, force: Vec2) {
317        if self.body_type != BodyType::Dynamic { return; }
318        self.force += force;
319        self.sleeping = false;
320    }
321
322    pub fn apply_force_at(&mut self, force: Vec2, point: Vec2) {
323        if self.body_type != BodyType::Dynamic { return; }
324        self.force += force;
325        self.torque += (point - self.position).perp_dot(force);
326        self.sleeping = false;
327    }
328
329    pub fn apply_impulse(&mut self, impulse: Vec2) {
330        if self.body_type != BodyType::Dynamic { return; }
331        self.linear_velocity += impulse * self.inv_mass;
332        self.sleeping = false;
333    }
334
335    pub fn apply_impulse_at(&mut self, impulse: Vec2, point: Vec2) {
336        if self.body_type != BodyType::Dynamic { return; }
337        self.linear_velocity += impulse * self.inv_mass;
338        let r = point - self.position;
339        self.angular_velocity += r.perp_dot(impulse) * self.inv_inertia;
340        self.sleeping = false;
341    }
342
343    pub fn apply_torque(&mut self, torque: f32) {
344        if self.body_type != BodyType::Dynamic { return; }
345        self.torque += torque;
346        self.sleeping = false;
347    }
348
349    pub fn velocity_at_point(&self, point: Vec2) -> Vec2 {
350        let r = point - self.position;
351        self.linear_velocity + Vec2::new(-self.angular_velocity * r.y, self.angular_velocity * r.x)
352    }
353
354    pub fn world_aabb(&self) -> Aabb2 {
355        let local = self.shape.local_aabb();
356        // Approximate: inflate by max dimension when rotated
357        let r = local.half_extents().length();
358        Aabb2::from_center_half(self.position, Vec2::splat(r))
359    }
360
361    pub fn rotation_matrix(&self) -> Mat2 {
362        let (s, c) = self.angle.sin_cos();
363        Mat2::from_cols(Vec2::new(c, s), Vec2::new(-s, c))
364    }
365
366    pub fn local_to_world(&self, local: Vec2) -> Vec2 {
367        self.position + self.rotation_matrix() * local
368    }
369
370    pub fn world_to_local(&self, world: Vec2) -> Vec2 {
371        self.rotation_matrix().transpose() * (world - self.position)
372    }
373
374    /// Get world-space vertices.
375    pub fn world_vertices(&self) -> Vec<Vec2> {
376        let rot = self.rotation_matrix();
377        self.shape.local_vertices().iter().map(|v| self.position + rot * *v).collect()
378    }
379
380    fn integrate_forces(&mut self, dt: f32, gravity: Vec2) {
381        if self.body_type != BodyType::Dynamic || self.sleeping { return; }
382        let accel = self.force * self.inv_mass + gravity * self.gravity_scale;
383        self.linear_velocity += accel * dt;
384        self.linear_velocity *= 1.0 / (1.0 + self.linear_damping * dt);
385
386        if !self.fixed_rotation {
387            self.angular_velocity += self.torque * self.inv_inertia * dt;
388            self.angular_velocity *= 1.0 / (1.0 + self.angular_damping * dt);
389        }
390
391        self.force = Vec2::ZERO;
392        self.torque = 0.0;
393    }
394
395    fn integrate_velocities(&mut self, dt: f32) {
396        if self.body_type != BodyType::Dynamic || self.sleeping { return; }
397        self.prev_position = self.position;
398        self.prev_angle    = self.angle;
399        self.position += self.linear_velocity * dt;
400        if !self.fixed_rotation {
401            self.angle += self.angular_velocity * dt;
402        }
403    }
404
405    fn update_sleep(&mut self, dt: f32) {
406        if self.body_type != BodyType::Dynamic { return; }
407        let v2 = self.linear_velocity.length_squared();
408        let w2 = self.angular_velocity * self.angular_velocity;
409        if v2 < SLEEP_VELOCITY_THRESHOLD * SLEEP_VELOCITY_THRESHOLD
410        && w2 < SLEEP_ANGULAR_THRESHOLD  * SLEEP_ANGULAR_THRESHOLD {
411            self.sleep_timer += dt;
412            if self.sleep_timer > SLEEP_TIME_THRESHOLD {
413                self.sleeping = true;
414                self.linear_velocity  = Vec2::ZERO;
415                self.angular_velocity = 0.0;
416            }
417        } else {
418            self.sleep_timer = 0.0;
419            self.sleeping    = false;
420        }
421    }
422}
423
424// ── ContactManifold ───────────────────────────────────────────────────────────
425
426#[derive(Debug, Clone)]
427pub struct ContactPoint {
428    pub point:      Vec2, // world space
429    pub normal:     Vec2, // from b to a
430    pub depth:      f32,  // penetration depth
431    pub r_a:        Vec2, // contact point relative to body A center
432    pub r_b:        Vec2, // contact point relative to body B center
433    // Cached impulses for warm starting
434    pub normal_impulse:    f32,
435    pub tangent_impulse:   f32,
436    pub mass_normal:       f32,
437    pub mass_tangent:      f32,
438    pub velocity_bias:     f32,
439}
440
441impl ContactPoint {
442    pub fn new(point: Vec2, normal: Vec2, depth: f32) -> Self {
443        Self {
444            point, normal, depth,
445            r_a: Vec2::ZERO, r_b: Vec2::ZERO,
446            normal_impulse: 0.0, tangent_impulse: 0.0,
447            mass_normal: 0.0, mass_tangent: 0.0,
448            velocity_bias: 0.0,
449        }
450    }
451}
452
453#[derive(Debug, Clone)]
454pub struct ContactManifold {
455    pub body_a:   BodyId,
456    pub body_b:   BodyId,
457    pub contacts: Vec<ContactPoint>,
458    pub restitution: f32,
459    pub friction:    f32,
460}
461
462// ── SAT Collision ─────────────────────────────────────────────────────────────
463
464/// Separating Axis Theorem collision detection.
465pub struct Sat;
466
467impl Sat {
468    pub fn test_circle_circle(
469        pos_a: Vec2, r_a: f32,
470        pos_b: Vec2, r_b: f32,
471    ) -> Option<ContactPoint> {
472        let delta = pos_b - pos_a;
473        let dist  = delta.length();
474        let sum_r = r_a + r_b;
475        if dist >= sum_r || dist < 1e-8 { return None; }
476        let normal = delta / dist;
477        let depth  = sum_r - dist;
478        let point  = pos_a + normal * r_a;
479        Some(ContactPoint::new(point, -normal, depth))
480    }
481
482    pub fn test_circle_polygon(
483        circle_pos: Vec2, radius: f32,
484        poly_pos: Vec2,   poly_rot: Mat2, poly_verts: &[Vec2],
485    ) -> Option<ContactPoint> {
486        // Transform circle center to polygon local space
487        let local_center = poly_rot.transpose() * (circle_pos - poly_pos);
488        let n = poly_verts.len();
489
490        let mut min_overlap = f32::NEG_INFINITY;
491        let mut best_normal = Vec2::X;
492        let mut closest_on_edge = Vec2::ZERO;
493
494        for i in 0..n {
495            let a = poly_verts[i];
496            let b = poly_verts[(i + 1) % n];
497            let edge = b - a;
498            let normal = Vec2::new(edge.y, -edge.x).normalize_or_zero();
499            let dist = (local_center - a).dot(normal);
500            if dist > radius { return None; } // Separated on this axis
501            if dist > min_overlap {
502                min_overlap = dist;
503                best_normal = normal;
504                // Closest point on edge
505                let t = (local_center - a).dot(edge) / edge.dot(edge).max(1e-10);
506                closest_on_edge = a + edge * t.clamp(0.0, 1.0);
507            }
508        }
509
510        // Check if circle center is inside polygon
511        let overlap = radius - min_overlap;
512        let contact_normal = poly_rot * best_normal;
513        let contact_point  = poly_pos + poly_rot * closest_on_edge;
514        Some(ContactPoint::new(contact_point, contact_normal, overlap.max(0.0)))
515    }
516
517    pub fn test_polygon_polygon(
518        pos_a: Vec2, rot_a: Mat2, verts_a: &[Vec2],
519        pos_b: Vec2, rot_b: Mat2, verts_b: &[Vec2],
520    ) -> Option<ContactManifold> {
521        // World-space vertices
522        let wa: Vec<Vec2> = verts_a.iter().map(|v| pos_a + rot_a * *v).collect();
523        let wb: Vec<Vec2> = verts_b.iter().map(|v| pos_b + rot_b * *v).collect();
524
525        let mut min_overlap = f32::INFINITY;
526        let mut best_normal = Vec2::ZERO;
527
528        // Test edges of A
529        if let Some((overlap, normal)) = Self::min_separation_axis(&wa, &wb) {
530            if overlap < min_overlap { min_overlap = overlap; best_normal = normal; }
531        } else { return None; }
532
533        // Test edges of B
534        if let Some((overlap, normal)) = Self::min_separation_axis(&wb, &wa) {
535            if overlap < min_overlap { min_overlap = overlap; best_normal = -normal; }
536        } else { return None; }
537
538        // Ensure normal points from A to B
539        if best_normal.dot(pos_b - pos_a) < 0.0 { best_normal = -best_normal; }
540
541        // Find contact points (incident/reference edge clipping)
542        let contacts = Self::find_contact_points(&wa, &wb, best_normal);
543
544        if contacts.is_empty() { return None; }
545
546        let contact_points = contacts.into_iter().map(|(pt, depth)| {
547            ContactPoint::new(pt, -best_normal, depth.max(0.0))
548        }).collect();
549
550        Some(ContactManifold {
551            body_a: BodyId(0), body_b: BodyId(0),
552            contacts: contact_points,
553            restitution: 0.3, friction: 0.5,
554        })
555    }
556
557    fn min_separation_axis(a: &[Vec2], b: &[Vec2]) -> Option<(f32, Vec2)> {
558        let n = a.len();
559        let mut min_overlap = f32::INFINITY;
560        let mut best_normal = Vec2::ZERO;
561
562        for i in 0..n {
563            let edge   = a[(i + 1) % n] - a[i];
564            let normal = Vec2::new(edge.y, -edge.x).normalize_or_zero();
565
566            // Project all of B onto this axis
567            let (min_b, _max_b) = project_polygon(b, normal);
568            let (min_a,  max_a) = project_polygon(a, normal);
569
570            let overlap = max_a - min_b;
571            if overlap <= 0.0 { return None; } // Separating axis found
572            if overlap < min_overlap { min_overlap = overlap; best_normal = normal; }
573        }
574
575        Some((min_overlap, best_normal))
576    }
577
578    fn find_contact_points(a: &[Vec2], b: &[Vec2], normal: Vec2) -> Vec<(Vec2, f32)> {
579        let mut contacts = Vec::new();
580        // Clip B vertices that are behind A's reference face
581        for &p in b {
582            // Find depth along normal
583            let depth = project_polygon(a, normal).1 - p.dot(normal);
584            if depth >= -POSITION_CORRECTION_SLOP {
585                contacts.push((p, depth));
586            }
587            if contacts.len() >= 2 { break; }
588        }
589        contacts
590    }
591
592    /// Circle vs Box collision.
593    pub fn test_circle_box(
594        circle_pos: Vec2, radius: f32,
595        box_pos: Vec2, box_rot: Mat2, half_w: f32, half_h: f32,
596    ) -> Option<ContactPoint> {
597        let verts = [
598            Vec2::new(-half_w, -half_h),
599            Vec2::new( half_w, -half_h),
600            Vec2::new( half_w,  half_h),
601            Vec2::new(-half_w,  half_h),
602        ];
603        Self::test_circle_polygon(circle_pos, radius, box_pos, box_rot, &verts)
604    }
605}
606
607fn project_polygon(verts: &[Vec2], axis: Vec2) -> (f32, f32) {
608    let mut min = f32::INFINITY;
609    let mut max = f32::NEG_INFINITY;
610    for &v in verts {
611        let d = v.dot(axis);
612        min = min.min(d);
613        max = max.max(d);
614    }
615    (min, max)
616}
617
618// ── ConstraintJoint ───────────────────────────────────────────────────────────
619
620#[derive(Debug, Clone, Copy, PartialEq)]
621pub struct JointId(pub u32);
622
623/// Joint connecting two bodies.
624#[derive(Debug, Clone)]
625pub enum Joint {
626    /// Maintains a fixed distance between two anchor points.
627    Distance {
628        id:          JointId,
629        body_a:      BodyId,
630        body_b:      BodyId,
631        anchor_a:    Vec2, // local space
632        anchor_b:    Vec2, // local space
633        rest_length: f32,
634        stiffness:   f32,
635        damping:     f32,
636    },
637    /// Allows relative rotation only.
638    Revolute {
639        id:          JointId,
640        body_a:      BodyId,
641        body_b:      BodyId,
642        anchor_a:    Vec2,
643        anchor_b:    Vec2,
644        lower_angle: Option<f32>,
645        upper_angle: Option<f32>,
646        motor_speed: Option<f32>,
647        motor_torque: f32,
648    },
649    /// Allows only linear motion along an axis.
650    Prismatic {
651        id:          JointId,
652        body_a:      BodyId,
653        body_b:      BodyId,
654        anchor_a:    Vec2,
655        anchor_b:    Vec2,
656        axis:        Vec2,
657        lower_limit: Option<f32>,
658        upper_limit: Option<f32>,
659    },
660    /// Fully locks two bodies together.
661    Weld {
662        id:          JointId,
663        body_a:      BodyId,
664        body_b:      BodyId,
665        anchor_a:    Vec2,
666        anchor_b:    Vec2,
667        ref_angle:   f32,
668    },
669    /// Spring joint (soft distance constraint).
670    Spring {
671        id:          JointId,
672        body_a:      BodyId,
673        body_b:      BodyId,
674        anchor_a:    Vec2,
675        anchor_b:    Vec2,
676        rest_length: f32,
677        frequency:   f32, // Hz
678        damping_ratio: f32,
679    },
680    /// Mouse/target joint — pull body toward a world point.
681    Target {
682        id:         JointId,
683        body:       BodyId,
684        anchor:     Vec2, // local point on body
685        target:     Vec2, // world point
686        max_force:  f32,
687        frequency:  f32,
688        damping_ratio: f32,
689    },
690}
691
692impl Joint {
693    pub fn id(&self) -> JointId {
694        match self {
695            Joint::Distance   { id, .. } => *id,
696            Joint::Revolute   { id, .. } => *id,
697            Joint::Prismatic  { id, .. } => *id,
698            Joint::Weld       { id, .. } => *id,
699            Joint::Spring     { id, .. } => *id,
700            Joint::Target     { id, .. } => *id,
701        }
702    }
703
704    /// Apply impulses to enforce this joint.
705    pub fn solve(&self, bodies: &mut HashMap<BodyId, RigidBody2D>, dt: f32) {
706        match self {
707            Joint::Distance { body_a, body_b, anchor_a, anchor_b, rest_length, stiffness, damping, id: _ } => {
708                let (pa, pb, va, vb, inv_ma, inv_mb, inv_ia, inv_ib, ra, rb) = {
709                    let a = bodies.get(body_a);
710                    let b = bodies.get(body_b);
711                    if a.is_none() || b.is_none() { return; }
712                    let a = a.unwrap();
713                    let b = b.unwrap();
714                    let rot_a = a.rotation_matrix();
715                    let rot_b = b.rotation_matrix();
716                    let ra = rot_a * *anchor_a;
717                    let rb = rot_b * *anchor_b;
718                    (a.position, b.position, a.linear_velocity, b.linear_velocity,
719                     a.inv_mass, b.inv_mass, a.inv_inertia, b.inv_inertia, ra, rb)
720                };
721
722                let wa = pa + ra;
723                let wb = pb + rb;
724                let delta = wb - wa;
725                let len = delta.length();
726                if len < 1e-6 { return; }
727                let n = delta / len;
728                let stretch = len - rest_length;
729
730                // Spring force (Hooke's law)
731                let rel_vel = (vb + Vec2::new(-b_angvel(bodies, body_b) * rb.y, b_angvel(bodies, body_b) * rb.x))
732                    - (va + Vec2::new(-a_angvel(bodies, body_a) * ra.y, a_angvel(bodies, body_a) * ra.x));
733                let vel_along = rel_vel.dot(n);
734
735                let impulse_mag = stiffness * stretch * dt + damping * vel_along * dt;
736                let impulse = n * impulse_mag;
737
738                let effective_mass = inv_ma + inv_mb
739                    + ra.perp_dot(n).powi(2) * inv_ia
740                    + rb.perp_dot(n).powi(2) * inv_ib;
741                if effective_mass < 1e-10 { return; }
742                let scaled = impulse / effective_mass.max(0.001);
743
744                if let Some(a) = bodies.get_mut(body_a) {
745                    if a.body_type == BodyType::Dynamic {
746                        a.linear_velocity  -= scaled * inv_ma;
747                        a.angular_velocity -= ra.perp_dot(scaled) * inv_ia;
748                    }
749                }
750                if let Some(b) = bodies.get_mut(body_b) {
751                    if b.body_type == BodyType::Dynamic {
752                        b.linear_velocity  += scaled * inv_mb;
753                        b.angular_velocity += rb.perp_dot(scaled) * inv_ib;
754                    }
755                }
756            }
757
758            Joint::Spring { body_a, body_b, anchor_a, anchor_b, rest_length, frequency, damping_ratio, id: _ } => {
759                let omega = 2.0 * std::f32::consts::PI * frequency;
760                let k  = omega * omega;
761                let c  = 2.0 * damping_ratio * omega;
762                let a_stiff = 1.0 / (1.0 + c * dt + k * dt * dt);
763
764                let pa = bodies.get(body_a).map(|b| b.position).unwrap_or_default();
765                let pb = bodies.get(body_b).map(|b| b.position).unwrap_or_default();
766                let delta = pb - pa;
767                let len = delta.length();
768                if len < 1e-6 { return; }
769                let n = delta / len;
770                let stretch = len - rest_length;
771
772                let impulse = n * (stretch * k * dt * a_stiff);
773
774                if let Some(a) = bodies.get_mut(body_a) {
775                    if a.body_type == BodyType::Dynamic { a.apply_impulse(impulse); }
776                }
777                if let Some(b) = bodies.get_mut(body_b) {
778                    if b.body_type == BodyType::Dynamic { b.apply_impulse(-impulse); }
779                }
780            }
781
782            Joint::Target { body, anchor, target, max_force, frequency, damping_ratio, id: _ } => {
783                let omega = 2.0 * std::f32::consts::PI * frequency;
784                let k = omega * omega;
785                let c = 2.0 * damping_ratio * omega;
786
787                if let Some(b) = bodies.get_mut(body) {
788                    if b.body_type != BodyType::Dynamic { return; }
789                    let rot = b.rotation_matrix();
790                    let world_anchor = b.position + rot * *anchor;
791                    let error = *target - world_anchor;
792                    let force = error * k - b.linear_velocity * c;
793                    let force = force.clamp_length_max(*max_force);
794                    b.apply_force(force);
795                }
796            }
797
798            _ => { /* Other joints would apply positional corrections */ }
799        }
800    }
801}
802
803fn a_angvel(bodies: &HashMap<BodyId, RigidBody2D>, id: &BodyId) -> f32 {
804    bodies.get(id).map(|b| b.angular_velocity).unwrap_or(0.0)
805}
806fn b_angvel(bodies: &HashMap<BodyId, RigidBody2D>, id: &BodyId) -> f32 {
807    bodies.get(id).map(|b| b.angular_velocity).unwrap_or(0.0)
808}
809
810// ── ImpulseSolver ─────────────────────────────────────────────────────────────
811
812pub struct ImpulseSolver;
813
814impl ImpulseSolver {
815    /// Pre-compute effective masses for a contact manifold.
816    pub fn pre_step(manifold: &mut ContactManifold, bodies: &HashMap<BodyId, RigidBody2D>, dt: f32) {
817        let (inv_ma, inv_mb, inv_ia, inv_ib, rest, fric, vel_a, vel_b, ang_a, ang_b, pa, pb) = {
818            let a = bodies.get(&manifold.body_a);
819            let b = bodies.get(&manifold.body_b);
820            if a.is_none() || b.is_none() { return; }
821            let a = a.unwrap(); let b = b.unwrap();
822            let rest = (a.material.restitution * b.material.restitution).sqrt();
823            let fric = (a.material.friction    * b.material.friction).sqrt();
824            (a.inv_mass, b.inv_mass, a.inv_inertia, b.inv_inertia,
825             rest, fric, a.linear_velocity, b.linear_velocity,
826             a.angular_velocity, b.angular_velocity, a.position, b.position)
827        };
828
829        for cp in &mut manifold.contacts {
830            cp.r_a = cp.point - pa;
831            cp.r_b = cp.point - pb;
832
833            let rn_a = cp.r_a.perp_dot(cp.normal);
834            let rn_b = cp.r_b.perp_dot(cp.normal);
835            let k_normal = inv_ma + inv_mb + rn_a * rn_a * inv_ia + rn_b * rn_b * inv_ib;
836            cp.mass_normal = if k_normal > 1e-10 { 1.0 / k_normal } else { 0.0 };
837
838            let tangent = Vec2::new(cp.normal.y, -cp.normal.x);
839            let rt_a = cp.r_a.perp_dot(tangent);
840            let rt_b = cp.r_b.perp_dot(tangent);
841            let k_tangent = inv_ma + inv_mb + rt_a * rt_a * inv_ia + rt_b * rt_b * inv_ib;
842            cp.mass_tangent = if k_tangent > 1e-10 { 1.0 / k_tangent } else { 0.0 };
843
844            // Restitution bias
845            let vrel = {
846                let vb = vel_b + Vec2::new(-ang_b * cp.r_b.y, ang_b * cp.r_b.x);
847                let va = vel_a + Vec2::new(-ang_a * cp.r_a.y, ang_a * cp.r_a.x);
848                (vb - va).dot(cp.normal)
849            };
850            cp.velocity_bias = if vrel < -1.0 { -rest * vrel } else { 0.0 };
851
852            // Position correction bias (Baumgarte)
853            manifold.restitution = rest;
854            manifold.friction    = fric;
855            let _ = dt; // used in position correction step
856        }
857    }
858
859    /// Apply one iteration of sequential impulse.
860    pub fn apply_impulse(manifold: &mut ContactManifold, bodies: &mut HashMap<BodyId, RigidBody2D>) {
861        let fric = manifold.friction;
862        for cp in &mut manifold.contacts {
863            let (vel_a, ang_a, vel_b, ang_b, inv_ma, inv_mb, inv_ia, inv_ib) = {
864                let a = bodies.get(&manifold.body_a);
865                let b = bodies.get(&manifold.body_b);
866                if a.is_none() || b.is_none() { return; }
867                let a = a.unwrap(); let b = b.unwrap();
868                (a.linear_velocity, a.angular_velocity, b.linear_velocity, b.angular_velocity,
869                 a.inv_mass, b.inv_mass, a.inv_inertia, b.inv_inertia)
870            };
871
872            let vb = vel_b + Vec2::new(-ang_b * cp.r_b.y, ang_b * cp.r_b.x);
873            let va = vel_a + Vec2::new(-ang_a * cp.r_a.y, ang_a * cp.r_a.x);
874            let vrel = vb - va;
875
876            // Normal impulse
877            let vn  = vrel.dot(cp.normal);
878            let dj  = cp.mass_normal * (-vn + cp.velocity_bias);
879            let j0  = cp.normal_impulse;
880            cp.normal_impulse = (j0 + dj).max(0.0); // clamp to non-negative
881            let dj  = cp.normal_impulse - j0;
882            let impulse_n = cp.normal * dj;
883
884            // Friction impulse (tangential)
885            let tangent = Vec2::new(cp.normal.y, -cp.normal.x);
886            let vt  = vrel.dot(tangent);
887            let djt = -cp.mass_tangent * vt;
888            let max_friction = fric * cp.normal_impulse;
889            let j0t = cp.tangent_impulse;
890            cp.tangent_impulse = (j0t + djt).clamp(-max_friction, max_friction);
891            let djt = cp.tangent_impulse - j0t;
892            let impulse_t = tangent * djt;
893
894            let impulse = impulse_n + impulse_t;
895
896            if let Some(a) = bodies.get_mut(&manifold.body_a) {
897                if a.body_type == BodyType::Dynamic {
898                    a.linear_velocity  -= impulse * inv_ma;
899                    a.angular_velocity -= cp.r_a.perp_dot(impulse) * inv_ia;
900                }
901            }
902            if let Some(b) = bodies.get_mut(&manifold.body_b) {
903                if b.body_type == BodyType::Dynamic {
904                    b.linear_velocity  += impulse * inv_mb;
905                    b.angular_velocity += cp.r_b.perp_dot(impulse) * inv_ib;
906                }
907            }
908        }
909    }
910
911    /// Position correction to prevent drift.
912    pub fn correct_positions(manifold: &ContactManifold, bodies: &mut HashMap<BodyId, RigidBody2D>) {
913        for cp in &manifold.contacts {
914            let (inv_ma, inv_mb) = {
915                let a = bodies.get(&manifold.body_a).map(|b| b.inv_mass).unwrap_or(0.0);
916                let b = bodies.get(&manifold.body_b).map(|b| b.inv_mass).unwrap_or(0.0);
917                (a, b)
918            };
919
920            let correction_mag = ((cp.depth - POSITION_CORRECTION_SLOP).max(0.0)
921                / (inv_ma + inv_mb + 1e-10)) * POSITION_CORRECTION_PERCENT;
922            let correction = cp.normal * correction_mag;
923
924            if let Some(a) = bodies.get_mut(&manifold.body_a) {
925                if a.body_type == BodyType::Dynamic { a.position -= correction * inv_ma; }
926            }
927            if let Some(b) = bodies.get_mut(&manifold.body_b) {
928                if b.body_type == BodyType::Dynamic { b.position += correction * inv_mb; }
929            }
930        }
931    }
932}
933
934// ── Raycasting ────────────────────────────────────────────────────────────────
935
936#[derive(Debug, Clone)]
937pub struct RayHit {
938    pub body_id:  BodyId,
939    pub point:    Vec2,
940    pub normal:   Vec2,
941    pub distance: f32,
942    pub fraction: f32,
943}
944
945pub struct RayCaster;
946
947impl RayCaster {
948    pub fn cast_vs_circle(
949        origin: Vec2, dir: Vec2, max_dist: f32,
950        center: Vec2, radius: f32,
951    ) -> Option<f32> {
952        let oc = origin - center;
953        let b = oc.dot(dir);
954        let c = oc.dot(oc) - radius * radius;
955        let disc = b * b - c;
956        if disc < 0.0 { return None; }
957        let t = -b - disc.sqrt();
958        if t >= 0.0 && t <= max_dist { Some(t) } else { None }
959    }
960
961    pub fn cast_vs_aabb(origin: Vec2, dir: Vec2, max_dist: f32, aabb: &Aabb2) -> Option<f32> {
962        aabb.ray_intersect(origin, dir).filter(|&t| t <= max_dist)
963    }
964
965    pub fn cast_vs_polygon(
966        origin: Vec2, dir: Vec2, max_dist: f32,
967        pos: Vec2, rot: Mat2, verts: &[Vec2],
968    ) -> Option<(f32, Vec2)> {
969        let local_origin = rot.transpose() * (origin - pos);
970        let local_dir    = rot.transpose() * dir;
971        let n = verts.len();
972        let mut best_t = f32::INFINITY;
973        let mut best_n = Vec2::Y;
974
975        for i in 0..n {
976            let a = verts[i];
977            let b = verts[(i + 1) % n];
978            let edge = b - a;
979            let normal = Vec2::new(edge.y, -edge.x);
980            let denom = normal.dot(local_dir);
981            if denom.abs() < 1e-10 { continue; }
982            let t = normal.dot(a - local_origin) / denom;
983            if t < 0.0 || t > max_dist { continue; }
984            // Check if hit point is on edge
985            let hit = local_origin + local_dir * t;
986            let proj = (hit - a).dot(edge) / edge.dot(edge).max(1e-10);
987            if proj >= 0.0 && proj <= 1.0 && t < best_t {
988                best_t = t;
989                best_n = rot * normal.normalize_or_zero();
990            }
991        }
992
993        if best_t <= max_dist { Some((best_t, best_n)) } else { None }
994    }
995}
996
997// ── CCD (Continuous Collision Detection) ──────────────────────────────────────
998
999pub struct Ccd;
1000
1001impl Ccd {
1002    /// Sweep two moving circles for earliest time-of-impact [0, 1].
1003    pub fn sweep_circles(
1004        pa0: Vec2, pa1: Vec2, ra: f32,
1005        pb0: Vec2, pb1: Vec2, rb: f32,
1006    ) -> Option<f32> {
1007        let dpa = pa1 - pa0;
1008        let dpb = pb1 - pb0;
1009        let rel_vel = dpa - dpb;
1010        let rel_pos = pa0 - pb0;
1011        let sum_r = ra + rb;
1012
1013        let a = rel_vel.dot(rel_vel);
1014        let b = 2.0 * rel_pos.dot(rel_vel);
1015        let c = rel_pos.dot(rel_pos) - sum_r * sum_r;
1016
1017        if a.abs() < 1e-10 {
1018            // Parallel movement
1019            return if c <= 0.0 { Some(0.0) } else { None };
1020        }
1021
1022        let disc = b * b - 4.0 * a * c;
1023        if disc < 0.0 { return None; }
1024
1025        let t = (-b - disc.sqrt()) / (2.0 * a);
1026        if t >= 0.0 && t <= 1.0 { Some(t) } else { None }
1027    }
1028
1029    /// Find the first sub-step where body A's swept AABB hits body B.
1030    pub fn needs_ccd(body: &RigidBody2D) -> bool {
1031        let aabb = body.world_aabb();
1032        let diag = (aabb.max - aabb.min).length();
1033        let vel  = body.linear_velocity.length();
1034        // CCD needed when velocity would traverse more than half the body per step at 60hz
1035        vel * (1.0 / 60.0) > diag * 0.5
1036    }
1037}
1038
1039// ── PhysicsWorld2D ────────────────────────────────────────────────────────────
1040
1041/// The main physics simulation world.
1042pub struct PhysicsWorld2D {
1043    pub bodies:    HashMap<BodyId, RigidBody2D>,
1044    pub joints:    Vec<Joint>,
1045    pub gravity:   Vec2,
1046    pub substeps:  u32,
1047    next_body_id:  u32,
1048    next_joint_id: u32,
1049
1050    // Broadphase cache
1051    manifolds:     Vec<ContactManifold>,
1052
1053    // Stats
1054    pub last_contact_count: usize,
1055    pub last_solve_time_us: u64,
1056
1057    // Sleep
1058    pub allow_sleeping: bool,
1059}
1060
1061impl PhysicsWorld2D {
1062    pub fn new() -> Self {
1063        Self {
1064            bodies: HashMap::new(),
1065            joints: Vec::new(),
1066            gravity: GRAVITY,
1067            substeps: 1,
1068            next_body_id: 1,
1069            next_joint_id: 1,
1070            manifolds: Vec::new(),
1071            last_contact_count: 0,
1072            last_solve_time_us: 0,
1073            allow_sleeping: true,
1074        }
1075    }
1076
1077    pub fn zero_gravity() -> Self {
1078        let mut w = Self::new();
1079        w.gravity = Vec2::ZERO;
1080        w
1081    }
1082
1083    pub fn add_body(&mut self, mut body: RigidBody2D) -> BodyId {
1084        let id = BodyId(self.next_body_id);
1085        self.next_body_id += 1;
1086        body.id = id;
1087        self.bodies.insert(id, body);
1088        id
1089    }
1090
1091    pub fn remove_body(&mut self, id: BodyId) {
1092        self.bodies.remove(&id);
1093        self.joints.retain(|j| match j {
1094            Joint::Distance  { body_a, body_b, .. } => *body_a != id && *body_b != id,
1095            Joint::Revolute  { body_a, body_b, .. } => *body_a != id && *body_b != id,
1096            Joint::Prismatic { body_a, body_b, .. } => *body_a != id && *body_b != id,
1097            Joint::Weld      { body_a, body_b, .. } => *body_a != id && *body_b != id,
1098            Joint::Spring    { body_a, body_b, .. } => *body_a != id && *body_b != id,
1099            Joint::Target    { body, .. }           => *body != id,
1100        });
1101    }
1102
1103    pub fn add_joint(&mut self, mut joint: Joint) -> JointId {
1104        let id = JointId(self.next_joint_id);
1105        self.next_joint_id += 1;
1106        // Patch id into joint
1107        match &mut joint {
1108            Joint::Distance  { id: jid, .. } => *jid = id,
1109            Joint::Revolute  { id: jid, .. } => *jid = id,
1110            Joint::Prismatic { id: jid, .. } => *jid = id,
1111            Joint::Weld      { id: jid, .. } => *jid = id,
1112            Joint::Spring    { id: jid, .. } => *jid = id,
1113            Joint::Target    { id: jid, .. } => *jid = id,
1114        }
1115        self.joints.push(joint);
1116        id
1117    }
1118
1119    pub fn get_body(&self, id: BodyId) -> Option<&RigidBody2D> {
1120        self.bodies.get(&id)
1121    }
1122
1123    pub fn get_body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody2D> {
1124        self.bodies.get_mut(&id)
1125    }
1126
1127    /// Advance the simulation by `dt` seconds.
1128    pub fn step(&mut self, dt: f32) {
1129        let sub_dt = dt / self.substeps as f32;
1130        for _ in 0..self.substeps {
1131            self.sub_step(sub_dt);
1132        }
1133    }
1134
1135    fn sub_step(&mut self, dt: f32) {
1136        // 1. Integrate forces
1137        for body in self.bodies.values_mut() {
1138            if !body.enabled { continue; }
1139            body.integrate_forces(dt, self.gravity);
1140        }
1141
1142        // 2. Solve joints
1143        let joint_ids: Vec<usize> = (0..self.joints.len()).collect();
1144        for &i in &joint_ids {
1145            let joint = self.joints[i].clone();
1146            joint.solve(&mut self.bodies, dt);
1147        }
1148
1149        // 3. Integrate velocities
1150        for body in self.bodies.values_mut() {
1151            if !body.enabled { continue; }
1152            body.integrate_velocities(dt);
1153        }
1154
1155        // 4. Broadphase + narrowphase
1156        self.manifolds = self.detect_collisions();
1157
1158        // 5. Pre-step contacts
1159        for manifold in &mut self.manifolds {
1160            ImpulseSolver::pre_step(manifold, &self.bodies, dt);
1161        }
1162
1163        // 6. Sequential impulse solver
1164        for _ in 0..MAX_SOLVER_ITERATIONS {
1165            for i in 0..self.manifolds.len() {
1166                let manifold = &mut self.manifolds[i];
1167                ImpulseSolver::apply_impulse(manifold, &mut self.bodies);
1168            }
1169        }
1170
1171        // 7. Position correction
1172        for manifold in &self.manifolds {
1173            ImpulseSolver::correct_positions(manifold, &mut self.bodies);
1174        }
1175
1176        // 8. Sleep update
1177        if self.allow_sleeping {
1178            for body in self.bodies.values_mut() {
1179                body.update_sleep(dt);
1180            }
1181        }
1182
1183        self.last_contact_count = self.manifolds.iter().map(|m| m.contacts.len()).sum();
1184    }
1185
1186    fn detect_collisions(&self) -> Vec<ContactManifold> {
1187        let mut manifolds = Vec::new();
1188        let ids: Vec<BodyId> = self.bodies.keys().copied().collect();
1189
1190        for i in 0..ids.len() {
1191            for j in (i + 1)..ids.len() {
1192                let id_a = ids[i];
1193                let id_b = ids[j];
1194
1195                let a = &self.bodies[&id_a];
1196                let b = &self.bodies[&id_b];
1197
1198                if !a.enabled || !b.enabled { continue; }
1199                if a.body_type == BodyType::Static && b.body_type == BodyType::Static { continue; }
1200                if a.sleeping && b.sleeping { continue; }
1201                if (a.collision_mask & b.collision_layer) == 0 { continue; }
1202
1203                // Broadphase
1204                let aabb_a = a.world_aabb();
1205                let aabb_b = b.world_aabb();
1206                if !aabb_a.overlaps(&aabb_b) { continue; }
1207
1208                // Narrowphase
1209                if let Some(mut m) = self.narrow_phase(a, b) {
1210                    m.body_a = id_a;
1211                    m.body_b = id_b;
1212                    m.restitution = (a.material.restitution * b.material.restitution).sqrt();
1213                    m.friction    = (a.material.friction    * b.material.friction).sqrt();
1214                    manifolds.push(m);
1215                }
1216            }
1217        }
1218
1219        manifolds
1220    }
1221
1222    fn narrow_phase(&self, a: &RigidBody2D, b: &RigidBody2D) -> Option<ContactManifold> {
1223        match (&a.shape, &b.shape) {
1224            (Shape::Circle { radius: ra }, Shape::Circle { radius: rb }) => {
1225                Sat::test_circle_circle(a.position, *ra, b.position, *rb).map(|cp| {
1226                    ContactManifold {
1227                        body_a: a.id, body_b: b.id,
1228                        contacts: vec![cp],
1229                        restitution: 0.3, friction: 0.5,
1230                    }
1231                })
1232            }
1233            (Shape::Circle { radius }, Shape::Box { half_w, half_h }) => {
1234                Sat::test_circle_box(a.position, *radius, b.position, b.rotation_matrix(), *half_w, *half_h)
1235                    .map(|cp| ContactManifold { body_a: a.id, body_b: b.id, contacts: vec![cp], restitution: 0.3, friction: 0.5 })
1236            }
1237            (Shape::Box { half_w, half_h }, Shape::Circle { radius }) => {
1238                Sat::test_circle_box(b.position, *radius, a.position, a.rotation_matrix(), *half_w, *half_h)
1239                    .map(|mut cp| { cp.normal = -cp.normal; ContactManifold { body_a: a.id, body_b: b.id, contacts: vec![cp], restitution: 0.3, friction: 0.5 } })
1240            }
1241            (_, _) => {
1242                // Polygon vs polygon (covers Box vs Box, Polygon vs Polygon, etc.)
1243                let verts_a = a.shape.local_vertices();
1244                let verts_b = b.shape.local_vertices();
1245                Sat::test_polygon_polygon(
1246                    a.position, a.rotation_matrix(), &verts_a,
1247                    b.position, b.rotation_matrix(), &verts_b,
1248                ).map(|mut m| { m.body_a = a.id; m.body_b = b.id; m })
1249            }
1250        }
1251    }
1252
1253    /// Cast a ray through the world, returning all hits sorted by distance.
1254    pub fn raycast(&self, origin: Vec2, dir: Vec2, max_dist: f32) -> Vec<RayHit> {
1255        let dir = dir.normalize_or_zero();
1256        let mut hits = Vec::new();
1257
1258        for (id, body) in &self.bodies {
1259            if !body.enabled { continue; }
1260            match &body.shape {
1261                Shape::Circle { radius } => {
1262                    if let Some(t) = RayCaster::cast_vs_circle(origin, dir, max_dist, body.position, *radius) {
1263                        let point  = origin + dir * t;
1264                        let normal = (point - body.position).normalize_or_zero();
1265                        hits.push(RayHit { body_id: *id, point, normal, distance: t, fraction: t / max_dist });
1266                    }
1267                }
1268                _ => {
1269                    let verts = body.world_vertices();
1270                    if let Some((t, normal)) = RayCaster::cast_vs_polygon(origin, dir, max_dist, body.position, body.rotation_matrix(), &body.shape.local_vertices()) {
1271                        let _ = verts;
1272                        hits.push(RayHit { body_id: *id, point: origin + dir * t, normal, distance: t, fraction: t / max_dist });
1273                    }
1274                }
1275            }
1276        }
1277
1278        hits.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
1279        hits
1280    }
1281
1282    /// Query all bodies overlapping a circle.
1283    pub fn query_circle(&self, center: Vec2, radius: f32) -> Vec<BodyId> {
1284        self.bodies.iter()
1285            .filter(|(_, b)| (b.position - center).length() < radius + b.world_aabb().half_extents().length())
1286            .map(|(id, _)| *id)
1287            .collect()
1288    }
1289
1290    /// Apply an explosion impulse: pushes all dynamic bodies within radius.
1291    pub fn explode(&mut self, center: Vec2, radius: f32, force: f32) {
1292        let ids: Vec<BodyId> = self.bodies.keys().copied().collect();
1293        for id in ids {
1294            if let Some(body) = self.bodies.get_mut(&id) {
1295                if body.body_type != BodyType::Dynamic { continue; }
1296                let delta = body.position - center;
1297                let dist  = delta.length();
1298                if dist < radius && dist > 1e-6 {
1299                    let falloff = 1.0 - (dist / radius);
1300                    let impulse = delta.normalize_or_zero() * force * falloff;
1301                    body.apply_impulse(impulse);
1302                }
1303            }
1304        }
1305    }
1306
1307    /// Wake all sleeping bodies.
1308    pub fn wake_all(&mut self) {
1309        for body in self.bodies.values_mut() {
1310            body.sleeping = false;
1311            body.sleep_timer = 0.0;
1312        }
1313    }
1314}
1315
1316impl Default for PhysicsWorld2D {
1317    fn default() -> Self { Self::new() }
1318}
1319
1320// ── Tests ─────────────────────────────────────────────────────────────────────
1321
1322#[cfg(test)]
1323mod tests {
1324    use super::*;
1325
1326    fn circle_body(id: u32, pos: Vec2, r: f32) -> RigidBody2D {
1327        let mut b = RigidBody2D::new(BodyId(id), Shape::Circle { radius: r }, PhysicsMaterial::default());
1328        b.position = pos;
1329        b
1330    }
1331
1332    fn static_box(id: u32, pos: Vec2, hw: f32, hh: f32) -> RigidBody2D {
1333        let mut b = RigidBody2D::static_body(BodyId(id), Shape::Box { half_w: hw, half_h: hh });
1334        b.position = pos;
1335        b
1336    }
1337
1338    #[test]
1339    fn test_circle_circle_collision() {
1340        let hit = Sat::test_circle_circle(Vec2::ZERO, 1.0, Vec2::new(1.5, 0.0), 1.0);
1341        assert!(hit.is_some(), "overlapping circles should collide");
1342        let cp = hit.unwrap();
1343        assert!(cp.depth > 0.0);
1344        assert!((cp.depth - 0.5).abs() < 0.01);
1345    }
1346
1347    #[test]
1348    fn test_circle_circle_no_collision() {
1349        let hit = Sat::test_circle_circle(Vec2::ZERO, 0.5, Vec2::new(2.0, 0.0), 0.5);
1350        assert!(hit.is_none(), "separated circles should not collide");
1351    }
1352
1353    #[test]
1354    fn test_sat_box_box() {
1355        let va = [Vec2::new(-1.0,-1.0), Vec2::new(1.0,-1.0), Vec2::new(1.0,1.0), Vec2::new(-1.0,1.0)];
1356        let vb = [Vec2::new(-1.0,-1.0), Vec2::new(1.0,-1.0), Vec2::new(1.0,1.0), Vec2::new(-1.0,1.0)];
1357        // Overlapping boxes at offset 1.5 — should collide
1358        let result = Sat::test_polygon_polygon(
1359            Vec2::ZERO, Mat2::IDENTITY, &va,
1360            Vec2::new(1.5, 0.0), Mat2::IDENTITY, &vb,
1361        );
1362        assert!(result.is_some());
1363    }
1364
1365    #[test]
1366    fn test_sat_box_box_separated() {
1367        let va = [Vec2::new(-0.5,-0.5), Vec2::new(0.5,-0.5), Vec2::new(0.5,0.5), Vec2::new(-0.5,0.5)];
1368        let vb = va;
1369        let result = Sat::test_polygon_polygon(
1370            Vec2::ZERO, Mat2::IDENTITY, &va,
1371            Vec2::new(3.0, 0.0), Mat2::IDENTITY, &vb,
1372        );
1373        assert!(result.is_none());
1374    }
1375
1376    #[test]
1377    fn test_rigid_body_gravity() {
1378        let mut world = PhysicsWorld2D::new();
1379        let id = world.add_body(circle_body(1, Vec2::new(0.0, 10.0), 0.5));
1380        let y0 = world.get_body(id).unwrap().position.y;
1381        world.step(1.0);
1382        let y1 = world.get_body(id).unwrap().position.y;
1383        assert!(y1 < y0, "body should fall under gravity");
1384    }
1385
1386    #[test]
1387    fn test_static_body_no_move() {
1388        let mut world = PhysicsWorld2D::new();
1389        let id = world.add_body(static_box(1, Vec2::ZERO, 5.0, 0.5));
1390        let p0 = world.get_body(id).unwrap().position;
1391        world.step(1.0);
1392        let p1 = world.get_body(id).unwrap().position;
1393        assert_eq!(p0, p1, "static body should not move");
1394    }
1395
1396    #[test]
1397    fn test_collision_response() {
1398        let mut world = PhysicsWorld2D::new();
1399        // Floor
1400        world.add_body(static_box(1, Vec2::new(0.0, -5.0), 10.0, 0.5));
1401        // Ball falling onto floor
1402        let ball_id = world.add_body(circle_body(2, Vec2::new(0.0, -4.0), 0.5));
1403
1404        // Run simulation
1405        for _ in 0..60 {
1406            world.step(1.0 / 60.0);
1407        }
1408
1409        let ball = world.get_body(ball_id).unwrap();
1410        // Ball should be resting on floor: y ≈ -4.5 (floor top at -4.5, ball radius 0.5)
1411        assert!(ball.position.y > -5.5, "ball should not fall through floor");
1412    }
1413
1414    #[test]
1415    fn test_raycast_hits_circle() {
1416        let mut world = PhysicsWorld2D::new();
1417        world.add_body(circle_body(1, Vec2::new(5.0, 0.0), 1.0));
1418        let hits = world.raycast(Vec2::ZERO, Vec2::X, 20.0);
1419        assert!(!hits.is_empty(), "ray should hit the circle");
1420        assert!((hits[0].distance - 4.0).abs() < 0.1);
1421    }
1422
1423    #[test]
1424    fn test_raycast_misses() {
1425        let mut world = PhysicsWorld2D::new();
1426        world.add_body(circle_body(1, Vec2::new(0.0, 5.0), 1.0));
1427        let hits = world.raycast(Vec2::ZERO, Vec2::X, 20.0);
1428        assert!(hits.is_empty(), "horizontal ray should miss circle above");
1429    }
1430
1431    #[test]
1432    fn test_aabb_overlap() {
1433        let a = Aabb2::new(Vec2::ZERO, Vec2::ONE);
1434        let b = Aabb2::new(Vec2::new(0.5, 0.5), Vec2::new(1.5, 1.5));
1435        assert!(a.overlaps(&b));
1436        let c = Aabb2::new(Vec2::new(2.0, 0.0), Vec2::new(3.0, 1.0));
1437        assert!(!a.overlaps(&c));
1438    }
1439
1440    #[test]
1441    fn test_explode_pushes_bodies() {
1442        let mut world = PhysicsWorld2D::zero_gravity();
1443        let id = world.add_body(circle_body(1, Vec2::new(2.0, 0.0), 0.5));
1444        world.explode(Vec2::ZERO, 5.0, 10.0);
1445        let body = world.get_body(id).unwrap();
1446        assert!(body.linear_velocity.length() > 0.0, "explosion should impart velocity");
1447    }
1448
1449    #[test]
1450    fn test_distance_joint() {
1451        let mut world = PhysicsWorld2D::zero_gravity();
1452        let a = world.add_body(circle_body(1, Vec2::ZERO, 0.2));
1453        let b = world.add_body(circle_body(2, Vec2::new(2.0, 0.0), 0.2));
1454        world.add_joint(Joint::Distance {
1455            id: JointId(0),
1456            body_a: a, body_b: b,
1457            anchor_a: Vec2::ZERO, anchor_b: Vec2::ZERO,
1458            rest_length: 2.0,
1459            stiffness: 100.0,
1460            damping: 1.0,
1461        });
1462        world.step(0.1);
1463        // Both bodies should still be roughly separated by ~2 units
1464        let pa = world.get_body(a).unwrap().position;
1465        let pb = world.get_body(b).unwrap().position;
1466        assert!((pa - pb).length() < 3.0);
1467    }
1468
1469    #[test]
1470    fn test_ccd_fast_body() {
1471        let mut b = circle_body(1, Vec2::ZERO, 0.1);
1472        b.linear_velocity = Vec2::new(1000.0, 0.0); // Very fast
1473        assert!(Ccd::needs_ccd(&b));
1474        b.linear_velocity = Vec2::new(0.1, 0.0); // Slow
1475        assert!(!Ccd::needs_ccd(&b));
1476    }
1477
1478    #[test]
1479    fn test_sweep_circles() {
1480        // Two circles approaching each other
1481        let toi = Ccd::sweep_circles(
1482            Vec2::ZERO,       Vec2::new(1.0, 0.0), 0.5,
1483            Vec2::new(2.0, 0.0), Vec2::new(1.0, 0.0), 0.5,
1484        );
1485        assert!(toi.is_some());
1486    }
1487
1488    #[test]
1489    fn test_polygon_area() {
1490        let verts = vec![
1491            Vec2::new(0.0, 0.0),
1492            Vec2::new(1.0, 0.0),
1493            Vec2::new(1.0, 1.0),
1494            Vec2::new(0.0, 1.0),
1495        ];
1496        let area = polygon_area(&verts).abs();
1497        assert!((area - 1.0).abs() < 0.001);
1498    }
1499}