1use glam::{Vec2, Mat2};
15use std::collections::HashMap;
16
17pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub struct BodyId(pub u32);
31
32#[derive(Debug, Clone)]
36pub enum Shape {
37 Circle { radius: f32 },
39 Box { half_w: f32, half_h: f32 },
41 Polygon { vertices: Vec<Vec2> },
43 Capsule { half_height: f32, radius: f32 },
45}
46
47impl Shape {
48 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 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 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 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 (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#[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 pub fn union(&self, other: &Aabb2) -> Self {
174 Self { min: self.min.min(other.min), max: self.max.max(other.max) }
175 }
176
177 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#[derive(Debug, Clone, Copy)]
196pub struct PhysicsMaterial {
197 pub restitution: f32, pub friction: f32, pub density: f32, }
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#[derive(Debug, Clone, Copy, PartialEq)]
218pub enum BodyType {
219 Dynamic,
221 Kinematic,
223 Static,
225}
226
227#[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 pub position: Vec2,
238 pub angle: f32,
239 pub linear_velocity: Vec2,
240 pub angular_velocity: f32,
241
242 pub mass: f32,
244 pub inv_mass: f32,
245 pub inertia: f32,
246 pub inv_inertia: f32,
247
248 pub force: Vec2,
250 pub torque: f32,
251
252 pub linear_damping: f32,
254 pub angular_damping: f32,
255
256 pub sleeping: bool,
258 sleep_timer: f32,
259
260 pub user_data: u64,
262 pub collision_layer: u32,
263 pub collision_mask: u32,
264
265 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 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 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#[derive(Debug, Clone)]
427pub struct ContactPoint {
428 pub point: Vec2, pub normal: Vec2, pub depth: f32, pub r_a: Vec2, pub r_b: Vec2, 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
462pub 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 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; } if dist > min_overlap {
502 min_overlap = dist;
503 best_normal = normal;
504 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 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 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 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 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 if best_normal.dot(pos_b - pos_a) < 0.0 { best_normal = -best_normal; }
540
541 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 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; } 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 for &p in b {
582 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 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#[derive(Debug, Clone, Copy, PartialEq)]
621pub struct JointId(pub u32);
622
623#[derive(Debug, Clone)]
625pub enum Joint {
626 Distance {
628 id: JointId,
629 body_a: BodyId,
630 body_b: BodyId,
631 anchor_a: Vec2, anchor_b: Vec2, rest_length: f32,
634 stiffness: f32,
635 damping: f32,
636 },
637 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 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 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 {
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, damping_ratio: f32,
679 },
680 Target {
682 id: JointId,
683 body: BodyId,
684 anchor: Vec2, target: Vec2, 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 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 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 _ => { }
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
810pub struct ImpulseSolver;
813
814impl ImpulseSolver {
815 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 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 manifold.restitution = rest;
854 manifold.friction = fric;
855 let _ = dt; }
857 }
858
859 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 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); let dj = cp.normal_impulse - j0;
882 let impulse_n = cp.normal * dj;
883
884 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 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#[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 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
997pub struct Ccd;
1000
1001impl Ccd {
1002 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 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 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 vel * (1.0 / 60.0) > diag * 0.5
1036 }
1037}
1038
1039pub 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 manifolds: Vec<ContactManifold>,
1052
1053 pub last_contact_count: usize,
1055 pub last_solve_time_us: u64,
1056
1057 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 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 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 for body in self.bodies.values_mut() {
1138 if !body.enabled { continue; }
1139 body.integrate_forces(dt, self.gravity);
1140 }
1141
1142 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 for body in self.bodies.values_mut() {
1151 if !body.enabled { continue; }
1152 body.integrate_velocities(dt);
1153 }
1154
1155 self.manifolds = self.detect_collisions();
1157
1158 for manifold in &mut self.manifolds {
1160 ImpulseSolver::pre_step(manifold, &self.bodies, dt);
1161 }
1162
1163 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 for manifold in &self.manifolds {
1173 ImpulseSolver::correct_positions(manifold, &mut self.bodies);
1174 }
1175
1176 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 let aabb_a = a.world_aabb();
1205 let aabb_b = b.world_aabb();
1206 if !aabb_a.overlaps(&aabb_b) { continue; }
1207
1208 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 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 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 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 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 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#[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 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 world.add_body(static_box(1, Vec2::new(0.0, -5.0), 10.0, 0.5));
1401 let ball_id = world.add_body(circle_body(2, Vec2::new(0.0, -4.0), 0.5));
1403
1404 for _ in 0..60 {
1406 world.step(1.0 / 60.0);
1407 }
1408
1409 let ball = world.get_body(ball_id).unwrap();
1410 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 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); assert!(Ccd::needs_ccd(&b));
1474 b.linear_velocity = Vec2::new(0.1, 0.0); assert!(!Ccd::needs_ccd(&b));
1476 }
1477
1478 #[test]
1479 fn test_sweep_circles() {
1480 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}