1use crate::core::math::{Rect, Vec2};
82use crate::ecs::Component;
83
84#[derive(Debug, Clone, PartialEq)]
97pub enum ColliderShape {
98 Circle {
102 radius: f32,
104 },
105
106 Aabb {
111 half_extents: Vec2,
113 },
114
115 Obb {
119 half_extents: Vec2,
121 },
122
123 Capsule {
128 half_height: f32,
130 radius: f32,
132 },
133
134 Polygon {
139 vertices: Vec<Vec2>,
141 },
142}
143
144impl ColliderShape {
145 pub fn type_name(&self) -> &'static str {
147 match self {
148 ColliderShape::Circle { .. } => "Circle",
149 ColliderShape::Aabb { .. } => "AABB",
150 ColliderShape::Obb { .. } => "OBB",
151 ColliderShape::Capsule { .. } => "Capsule",
152 ColliderShape::Polygon { .. } => "Polygon",
153 }
154 }
155
156 pub fn compute_aabb(&self) -> Rect {
161 match self {
162 ColliderShape::Circle { radius } => {
163 Rect::new(-radius, -radius, radius * 2.0, radius * 2.0)
164 }
165 ColliderShape::Aabb { half_extents } | ColliderShape::Obb { half_extents } => {
166 Rect::new(
167 -half_extents.x,
168 -half_extents.y,
169 half_extents.x * 2.0,
170 half_extents.y * 2.0,
171 )
172 }
173 ColliderShape::Capsule {
174 half_height,
175 radius,
176 } => {
177 let width = radius * 2.0;
178 let height = (half_height + radius) * 2.0;
179 Rect::new(-radius, -(half_height + radius), width, height)
180 }
181 ColliderShape::Polygon { vertices } => {
182 if vertices.is_empty() {
183 return Rect::unit();
184 }
185
186 let mut min_x = vertices[0].x;
187 let mut min_y = vertices[0].y;
188 let mut max_x = vertices[0].x;
189 let mut max_y = vertices[0].y;
190
191 for v in vertices.iter().skip(1) {
192 min_x = min_x.min(v.x);
193 min_y = min_y.min(v.y);
194 max_x = max_x.max(v.x);
195 max_y = max_y.max(v.y);
196 }
197
198 Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
199 }
200 }
201 }
202
203 pub fn is_circle(&self) -> bool {
205 matches!(self, ColliderShape::Circle { .. })
206 }
207
208 pub fn is_aabb(&self) -> bool {
210 matches!(self, ColliderShape::Aabb { .. })
211 }
212
213 pub fn is_obb(&self) -> bool {
215 matches!(self, ColliderShape::Obb { .. })
216 }
217
218 pub fn is_capsule(&self) -> bool {
220 matches!(self, ColliderShape::Capsule { .. })
221 }
222
223 pub fn is_polygon(&self) -> bool {
225 matches!(self, ColliderShape::Polygon { .. })
226 }
227
228 pub fn is_valid(&self) -> bool {
235 match self {
236 ColliderShape::Circle { radius } => *radius > 0.0,
237 ColliderShape::Aabb { half_extents } | ColliderShape::Obb { half_extents } => {
238 half_extents.x > 0.0 && half_extents.y > 0.0
239 }
240 ColliderShape::Capsule {
241 half_height,
242 radius,
243 } => *half_height > 0.0 && *radius > 0.0,
244 ColliderShape::Polygon { vertices } => vertices.len() >= 3,
245 }
246 }
247}
248
249impl Default for ColliderShape {
250 fn default() -> Self {
252 ColliderShape::Circle { radius: 1.0 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq)]
284pub struct Collider {
285 shape: ColliderShape,
287
288 restitution: f32,
292
293 friction: f32,
297
298 density: Option<f32>,
303
304 layer: u32,
308
309 mask: u32,
313
314 is_sensor: bool,
317
318 enabled: bool,
320}
321
322impl Collider {
323 pub fn circle(radius: f32) -> Self {
337 Self {
338 shape: ColliderShape::Circle { radius },
339 restitution: 0.3,
340 friction: 0.5,
341 density: None,
342 layer: 0xFFFFFFFF, mask: 0xFFFFFFFF, is_sensor: false,
345 enabled: true,
346 }
347 }
348
349 pub fn aabb(half_extents: Vec2) -> Self {
361 Self {
362 shape: ColliderShape::Aabb { half_extents },
363 restitution: 0.3,
364 friction: 0.5,
365 density: None,
366 layer: 0xFFFFFFFF,
367 mask: 0xFFFFFFFF,
368 is_sensor: false,
369 enabled: true,
370 }
371 }
372
373 pub fn obb(half_extents: Vec2) -> Self {
377 Self {
378 shape: ColliderShape::Obb { half_extents },
379 restitution: 0.3,
380 friction: 0.5,
381 density: None,
382 layer: 0xFFFFFFFF,
383 mask: 0xFFFFFFFF,
384 is_sensor: false,
385 enabled: true,
386 }
387 }
388
389 pub fn capsule(half_height: f32, radius: f32) -> Self {
402 Self {
403 shape: ColliderShape::Capsule {
404 half_height,
405 radius,
406 },
407 restitution: 0.3,
408 friction: 0.5,
409 density: None,
410 layer: 0xFFFFFFFF,
411 mask: 0xFFFFFFFF,
412 is_sensor: false,
413 enabled: true,
414 }
415 }
416
417 pub fn polygon(vertices: Vec<Vec2>) -> Self {
425 assert!(
426 vertices.len() >= 3,
427 "Polygon collider must have at least 3 vertices"
428 );
429 Self {
430 shape: ColliderShape::Polygon { vertices },
431 restitution: 0.3,
432 friction: 0.5,
433 density: None,
434 layer: 0xFFFFFFFF,
435 mask: 0xFFFFFFFF,
436 is_sensor: false,
437 enabled: true,
438 }
439 }
440
441 pub fn with_restitution(mut self, restitution: f32) -> Self {
449 self.restitution = restitution.clamp(0.0, 1.0);
450 self
451 }
452
453 pub fn with_friction(mut self, friction: f32) -> Self {
457 self.friction = friction.max(0.0);
458 self
459 }
460
461 pub fn with_density(mut self, density: f32) -> Self {
465 self.density = Some(density.max(0.0));
466 self
467 }
468
469 pub fn with_layer(mut self, layer: u32) -> Self {
471 self.layer = layer;
472 self
473 }
474
475 pub fn with_mask(mut self, mask: u32) -> Self {
477 self.mask = mask;
478 self
479 }
480
481 pub fn with_is_sensor(mut self, is_sensor: bool) -> Self {
483 self.is_sensor = is_sensor;
484 self
485 }
486
487 pub fn with_enabled(mut self, enabled: bool) -> Self {
489 self.enabled = enabled;
490 self
491 }
492
493 pub fn shape(&self) -> &ColliderShape {
499 &self.shape
500 }
501
502 pub fn restitution(&self) -> f32 {
504 self.restitution
505 }
506
507 pub fn friction(&self) -> f32 {
509 self.friction
510 }
511
512 pub fn density(&self) -> Option<f32> {
514 self.density
515 }
516
517 pub fn layer(&self) -> u32 {
519 self.layer
520 }
521
522 pub fn mask(&self) -> u32 {
524 self.mask
525 }
526
527 pub fn is_sensor(&self) -> bool {
529 self.is_sensor
530 }
531
532 pub fn is_enabled(&self) -> bool {
534 self.enabled
535 }
536
537 pub fn compute_aabb(&self) -> Rect {
539 self.shape.compute_aabb()
540 }
541
542 pub fn can_collide_with(&self, other: &Collider) -> bool {
546 (self.layer & other.mask) != 0 && (other.layer & self.mask) != 0
547 }
548
549 pub fn set_restitution(&mut self, restitution: f32) {
555 self.restitution = restitution.clamp(0.0, 1.0);
556 }
557
558 pub fn set_friction(&mut self, friction: f32) {
560 self.friction = friction.max(0.0);
561 }
562
563 pub fn set_density(&mut self, density: Option<f32>) {
565 self.density = density.map(|d| d.max(0.0));
566 }
567
568 pub fn set_layer(&mut self, layer: u32) {
570 self.layer = layer;
571 }
572
573 pub fn set_mask(&mut self, mask: u32) {
575 self.mask = mask;
576 }
577
578 pub fn set_is_sensor(&mut self, is_sensor: bool) {
580 self.is_sensor = is_sensor;
581 }
582
583 pub fn set_enabled(&mut self, enabled: bool) {
585 self.enabled = enabled;
586 }
587
588 pub fn set_shape(&mut self, shape: ColliderShape) {
590 self.shape = shape;
591 }
592}
593
594impl Default for Collider {
595 fn default() -> Self {
597 Self::circle(0.5)
598 }
599}
600
601impl std::fmt::Display for Collider {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 write!(
604 f,
605 "Collider({}, restitution: {:.2}, friction: {:.2}{}{})",
606 self.shape.type_name(),
607 self.restitution,
608 self.friction,
609 if self.is_sensor { ", sensor" } else { "" },
610 if !self.enabled { ", disabled" } else { "" }
611 )
612 }
613}
614
615impl Component for Collider {}
617
618pub mod aabb {
627 use crate::core::math::{Rect, Vec2};
628 use crate::ecs::components::{ColliderShape, Transform2D};
629
630 pub fn compute_world_aabb(shape: &ColliderShape, transform: &Transform2D) -> Rect {
649 let local_aabb = shape.compute_aabb();
650
651 if matches!(shape, ColliderShape::Circle { .. })
653 || (matches!(shape, ColliderShape::Aabb { .. })
654 && transform.rotation.abs() < f32::EPSILON)
655 {
656 let half_size = local_aabb.size() * 0.5;
658 let scaled_half_size = Vec2::new(
659 half_size.x * transform.scale.x.abs(),
660 half_size.y * transform.scale.y.abs(),
661 );
662 let center = transform.position;
663 return Rect::from_min_max(center - scaled_half_size, center + scaled_half_size);
664 }
665
666 let corners = [
668 Vec2::new(local_aabb.x, local_aabb.y),
669 Vec2::new(local_aabb.x + local_aabb.width, local_aabb.y),
670 Vec2::new(
671 local_aabb.x + local_aabb.width,
672 local_aabb.y + local_aabb.height,
673 ),
674 Vec2::new(local_aabb.x, local_aabb.y + local_aabb.height),
675 ];
676
677 let matrix = transform.matrix();
678 let transformed_corners: Vec<Vec2> = corners
679 .iter()
680 .map(|&corner| matrix.transform_point(corner))
681 .collect();
682
683 let mut min_x = transformed_corners[0].x;
685 let mut min_y = transformed_corners[0].y;
686 let mut max_x = transformed_corners[0].x;
687 let mut max_y = transformed_corners[0].y;
688
689 for corner in &transformed_corners[1..] {
690 min_x = min_x.min(corner.x);
691 min_y = min_y.min(corner.y);
692 max_x = max_x.max(corner.x);
693 max_y = max_y.max(corner.y);
694 }
695
696 Rect::from_min_max(Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
697 }
698
699 #[inline]
703 pub fn overlaps(a: &Rect, b: &Rect) -> bool {
704 a.intersects(b)
705 }
706
707 #[inline]
711 pub fn intersection(a: &Rect, b: &Rect) -> Option<Rect> {
712 a.intersection(b)
713 }
714
715 pub fn expand(aabb: &Rect, margin: f32) -> Rect {
731 Rect::new(
732 aabb.x - margin,
733 aabb.y - margin,
734 aabb.width + margin * 2.0,
735 aabb.height + margin * 2.0,
736 )
737 }
738
739 pub fn merge(a: &Rect, b: &Rect) -> Rect {
756 let min_x = a.x.min(b.x);
757 let min_y = a.y.min(b.y);
758 let max_x = (a.x + a.width).max(b.x + b.width);
759 let max_y = (a.y + a.height).max(b.y + b.height);
760 Rect::from_min_max(Vec2::new(min_x, min_y), Vec2::new(max_x, max_y))
761 }
762
763 #[inline]
765 pub fn contains_point(aabb: &Rect, point: Vec2) -> bool {
766 aabb.contains(point)
767 }
768
769 pub fn raycast(
788 aabb: &Rect,
789 ray_origin: Vec2,
790 ray_direction: Vec2,
791 max_distance: f32,
792 ) -> Option<f32> {
793 let inv_dir = Vec2::new(
795 if ray_direction.x.abs() < f32::EPSILON {
796 f32::INFINITY
797 } else {
798 1.0 / ray_direction.x
799 },
800 if ray_direction.y.abs() < f32::EPSILON {
801 f32::INFINITY
802 } else {
803 1.0 / ray_direction.y
804 },
805 );
806
807 let min = aabb.min();
808 let max = aabb.max();
809
810 let t1 = (min.x - ray_origin.x) * inv_dir.x;
811 let t2 = (max.x - ray_origin.x) * inv_dir.x;
812 let t3 = (min.y - ray_origin.y) * inv_dir.y;
813 let t4 = (max.y - ray_origin.y) * inv_dir.y;
814
815 let tmin = t1.min(t2).max(t3.min(t4)).max(0.0);
816 let tmax = t1.max(t2).min(t3.max(t4)).min(max_distance);
817
818 if tmax >= tmin && tmin <= max_distance {
819 Some(tmin)
820 } else {
821 None
822 }
823 }
824
825 pub fn closest_point(aabb: &Rect, point: Vec2) -> Vec2 {
842 Vec2::new(
843 point.x.clamp(aabb.x, aabb.x + aabb.width),
844 point.y.clamp(aabb.y, aabb.y + aabb.height),
845 )
846 }
847
848 pub fn distance_squared_to_point(aabb: &Rect, point: Vec2) -> f32 {
852 let closest = closest_point(aabb, point);
853 let dx = point.x - closest.x;
854 let dy = point.y - closest.y;
855 dx * dx + dy * dy
856 }
857
858 #[inline]
860 pub fn area(aabb: &Rect) -> f32 {
861 aabb.area()
862 }
863
864 pub fn perimeter(aabb: &Rect) -> f32 {
866 2.0 * (aabb.width + aabb.height)
867 }
868}
869
870#[cfg(test)]
875mod tests {
876 use super::*;
877
878 #[test]
883 fn test_collider_shape_type_names() {
884 assert_eq!(ColliderShape::Circle { radius: 1.0 }.type_name(), "Circle");
885 assert_eq!(
886 ColliderShape::Aabb {
887 half_extents: Vec2::one()
888 }
889 .type_name(),
890 "AABB"
891 );
892 assert_eq!(
893 ColliderShape::Obb {
894 half_extents: Vec2::one()
895 }
896 .type_name(),
897 "OBB"
898 );
899 assert_eq!(
900 ColliderShape::Capsule {
901 half_height: 1.0,
902 radius: 0.5
903 }
904 .type_name(),
905 "Capsule"
906 );
907 assert_eq!(
908 ColliderShape::Polygon {
909 vertices: vec![Vec2::zero(), Vec2::unit_x(), Vec2::unit_y()]
910 }
911 .type_name(),
912 "Polygon"
913 );
914 }
915
916 #[test]
917 fn test_collider_shape_predicates() {
918 let circle = ColliderShape::Circle { radius: 1.0 };
919 assert!(circle.is_circle());
920 assert!(!circle.is_aabb());
921 assert!(!circle.is_obb());
922 assert!(!circle.is_capsule());
923 assert!(!circle.is_polygon());
924
925 let aabb = ColliderShape::Aabb {
926 half_extents: Vec2::one(),
927 };
928 assert!(!aabb.is_circle());
929 assert!(aabb.is_aabb());
930 assert!(!aabb.is_obb());
931 }
932
933 #[test]
934 fn test_collider_shape_is_valid() {
935 assert!(ColliderShape::Circle { radius: 1.0 }.is_valid());
937 assert!(ColliderShape::Aabb {
938 half_extents: Vec2::one()
939 }
940 .is_valid());
941 assert!(ColliderShape::Capsule {
942 half_height: 1.0,
943 radius: 0.5
944 }
945 .is_valid());
946 assert!(ColliderShape::Polygon {
947 vertices: vec![Vec2::zero(), Vec2::unit_x(), Vec2::unit_y()]
948 }
949 .is_valid());
950
951 assert!(!ColliderShape::Circle { radius: 0.0 }.is_valid());
953 assert!(!ColliderShape::Circle { radius: -1.0 }.is_valid());
954 assert!(!ColliderShape::Aabb {
955 half_extents: Vec2::zero()
956 }
957 .is_valid());
958 assert!(!ColliderShape::Polygon {
959 vertices: vec![Vec2::zero(), Vec2::unit_x()]
960 }
961 .is_valid());
962 }
963
964 #[test]
965 fn test_collider_shape_compute_aabb_circle() {
966 let shape = ColliderShape::Circle { radius: 2.0 };
967 let aabb = shape.compute_aabb();
968 assert_eq!(aabb.x, -2.0);
969 assert_eq!(aabb.y, -2.0);
970 assert_eq!(aabb.width, 4.0);
971 assert_eq!(aabb.height, 4.0);
972 }
973
974 #[test]
975 fn test_collider_shape_compute_aabb_box() {
976 let shape = ColliderShape::Aabb {
977 half_extents: Vec2::new(3.0, 2.0),
978 };
979 let aabb = shape.compute_aabb();
980 assert_eq!(aabb.x, -3.0);
981 assert_eq!(aabb.y, -2.0);
982 assert_eq!(aabb.width, 6.0);
983 assert_eq!(aabb.height, 4.0);
984 }
985
986 #[test]
987 fn test_collider_shape_compute_aabb_capsule() {
988 let shape = ColliderShape::Capsule {
989 half_height: 1.0,
990 radius: 0.5,
991 };
992 let aabb = shape.compute_aabb();
993 assert_eq!(aabb.x, -0.5);
994 assert_eq!(aabb.y, -1.5);
995 assert_eq!(aabb.width, 1.0);
996 assert_eq!(aabb.height, 3.0);
997 }
998
999 #[test]
1000 fn test_collider_shape_compute_aabb_polygon() {
1001 let shape = ColliderShape::Polygon {
1002 vertices: vec![
1003 Vec2::new(-1.0, -1.0),
1004 Vec2::new(2.0, 0.0),
1005 Vec2::new(0.0, 3.0),
1006 ],
1007 };
1008 let aabb = shape.compute_aabb();
1009 assert_eq!(aabb.x, -1.0);
1010 assert_eq!(aabb.y, -1.0);
1011 assert_eq!(aabb.width, 3.0);
1012 assert_eq!(aabb.height, 4.0);
1013 }
1014
1015 #[test]
1016 fn test_collider_shape_compute_aabb_empty_polygon() {
1017 let shape = ColliderShape::Polygon { vertices: vec![] };
1018 let aabb = shape.compute_aabb();
1019 assert_eq!(aabb, Rect::unit());
1021 }
1022
1023 #[test]
1024 fn test_collider_shape_default() {
1025 let shape = ColliderShape::default();
1026 assert!(shape.is_circle());
1027 if let ColliderShape::Circle { radius } = shape {
1028 assert_eq!(radius, 1.0);
1029 }
1030 }
1031
1032 #[test]
1037 fn test_collider_circle() {
1038 let collider = Collider::circle(1.5);
1039 assert!(collider.shape().is_circle());
1040 assert_eq!(collider.restitution(), 0.3);
1041 assert_eq!(collider.friction(), 0.5);
1042 assert!(!collider.is_sensor());
1043 assert!(collider.is_enabled());
1044 }
1045
1046 #[test]
1047 fn test_collider_aabb() {
1048 let collider = Collider::aabb(Vec2::new(2.0, 3.0));
1049 assert!(collider.shape().is_aabb());
1050 }
1051
1052 #[test]
1053 fn test_collider_obb() {
1054 let collider = Collider::obb(Vec2::new(2.0, 3.0));
1055 assert!(collider.shape().is_obb());
1056 }
1057
1058 #[test]
1059 fn test_collider_capsule() {
1060 let collider = Collider::capsule(1.0, 0.5);
1061 assert!(collider.shape().is_capsule());
1062 }
1063
1064 #[test]
1065 fn test_collider_polygon() {
1066 let collider = Collider::polygon(vec![Vec2::zero(), Vec2::unit_x(), Vec2::new(0.5, 1.0)]);
1067 assert!(collider.shape().is_polygon());
1068 }
1069
1070 #[test]
1071 #[should_panic(expected = "must have at least 3 vertices")]
1072 fn test_collider_polygon_panics_with_too_few_vertices() {
1073 Collider::polygon(vec![Vec2::zero(), Vec2::unit_x()]);
1074 }
1075
1076 #[test]
1077 fn test_collider_builder_pattern() {
1078 let collider = Collider::circle(1.0)
1079 .with_restitution(0.8)
1080 .with_friction(0.1)
1081 .with_density(2.5)
1082 .with_layer(0b0010)
1083 .with_mask(0b1100)
1084 .with_is_sensor(true)
1085 .with_enabled(false);
1086
1087 assert_eq!(collider.restitution(), 0.8);
1088 assert_eq!(collider.friction(), 0.1);
1089 assert_eq!(collider.density(), Some(2.5));
1090 assert_eq!(collider.layer(), 0b0010);
1091 assert_eq!(collider.mask(), 0b1100);
1092 assert!(collider.is_sensor());
1093 assert!(!collider.is_enabled());
1094 }
1095
1096 #[test]
1097 fn test_collider_restitution_clamping() {
1098 let collider = Collider::circle(1.0).with_restitution(1.5);
1099 assert_eq!(collider.restitution(), 1.0);
1100
1101 let collider = Collider::circle(1.0).with_restitution(-0.5);
1102 assert_eq!(collider.restitution(), 0.0);
1103 }
1104
1105 #[test]
1106 fn test_collider_friction_clamping() {
1107 let collider = Collider::circle(1.0).with_friction(-1.0);
1108 assert_eq!(collider.friction(), 0.0);
1109
1110 let collider = Collider::circle(1.0).with_friction(2.0);
1112 assert_eq!(collider.friction(), 2.0);
1113 }
1114
1115 #[test]
1116 fn test_collider_density_clamping() {
1117 let collider = Collider::circle(1.0).with_density(-1.0);
1118 assert_eq!(collider.density(), Some(0.0));
1119 }
1120
1121 #[test]
1122 fn test_collider_mutators() {
1123 let mut collider = Collider::circle(1.0);
1124
1125 collider.set_restitution(0.9);
1126 assert_eq!(collider.restitution(), 0.9);
1127
1128 collider.set_friction(0.2);
1129 assert_eq!(collider.friction(), 0.2);
1130
1131 collider.set_density(Some(3.0));
1132 assert_eq!(collider.density(), Some(3.0));
1133
1134 collider.set_layer(0b0100);
1135 assert_eq!(collider.layer(), 0b0100);
1136
1137 collider.set_mask(0b1000);
1138 assert_eq!(collider.mask(), 0b1000);
1139
1140 collider.set_is_sensor(true);
1141 assert!(collider.is_sensor());
1142
1143 collider.set_enabled(false);
1144 assert!(!collider.is_enabled());
1145 }
1146
1147 #[test]
1148 fn test_collider_can_collide_with() {
1149 let collider_a = Collider::circle(1.0).with_layer(0b0001).with_mask(0b0010);
1150 let collider_b = Collider::circle(1.0).with_layer(0b0010).with_mask(0b0001);
1151 let collider_c = Collider::circle(1.0).with_layer(0b0100).with_mask(0b1000);
1152
1153 assert!(collider_a.can_collide_with(&collider_b));
1155 assert!(collider_b.can_collide_with(&collider_a));
1156
1157 assert!(!collider_a.can_collide_with(&collider_c));
1159 assert!(!collider_c.can_collide_with(&collider_a));
1160
1161 assert!(!collider_b.can_collide_with(&collider_c));
1163 }
1164
1165 #[test]
1166 fn test_collider_compute_aabb() {
1167 let collider = Collider::circle(2.0);
1168 let aabb = collider.compute_aabb();
1169 assert_eq!(aabb.x, -2.0);
1170 assert_eq!(aabb.y, -2.0);
1171 assert_eq!(aabb.width, 4.0);
1172 assert_eq!(aabb.height, 4.0);
1173 }
1174
1175 #[test]
1176 fn test_collider_set_shape() {
1177 let mut collider = Collider::circle(1.0);
1178 assert!(collider.shape().is_circle());
1179
1180 collider.set_shape(ColliderShape::Aabb {
1181 half_extents: Vec2::one(),
1182 });
1183 assert!(collider.shape().is_aabb());
1184 }
1185
1186 #[test]
1187 fn test_collider_default() {
1188 let collider = Collider::default();
1189 assert!(collider.shape().is_circle());
1190 assert_eq!(collider.restitution(), 0.3);
1191 assert_eq!(collider.friction(), 0.5);
1192 assert!(!collider.is_sensor());
1193 assert!(collider.is_enabled());
1194 }
1195
1196 #[test]
1197 fn test_collider_display() {
1198 let collider = Collider::circle(1.0);
1199 let display = format!("{}", collider);
1200 assert!(display.contains("Circle"));
1201 assert!(display.contains("restitution"));
1202 assert!(display.contains("friction"));
1203
1204 let sensor = Collider::circle(1.0).with_is_sensor(true);
1205 let display = format!("{}", sensor);
1206 assert!(display.contains("sensor"));
1207
1208 let disabled = Collider::circle(1.0).with_enabled(false);
1209 let display = format!("{}", disabled);
1210 assert!(display.contains("disabled"));
1211 }
1212
1213 #[test]
1214 fn test_collider_is_component() {
1215 fn assert_component<T: Component>() {}
1216 assert_component::<Collider>();
1217 }
1218
1219 #[test]
1220 fn test_collider_is_send() {
1221 fn assert_send<T: Send>() {}
1222 assert_send::<Collider>();
1223 }
1224
1225 #[test]
1226 fn test_collider_is_sync() {
1227 fn assert_sync<T: Sync>() {}
1228 assert_sync::<Collider>();
1229 }
1230
1231 #[test]
1232 fn test_collider_clone() {
1233 let collider = Collider::circle(1.0).with_restitution(0.8);
1234 let cloned = collider.clone();
1235 assert_eq!(collider, cloned);
1236 }
1237
1238 #[test]
1239 fn test_collider_shape_clone() {
1240 let shape = ColliderShape::Circle { radius: 1.0 };
1241 let cloned = shape.clone();
1242 assert_eq!(shape, cloned);
1243 }
1244
1245 use super::aabb;
1250 use crate::ecs::components::Transform2D;
1251
1252 #[test]
1253 fn test_aabb_compute_world_aabb_circle_no_rotation() {
1254 let shape = ColliderShape::Circle { radius: 2.0 };
1255 let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1256
1257 let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1258 assert_eq!(world_aabb.center(), Vec2::new(10.0, 20.0));
1259 assert_eq!(world_aabb.width, 4.0);
1260 assert_eq!(world_aabb.height, 4.0);
1261 }
1262
1263 #[test]
1264 fn test_aabb_compute_world_aabb_circle_with_scale() {
1265 let shape = ColliderShape::Circle { radius: 1.0 };
1266 let mut transform = Transform2D::from_position(Vec2::new(5.0, 5.0));
1267 transform.set_scale_uniform(2.0);
1268
1269 let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1270 assert_eq!(world_aabb.center(), Vec2::new(5.0, 5.0));
1271 assert_eq!(world_aabb.width, 4.0); assert_eq!(world_aabb.height, 4.0);
1273 }
1274
1275 #[test]
1276 fn test_aabb_compute_world_aabb_box_no_rotation() {
1277 let shape = ColliderShape::Aabb {
1278 half_extents: Vec2::new(3.0, 2.0),
1279 };
1280 let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1281
1282 let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1283 assert_eq!(world_aabb.center(), Vec2::new(10.0, 20.0));
1284 assert_eq!(world_aabb.width, 6.0);
1285 assert_eq!(world_aabb.height, 4.0);
1286 }
1287
1288 #[test]
1289 fn test_aabb_compute_world_aabb_box_with_rotation() {
1290 let shape = ColliderShape::Obb {
1291 half_extents: Vec2::new(2.0, 1.0),
1292 };
1293 let mut transform = Transform2D::from_position(Vec2::new(0.0, 0.0));
1294 transform.set_rotation(std::f32::consts::PI / 4.0); let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1297
1298 assert!(world_aabb.width > 4.0 && world_aabb.width < 4.5);
1301 assert!(world_aabb.height > 4.0 && world_aabb.height < 4.5);
1302 assert_eq!(world_aabb.center(), Vec2::new(0.0, 0.0));
1303 }
1304
1305 #[test]
1306 fn test_aabb_compute_world_aabb_capsule() {
1307 let shape = ColliderShape::Capsule {
1308 half_height: 1.0,
1309 radius: 0.5,
1310 };
1311 let transform = Transform2D::from_position(Vec2::new(5.0, 10.0));
1312
1313 let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1314 assert_eq!(world_aabb.center(), Vec2::new(5.0, 10.0));
1315 assert_eq!(world_aabb.width, 1.0); assert_eq!(world_aabb.height, 3.0); }
1318
1319 #[test]
1320 fn test_aabb_overlaps() {
1321 let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1322 let b = Rect::new(5.0, 5.0, 10.0, 10.0);
1323 let c = Rect::new(20.0, 20.0, 10.0, 10.0);
1324
1325 assert!(aabb::overlaps(&a, &b));
1326 assert!(aabb::overlaps(&b, &a));
1327 assert!(!aabb::overlaps(&a, &c));
1328 }
1329
1330 #[test]
1331 fn test_aabb_intersection() {
1332 let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1333 let b = Rect::new(5.0, 5.0, 10.0, 10.0);
1334
1335 let intersection = aabb::intersection(&a, &b);
1336 assert!(intersection.is_some());
1337 let rect = intersection.unwrap();
1338 assert_eq!(rect.x, 5.0);
1339 assert_eq!(rect.y, 5.0);
1340 assert_eq!(rect.width, 5.0);
1341 assert_eq!(rect.height, 5.0);
1342 }
1343
1344 #[test]
1345 fn test_aabb_intersection_none() {
1346 let a = Rect::new(0.0, 0.0, 10.0, 10.0);
1347 let b = Rect::new(20.0, 20.0, 10.0, 10.0);
1348
1349 assert!(aabb::intersection(&a, &b).is_none());
1350 }
1351
1352 #[test]
1353 fn test_aabb_expand() {
1354 let aabb = Rect::new(5.0, 5.0, 10.0, 10.0);
1355 let expanded = aabb::expand(&aabb, 2.0);
1356
1357 assert_eq!(expanded.x, 3.0);
1358 assert_eq!(expanded.y, 3.0);
1359 assert_eq!(expanded.width, 14.0);
1360 assert_eq!(expanded.height, 14.0);
1361 }
1362
1363 #[test]
1364 fn test_aabb_expand_negative_margin() {
1365 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1366 let shrunk = aabb::expand(&aabb, -1.0);
1367
1368 assert_eq!(shrunk.x, 1.0);
1369 assert_eq!(shrunk.y, 1.0);
1370 assert_eq!(shrunk.width, 8.0);
1371 assert_eq!(shrunk.height, 8.0);
1372 }
1373
1374 #[test]
1375 fn test_aabb_merge() {
1376 let a = Rect::new(0.0, 0.0, 5.0, 5.0);
1377 let b = Rect::new(3.0, 3.0, 5.0, 5.0);
1378 let merged = aabb::merge(&a, &b);
1379
1380 assert_eq!(merged.x, 0.0);
1381 assert_eq!(merged.y, 0.0);
1382 assert_eq!(merged.width, 8.0);
1383 assert_eq!(merged.height, 8.0);
1384 }
1385
1386 #[test]
1387 fn test_aabb_merge_disjoint() {
1388 let a = Rect::new(0.0, 0.0, 5.0, 5.0);
1389 let b = Rect::new(10.0, 10.0, 5.0, 5.0);
1390 let merged = aabb::merge(&a, &b);
1391
1392 assert_eq!(merged.x, 0.0);
1393 assert_eq!(merged.y, 0.0);
1394 assert_eq!(merged.width, 15.0);
1395 assert_eq!(merged.height, 15.0);
1396 }
1397
1398 #[test]
1399 fn test_aabb_contains_point() {
1400 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1401
1402 assert!(aabb::contains_point(&aabb, Vec2::new(5.0, 5.0)));
1403 assert!(aabb::contains_point(&aabb, Vec2::new(0.0, 0.0)));
1404 assert!(!aabb::contains_point(&aabb, Vec2::new(-1.0, 5.0)));
1405 assert!(!aabb::contains_point(&aabb, Vec2::new(5.0, 11.0)));
1406 }
1407
1408 #[test]
1409 fn test_aabb_raycast_hit() {
1410 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1411 let ray_origin = Vec2::new(-5.0, 5.0);
1412 let ray_direction = Vec2::new(1.0, 0.0);
1413
1414 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1415 assert!(hit.is_some());
1416 let t = hit.unwrap();
1417 assert!((t - 5.0).abs() < 0.001); }
1419
1420 #[test]
1421 fn test_aabb_raycast_miss() {
1422 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1423 let ray_origin = Vec2::new(-5.0, 15.0);
1424 let ray_direction = Vec2::new(1.0, 0.0);
1425
1426 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1427 assert!(hit.is_none());
1428 }
1429
1430 #[test]
1431 fn test_aabb_raycast_from_inside() {
1432 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1433 let ray_origin = Vec2::new(5.0, 5.0);
1434 let ray_direction = Vec2::new(1.0, 0.0);
1435
1436 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1437 assert!(hit.is_some());
1438 assert_eq!(hit.unwrap(), 0.0); }
1440
1441 #[test]
1442 fn test_aabb_raycast_max_distance() {
1443 let aabb = Rect::new(100.0, 0.0, 10.0, 10.0);
1444 let ray_origin = Vec2::new(0.0, 5.0);
1445 let ray_direction = Vec2::new(1.0, 0.0);
1446
1447 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 50.0);
1449 assert!(hit.is_none());
1450
1451 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 200.0);
1453 assert!(hit.is_some());
1454 }
1455
1456 #[test]
1457 fn test_aabb_closest_point_outside() {
1458 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1459 let point = Vec2::new(-5.0, 5.0);
1460
1461 let closest = aabb::closest_point(&aabb, point);
1462 assert_eq!(closest, Vec2::new(0.0, 5.0));
1463 }
1464
1465 #[test]
1466 fn test_aabb_closest_point_inside() {
1467 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1468 let point = Vec2::new(5.0, 5.0);
1469
1470 let closest = aabb::closest_point(&aabb, point);
1471 assert_eq!(closest, point); }
1473
1474 #[test]
1475 fn test_aabb_closest_point_corner() {
1476 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1477 let point = Vec2::new(-5.0, -5.0);
1478
1479 let closest = aabb::closest_point(&aabb, point);
1480 assert_eq!(closest, Vec2::new(0.0, 0.0));
1481 }
1482
1483 #[test]
1484 fn test_aabb_distance_squared_to_point_outside() {
1485 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1486 let point = Vec2::new(-3.0, 0.0);
1487
1488 let dist_sq = aabb::distance_squared_to_point(&aabb, point);
1489 assert_eq!(dist_sq, 9.0); }
1491
1492 #[test]
1493 fn test_aabb_distance_squared_to_point_inside() {
1494 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1495 let point = Vec2::new(5.0, 5.0);
1496
1497 let dist_sq = aabb::distance_squared_to_point(&aabb, point);
1498 assert_eq!(dist_sq, 0.0);
1499 }
1500
1501 #[test]
1502 fn test_aabb_area() {
1503 let aabb = Rect::new(0.0, 0.0, 10.0, 5.0);
1504 assert_eq!(aabb::area(&aabb), 50.0);
1505 }
1506
1507 #[test]
1508 fn test_aabb_perimeter() {
1509 let aabb = Rect::new(0.0, 0.0, 10.0, 5.0);
1510 assert_eq!(aabb::perimeter(&aabb), 30.0); }
1512
1513 #[test]
1514 fn test_aabb_compute_world_aabb_polygon() {
1515 let vertices = vec![
1516 Vec2::new(-1.0, -1.0),
1517 Vec2::new(2.0, 0.0),
1518 Vec2::new(0.0, 3.0),
1519 ];
1520 let shape = ColliderShape::Polygon { vertices };
1521 let transform = Transform2D::from_position(Vec2::new(10.0, 20.0));
1522
1523 let world_aabb = aabb::compute_world_aabb(&shape, &transform);
1524
1525 let expected_min = Vec2::new(9.0, 19.0);
1528 let expected_max = Vec2::new(12.0, 23.0);
1529
1530 assert!((world_aabb.x - expected_min.x).abs() < 0.001);
1531 assert!((world_aabb.y - expected_min.y).abs() < 0.001);
1532 assert!((world_aabb.max().x - expected_max.x).abs() < 0.001);
1533 assert!((world_aabb.max().y - expected_max.y).abs() < 0.001);
1534 }
1535
1536 #[test]
1537 fn test_aabb_raycast_diagonal() {
1538 let aabb = Rect::new(0.0, 0.0, 10.0, 10.0);
1539 let ray_origin = Vec2::new(-5.0, -5.0);
1540 let ray_direction = Vec2::new(1.0, 1.0).normalize();
1541
1542 let hit = aabb::raycast(&aabb, ray_origin, ray_direction, 100.0);
1543 assert!(hit.is_some());
1544
1545 let t = hit.unwrap();
1547 let hit_point = ray_origin + ray_direction * t;
1548 assert!((hit_point.x - 0.0).abs() < 0.1);
1549 assert!((hit_point.y - 0.0).abs() < 0.1);
1550 }
1551
1552 #[test]
1553 fn test_aabb_expand_zero_margin() {
1554 let aabb = Rect::new(5.0, 5.0, 10.0, 10.0);
1555 let expanded = aabb::expand(&aabb, 0.0);
1556
1557 assert_eq!(expanded, aabb);
1558 }
1559}