1use crate::{
4 Annulus, Arc2d, BoundingVolume, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse,
5 Line2d, Plane2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Ring, Segment2d, Triangle2d,
6};
7use bevy_math::{ops, Dir2, Isometry2d, Mat2, Rot2, Vec2};
8use core::f32::consts::{FRAC_PI_2, PI, TAU};
9
10#[cfg(feature = "alloc")]
11use crate::{ConvexPolygon, Polygon, Polyline2d};
12
13use arrayvec::ArrayVec;
14
15use super::{Aabb2d, Bounded2d, BoundingCircle};
16
17impl Bounded2d for Circle {
18 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
19 let isometry = isometry.into();
20 Aabb2d::new(isometry.translation, Vec2::splat(self.radius))
21 }
22
23 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
24 let isometry = isometry.into();
25 BoundingCircle::new(isometry.translation, self.radius)
26 }
27}
28
29#[inline]
32fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> ArrayVec<Vec2, 7> {
33 let mut bounds = ArrayVec::<Vec2, 7>::new();
36 let rotation = rotation.into();
37 bounds.push(rotation * arc.left_endpoint());
38 bounds.push(rotation * arc.right_endpoint());
39
40 let left_angle = ops::rem_euclid(FRAC_PI_2 + arc.half_angle + rotation.as_radians(), TAU);
44 let right_angle = ops::rem_euclid(FRAC_PI_2 - arc.half_angle + rotation.as_radians(), TAU);
45 let inverted = left_angle < right_angle;
46 for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
47 let angle = ops::rem_euclid(extremum.to_angle(), TAU);
48 let angle_within_parameters = if inverted {
52 angle >= right_angle || angle <= left_angle
53 } else {
54 angle >= right_angle && angle <= left_angle
55 };
56 if angle_within_parameters {
57 bounds.push(extremum * arc.radius);
58 }
59 }
60 bounds
61}
62
63impl Bounded2d for Arc2d {
64 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
65 if self.half_angle >= PI {
67 return Circle::new(self.radius).aabb_2d(isometry);
68 }
69
70 let isometry = isometry.into();
71
72 Aabb2d::from_point_cloud(
73 Isometry2d::from_translation(isometry.translation),
74 &arc_bounding_points(*self, isometry.rotation),
75 )
76 }
77
78 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
79 let isometry = isometry.into();
80
81 if self.is_major() {
83 BoundingCircle::new(isometry.translation, self.radius)
86 } else {
87 let center = isometry.rotation * self.chord_midpoint();
90 BoundingCircle::new(center + isometry.translation, self.half_chord_length())
91 }
92 }
93}
94
95impl Bounded2d for CircularSector {
96 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
97 let isometry = isometry.into();
98
99 if self.half_angle() >= PI {
101 return Circle::new(self.radius()).aabb_2d(isometry);
102 }
103
104 let mut bounds = arc_bounding_points(self.arc, isometry.rotation);
106 bounds.push(Vec2::ZERO);
107
108 Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds)
109 }
110
111 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
112 if self.arc.is_major() {
113 let isometry = isometry.into();
114
115 BoundingCircle::new(isometry.translation, self.arc.radius)
118 } else {
119 Triangle2d::new(
125 Vec2::ZERO,
126 self.arc.left_endpoint(),
127 self.arc.right_endpoint(),
128 )
129 .bounding_circle(isometry)
130 }
131 }
132}
133
134impl Bounded2d for CircularSegment {
135 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
136 self.arc.aabb_2d(isometry)
137 }
138
139 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
140 self.arc.bounding_circle(isometry)
141 }
142}
143
144impl Bounded2d for Ellipse {
145 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
146 let isometry = isometry.into();
147
148 let (hw, hh) = (self.half_size.x, self.half_size.y);
158
159 let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos();
161
162 let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);
166
167 let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);
169 let (vx, vy) = (hh * beta_cos, hh * beta_sin);
170
171 let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy));
172
173 Aabb2d::new(isometry.translation, half_size)
174 }
175
176 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
177 let isometry = isometry.into();
178 BoundingCircle::new(isometry.translation, self.semi_major())
179 }
180}
181
182impl Bounded2d for Annulus {
183 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
184 let isometry = isometry.into();
185 Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius))
186 }
187
188 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
189 let isometry = isometry.into();
190 BoundingCircle::new(isometry.translation, self.outer_circle.radius)
191 }
192}
193
194impl Bounded2d for Rhombus {
195 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
196 let isometry = isometry.into();
197
198 let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [
199 isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0),
200 isometry.rotation * Vec2::new(0.0, self.half_diagonals.y),
201 ];
202 let aabb_half_extent = rotated_x_half_diagonal
203 .abs()
204 .max(rotated_y_half_diagonal.abs());
205
206 Aabb2d {
207 min: -aabb_half_extent + isometry.translation,
208 max: aabb_half_extent + isometry.translation,
209 }
210 }
211
212 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
213 let isometry = isometry.into();
214 BoundingCircle::new(isometry.translation, self.circumradius())
215 }
216}
217
218impl Bounded2d for Plane2d {
219 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
220 let isometry = isometry.into();
221
222 let normal = isometry.rotation * *self.normal;
223 let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;
224 let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;
225
226 let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
229 let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
230 let half_size = Vec2::new(half_width, half_height);
231
232 Aabb2d::new(isometry.translation, half_size)
233 }
234
235 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
236 let isometry = isometry.into();
237 BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
238 }
239}
240
241impl Bounded2d for Line2d {
242 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
243 let isometry = isometry.into();
244
245 let direction = isometry.rotation * *self.direction;
246
247 let max = f32::MAX / 2.0;
250 let half_width = if direction.x == 0.0 { 0.0 } else { max };
251 let half_height = if direction.y == 0.0 { 0.0 } else { max };
252 let half_size = Vec2::new(half_width, half_height);
253
254 Aabb2d::new(isometry.translation, half_size)
255 }
256
257 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
258 let isometry = isometry.into();
259 BoundingCircle::new(isometry.translation, f32::MAX / 2.0)
260 }
261}
262
263impl Bounded2d for Segment2d {
264 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
265 Aabb2d::from_point_cloud(isometry, &[self.point1(), self.point2()])
266 }
267
268 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
269 let isometry: Isometry2d = isometry.into();
270 let local_center = self.center();
271 let radius = local_center.distance(self.point1());
272 let local_circle = BoundingCircle::new(local_center, radius);
273 local_circle.transformed_by(isometry.translation, isometry.rotation)
274 }
275}
276
277#[cfg(feature = "alloc")]
278impl Bounded2d for Polyline2d {
279 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
280 Aabb2d::from_point_cloud(isometry, &self.vertices)
281 }
282
283 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
284 BoundingCircle::from_point_cloud(isometry, &self.vertices)
285 }
286}
287
288impl Bounded2d for Triangle2d {
289 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
290 let isometry = isometry.into();
291 let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * vtx);
292
293 let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));
294 let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));
295
296 Aabb2d {
297 min: min + isometry.translation,
298 max: max + isometry.translation,
299 }
300 }
301
302 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
303 let isometry = isometry.into();
304 let [a, b, c] = self.vertices;
305
306 let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {
308 Some((b, c))
309 } else if (c - b).dot(a - b) <= 0.0 {
310 Some((c, a))
311 } else if (a - c).dot(b - c) <= 0.0 {
312 Some((a, b))
313 } else {
314 None
316 };
317
318 if let Some((point1, point2)) = side_opposite_to_non_acute {
321 let segment = Segment2d::new(point1, point2);
324 segment.bounding_circle(isometry)
325 } else {
326 let (Circle { radius }, circumcenter) = self.circumcircle();
328 BoundingCircle::new(isometry * circumcenter, radius)
329 }
330 }
331}
332
333impl Bounded2d for Rectangle {
334 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
335 let isometry = isometry.into();
336
337 let (sin, cos) = isometry.rotation.sin_cos();
340 let abs_rot_mat =
341 Mat2::from_cols_array(&[ops::abs(cos), ops::abs(sin), ops::abs(sin), ops::abs(cos)]);
342 let half_size = abs_rot_mat * self.half_size;
343
344 Aabb2d::new(isometry.translation, half_size)
345 }
346
347 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
348 let isometry = isometry.into();
349 let radius = self.half_size.length();
350 BoundingCircle::new(isometry.translation, radius)
351 }
352}
353
354#[cfg(feature = "alloc")]
355impl Bounded2d for Polygon {
356 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
357 Aabb2d::from_point_cloud(isometry, &self.vertices)
358 }
359
360 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
361 BoundingCircle::from_point_cloud(isometry, &self.vertices)
362 }
363}
364
365#[cfg(feature = "alloc")]
366impl Bounded2d for ConvexPolygon {
367 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
368 Aabb2d::from_point_cloud(isometry, self.vertices())
369 }
370
371 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
372 BoundingCircle::from_point_cloud(isometry, self.vertices())
373 }
374}
375
376impl Bounded2d for RegularPolygon {
377 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
378 let isometry = isometry.into();
379
380 let mut min = Vec2::ZERO;
381 let mut max = Vec2::ZERO;
382
383 for vertex in self.vertices(isometry.rotation.as_radians()) {
384 min = min.min(vertex);
385 max = max.max(vertex);
386 }
387
388 Aabb2d {
389 min: min + isometry.translation,
390 max: max + isometry.translation,
391 }
392 }
393
394 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
395 let isometry = isometry.into();
396 BoundingCircle::new(isometry.translation, self.circumcircle.radius)
397 }
398}
399
400impl Bounded2d for Capsule2d {
401 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
402 let isometry = isometry.into();
403
404 let segment = Segment2d::from_direction_and_length(
406 isometry.rotation * Dir2::Y,
407 self.half_length * 2.,
408 );
409 let (a, b) = (segment.point1(), segment.point2());
410
411 let min = a.min(b) - Vec2::splat(self.radius);
413 let max = a.max(b) + Vec2::splat(self.radius);
414
415 Aabb2d {
416 min: min + isometry.translation,
417 max: max + isometry.translation,
418 }
419 }
420
421 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
422 let isometry = isometry.into();
423 BoundingCircle::new(isometry.translation, self.radius + self.half_length)
424 }
425}
426
427impl<P: Bounded2d + Primitive2d> Bounded2d for Ring<P> {
428 fn aabb_2d(&self, isometry: impl Into<Isometry2d>) -> Aabb2d {
429 self.outer_shape.aabb_2d(isometry)
430 }
431
432 fn bounding_circle(&self, isometry: impl Into<Isometry2d>) -> BoundingCircle {
433 self.outer_shape.bounding_circle(isometry)
434 }
435}
436
437#[cfg(test)]
438#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
439mod tests {
440 use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
441 use std::println;
442
443 use approx::assert_abs_diff_eq;
444 use bevy_math::{
445 ops::{self, FloatPow},
446 Dir2, Isometry2d, Rot2, Vec2,
447 };
448
449 use crate::{
450 Annulus, Arc2d, Bounded2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse,
451 Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d,
452 Triangle2d,
453 };
454
455 #[test]
456 fn circle() {
457 let circle = Circle { radius: 1.0 };
458 let translation = Vec2::new(2.0, 1.0);
459 let isometry = Isometry2d::from_translation(translation);
460
461 let aabb = circle.aabb_2d(isometry);
462 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
463 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
464
465 let bounding_circle = circle.bounding_circle(isometry);
466 assert_eq!(bounding_circle.center, translation);
467 assert_eq!(bounding_circle.radius(), 1.0);
468 }
469
470 #[test]
471 fn arc_and_segment() {
473 struct TestCase {
474 name: &'static str,
475 arc: Arc2d,
476 translation: Vec2,
477 rotation: f32,
478 aabb_min: Vec2,
479 aabb_max: Vec2,
480 bounding_circle_center: Vec2,
481 bounding_circle_radius: f32,
482 }
483
484 impl TestCase {
485 fn isometry(&self) -> Isometry2d {
486 Isometry2d::new(self.translation, self.rotation.into())
487 }
488 }
489
490 let apothem = ops::sqrt(3.0) / 2.0;
492 let tests = [
493 TestCase {
495 name: "1/6th circle untransformed",
496 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
497 translation: Vec2::ZERO,
498 rotation: 0.0,
499 aabb_min: Vec2::new(-0.5, apothem),
500 aabb_max: Vec2::new(0.5, 1.0),
501 bounding_circle_center: Vec2::new(0.0, apothem),
502 bounding_circle_radius: 0.5,
503 },
504 TestCase {
506 name: "1/6th circle with radius 0.5",
507 arc: Arc2d::from_radians(0.5, FRAC_PI_3),
508 translation: Vec2::ZERO,
509 rotation: 0.0,
510 aabb_min: Vec2::new(-0.25, apothem / 2.0),
511 aabb_max: Vec2::new(0.25, 0.5),
512 bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
513 bounding_circle_radius: 0.25,
514 },
515 TestCase {
517 name: "1/6th circle with radius 2.0",
518 arc: Arc2d::from_radians(2.0, FRAC_PI_3),
519 translation: Vec2::ZERO,
520 rotation: 0.0,
521 aabb_min: Vec2::new(-1.0, 2.0 * apothem),
522 aabb_max: Vec2::new(1.0, 2.0),
523 bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
524 bounding_circle_radius: 1.0,
525 },
526 TestCase {
528 name: "1/6th circle translated",
529 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
530 translation: Vec2::new(2.0, 3.0),
531 rotation: 0.0,
532 aabb_min: Vec2::new(1.5, 3.0 + apothem),
533 aabb_max: Vec2::new(2.5, 4.0),
534 bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
535 bounding_circle_radius: 0.5,
536 },
537 TestCase {
539 name: "1/6th circle rotated",
540 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
541 translation: Vec2::ZERO,
542 rotation: FRAC_PI_6,
544 aabb_min: Vec2::new(-apothem, 0.5),
545 aabb_max: Vec2::new(0.0, 1.0),
546 bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()),
550 bounding_circle_radius: 0.5,
551 },
552 TestCase {
554 name: "1/4er circle rotated to be axis-aligned",
555 arc: Arc2d::from_radians(1.0, FRAC_PI_2),
556 translation: Vec2::ZERO,
557 rotation: -FRAC_PI_4,
559 aabb_min: Vec2::ZERO,
560 aabb_max: Vec2::splat(1.0),
561 bounding_circle_center: Vec2::splat(0.5),
562 bounding_circle_radius: ops::sqrt(2.0) / 2.0,
563 },
564 TestCase {
566 name: "5/6th circle untransformed",
567 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
568 translation: Vec2::ZERO,
569 rotation: 0.0,
570 aabb_min: Vec2::new(-1.0, -apothem),
571 aabb_max: Vec2::new(1.0, 1.0),
572 bounding_circle_center: Vec2::ZERO,
573 bounding_circle_radius: 1.0,
574 },
575 TestCase {
577 name: "5/6th circle translated",
578 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
579 translation: Vec2::new(2.0, 3.0),
580 rotation: 0.0,
581 aabb_min: Vec2::new(1.0, 3.0 - apothem),
582 aabb_max: Vec2::new(3.0, 4.0),
583 bounding_circle_center: Vec2::new(2.0, 3.0),
584 bounding_circle_radius: 1.0,
585 },
586 TestCase {
588 name: "5/6th circle rotated",
589 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
590 translation: Vec2::ZERO,
591 rotation: FRAC_PI_6,
593 aabb_min: Vec2::new(-1.0, -1.0),
594 aabb_max: Vec2::new(1.0, 1.0),
595 bounding_circle_center: Vec2::ZERO,
596 bounding_circle_radius: 1.0,
597 },
598 ];
599
600 for test in tests {
601 #[cfg(feature = "std")]
602 println!("subtest case: {}", test.name);
603 let segment: CircularSegment = test.arc.into();
604
605 let arc_aabb = test.arc.aabb_2d(test.isometry());
606 assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
607 assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
608 let segment_aabb = segment.aabb_2d(test.isometry());
609 assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
610 assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
611
612 let arc_bounding_circle = test.arc.bounding_circle(test.isometry());
613 assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
614 assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
615 let segment_bounding_circle = segment.bounding_circle(test.isometry());
616 assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
617 assert_abs_diff_eq!(
618 test.bounding_circle_radius,
619 segment_bounding_circle.radius()
620 );
621 }
622 }
623
624 #[test]
625 fn circular_sector() {
626 struct TestCase {
627 name: &'static str,
628 arc: Arc2d,
629 translation: Vec2,
630 rotation: f32,
631 aabb_min: Vec2,
632 aabb_max: Vec2,
633 bounding_circle_center: Vec2,
634 bounding_circle_radius: f32,
635 }
636
637 impl TestCase {
638 fn isometry(&self) -> Isometry2d {
639 Isometry2d::new(self.translation, self.rotation.into())
640 }
641 }
642
643 let apothem = ops::sqrt(3.0) / 2.0;
645 let inv_sqrt_3 = ops::sqrt(3.0).recip();
646 let tests = [
647 TestCase {
649 name: "1/3rd circle",
650 arc: Arc2d::from_radians(1.0, TAU / 3.0),
651 translation: Vec2::ZERO,
652 rotation: 0.0,
653 aabb_min: Vec2::new(-apothem, 0.0),
654 aabb_max: Vec2::new(apothem, 1.0),
655 bounding_circle_center: Vec2::new(0.0, 0.5),
656 bounding_circle_radius: apothem,
657 },
658 TestCase {
660 name: "1/6th circle untransformed",
661 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
662 translation: Vec2::ZERO,
663 rotation: 0.0,
664 aabb_min: Vec2::new(-0.5, 0.0),
665 aabb_max: Vec2::new(0.5, 1.0),
666 bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
669 bounding_circle_radius: inv_sqrt_3,
670 },
671 TestCase {
672 name: "1/6th circle with radius 0.5",
673 arc: Arc2d::from_radians(0.5, FRAC_PI_3),
674 translation: Vec2::ZERO,
675 rotation: 0.0,
676 aabb_min: Vec2::new(-0.25, 0.0),
677 aabb_max: Vec2::new(0.25, 0.5),
678 bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
679 bounding_circle_radius: inv_sqrt_3 / 2.0,
680 },
681 TestCase {
682 name: "1/6th circle with radius 2.0",
683 arc: Arc2d::from_radians(2.0, FRAC_PI_3),
684 translation: Vec2::ZERO,
685 rotation: 0.0,
686 aabb_min: Vec2::new(-1.0, 0.0),
687 aabb_max: Vec2::new(1.0, 2.0),
688 bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
689 bounding_circle_radius: 2.0 * inv_sqrt_3,
690 },
691 TestCase {
692 name: "1/6th circle translated",
693 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
694 translation: Vec2::new(2.0, 3.0),
695 rotation: 0.0,
696 aabb_min: Vec2::new(1.5, 3.0),
697 aabb_max: Vec2::new(2.5, 4.0),
698 bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
699 bounding_circle_radius: inv_sqrt_3,
700 },
701 TestCase {
702 name: "1/6th circle rotated",
703 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
704 translation: Vec2::ZERO,
705 rotation: FRAC_PI_6,
707 aabb_min: Vec2::new(-apothem, 0.0),
708 aabb_max: Vec2::new(0.0, 1.0),
709 bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
711 bounding_circle_radius: inv_sqrt_3,
712 },
713 TestCase {
714 name: "1/4er circle rotated to be axis-aligned",
715 arc: Arc2d::from_radians(1.0, FRAC_PI_2),
716 translation: Vec2::ZERO,
717 rotation: -FRAC_PI_4,
719 aabb_min: Vec2::ZERO,
720 aabb_max: Vec2::splat(1.0),
721 bounding_circle_center: Vec2::splat(0.5),
722 bounding_circle_radius: ops::sqrt(2.0) / 2.0,
723 },
724 TestCase {
725 name: "5/6th circle untransformed",
726 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
727 translation: Vec2::ZERO,
728 rotation: 0.0,
729 aabb_min: Vec2::new(-1.0, -apothem),
730 aabb_max: Vec2::new(1.0, 1.0),
731 bounding_circle_center: Vec2::ZERO,
732 bounding_circle_radius: 1.0,
733 },
734 TestCase {
735 name: "5/6th circle translated",
736 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
737 translation: Vec2::new(2.0, 3.0),
738 rotation: 0.0,
739 aabb_min: Vec2::new(1.0, 3.0 - apothem),
740 aabb_max: Vec2::new(3.0, 4.0),
741 bounding_circle_center: Vec2::new(2.0, 3.0),
742 bounding_circle_radius: 1.0,
743 },
744 TestCase {
745 name: "5/6th circle rotated",
746 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
747 translation: Vec2::ZERO,
748 rotation: FRAC_PI_6,
750 aabb_min: Vec2::new(-1.0, -1.0),
751 aabb_max: Vec2::new(1.0, 1.0),
752 bounding_circle_center: Vec2::ZERO,
753 bounding_circle_radius: 1.0,
754 },
755 ];
756
757 for test in tests {
758 #[cfg(feature = "std")]
759 println!("subtest case: {}", test.name);
760 let sector: CircularSector = test.arc.into();
761
762 let aabb = sector.aabb_2d(test.isometry());
763 assert_abs_diff_eq!(test.aabb_min, aabb.min);
764 assert_abs_diff_eq!(test.aabb_max, aabb.max);
765
766 let bounding_circle = sector.bounding_circle(test.isometry());
767 assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
768 assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
769 }
770 }
771
772 #[test]
773 fn ellipse() {
774 let ellipse = Ellipse::new(1.0, 0.5);
775 let translation = Vec2::new(2.0, 1.0);
776 let isometry = Isometry2d::from_translation(translation);
777
778 let aabb = ellipse.aabb_2d(isometry);
779 assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
780 assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
781
782 let bounding_circle = ellipse.bounding_circle(isometry);
783 assert_eq!(bounding_circle.center, translation);
784 assert_eq!(bounding_circle.radius(), 1.0);
785 }
786
787 #[test]
788 fn annulus() {
789 let annulus = Annulus::new(1.0, 2.0);
790 let translation = Vec2::new(2.0, 1.0);
791 let rotation = Rot2::radians(1.0);
792 let isometry = Isometry2d::new(translation, rotation);
793
794 let aabb = annulus.aabb_2d(isometry);
795 assert_eq!(aabb.min, Vec2::new(0.0, -1.0));
796 assert_eq!(aabb.max, Vec2::new(4.0, 3.0));
797
798 let bounding_circle = annulus.bounding_circle(isometry);
799 assert_eq!(bounding_circle.center, translation);
800 assert_eq!(bounding_circle.radius(), 2.0);
801 }
802
803 #[test]
804 fn rhombus() {
805 let rhombus = Rhombus::new(2.0, 1.0);
806 let translation = Vec2::new(2.0, 1.0);
807 let rotation = Rot2::radians(FRAC_PI_4);
808 let isometry = Isometry2d::new(translation, rotation);
809
810 let aabb = rhombus.aabb_2d(isometry);
811 assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
812 assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
813
814 let bounding_circle = rhombus.bounding_circle(isometry);
815 assert_eq!(bounding_circle.center, translation);
816 assert_eq!(bounding_circle.radius(), 1.0);
817
818 let rhombus = Rhombus::new(0.0, 0.0);
819 let translation = Vec2::new(0.0, 0.0);
820 let isometry = Isometry2d::new(translation, rotation);
821
822 let aabb = rhombus.aabb_2d(isometry);
823 assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
824 assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
825
826 let bounding_circle = rhombus.bounding_circle(isometry);
827 assert_eq!(bounding_circle.center, translation);
828 assert_eq!(bounding_circle.radius(), 0.0);
829 }
830
831 #[test]
832 fn plane() {
833 let translation = Vec2::new(2.0, 1.0);
834 let isometry = Isometry2d::from_translation(translation);
835
836 let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry);
837 assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
838 assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
839
840 let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry);
841 assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
842 assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
843
844 let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry);
845 assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
846 assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
847
848 let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry);
849 assert_eq!(bounding_circle.center, translation);
850 assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
851 }
852
853 #[test]
854 fn line() {
855 let translation = Vec2::new(2.0, 1.0);
856 let isometry = Isometry2d::from_translation(translation);
857
858 let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry);
859 assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
860 assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
861
862 let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry);
863 assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
864 assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
865
866 let aabb3 = Line2d {
867 direction: Dir2::from_xy(1.0, 1.0).unwrap(),
868 }
869 .aabb_2d(isometry);
870 assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
871 assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
872
873 let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry);
874 assert_eq!(bounding_circle.center, translation);
875 assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
876 }
877
878 #[test]
879 fn segment() {
880 let segment = Segment2d::new(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5));
881 let translation = Vec2::new(2.0, 1.0);
882 let isometry = Isometry2d::from_translation(translation);
883
884 let aabb = segment.aabb_2d(isometry);
885 assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
886 assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
887
888 let bounding_circle = segment.bounding_circle(isometry);
889 assert_eq!(bounding_circle.center, translation);
890 assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
891 }
892
893 #[test]
894 fn polyline() {
895 let polyline = Polyline2d::new([
896 Vec2::ONE,
897 Vec2::new(-1.0, 1.0),
898 Vec2::NEG_ONE,
899 Vec2::new(1.0, -1.0),
900 ]);
901 let translation = Vec2::new(2.0, 1.0);
902 let isometry = Isometry2d::from_translation(translation);
903
904 let aabb = polyline.aabb_2d(isometry);
905 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
906 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
907
908 let bounding_circle = polyline.bounding_circle(isometry);
909 assert_eq!(bounding_circle.center, translation);
910 assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
911 }
912
913 #[test]
914 fn acute_triangle() {
915 let acute_triangle =
916 Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
917 let translation = Vec2::new(2.0, 1.0);
918 let isometry = Isometry2d::from_translation(translation);
919
920 let aabb = acute_triangle.aabb_2d(isometry);
921 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
922 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
923
924 let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
926 let bounding_circle = acute_triangle.bounding_circle(isometry);
927 assert_eq!(bounding_circle.center, circumcenter + translation);
928 assert_eq!(bounding_circle.radius(), radius);
929 }
930
931 #[test]
932 fn obtuse_triangle() {
933 let obtuse_triangle = Triangle2d::new(
934 Vec2::new(0.0, 1.0),
935 Vec2::new(-10.0, -1.0),
936 Vec2::new(10.0, -1.0),
937 );
938 let translation = Vec2::new(2.0, 1.0);
939 let isometry = Isometry2d::from_translation(translation);
940
941 let aabb = obtuse_triangle.aabb_2d(isometry);
942 assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
943 assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
944
945 let bounding_circle = obtuse_triangle.bounding_circle(isometry);
947 assert_eq!(bounding_circle.center, translation - Vec2::Y);
948 assert_eq!(bounding_circle.radius(), 10.0);
949 }
950
951 #[test]
952 fn rectangle() {
953 let rectangle = Rectangle::new(2.0, 1.0);
954 let translation = Vec2::new(2.0, 1.0);
955
956 let aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(FRAC_PI_4)));
957 let expected_half_size = Vec2::splat(1.0606601);
958 assert_eq!(aabb.min, translation - expected_half_size);
959 assert_eq!(aabb.max, translation + expected_half_size);
960
961 let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation));
962 assert_eq!(bounding_circle.center, translation);
963 assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5));
964 }
965
966 #[test]
967 fn polygon() {
968 let polygon = Polygon::new([
969 Vec2::ONE,
970 Vec2::new(-1.0, 1.0),
971 Vec2::NEG_ONE,
972 Vec2::new(1.0, -1.0),
973 ]);
974 let translation = Vec2::new(2.0, 1.0);
975 let isometry = Isometry2d::from_translation(translation);
976
977 let aabb = polygon.aabb_2d(isometry);
978 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
979 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
980
981 let bounding_circle = polygon.bounding_circle(isometry);
982 assert_eq!(bounding_circle.center, translation);
983 assert_eq!(bounding_circle.radius(), core::f32::consts::SQRT_2);
984 }
985
986 #[test]
987 fn regular_polygon() {
988 let regular_polygon = RegularPolygon::new(1.0, 5);
989 let translation = Vec2::new(2.0, 1.0);
990 let isometry = Isometry2d::from_translation(translation);
991
992 let aabb = regular_polygon.aabb_2d(isometry);
993 assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
994 assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
995
996 let bounding_circle = regular_polygon.bounding_circle(isometry);
997 assert_eq!(bounding_circle.center, translation);
998 assert_eq!(bounding_circle.radius(), 1.0);
999 }
1000
1001 #[test]
1002 fn capsule() {
1003 let capsule = Capsule2d::new(0.5, 2.0);
1004 let translation = Vec2::new(2.0, 1.0);
1005 let isometry = Isometry2d::from_translation(translation);
1006
1007 let aabb = capsule.aabb_2d(isometry);
1008 assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
1009 assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
1010
1011 let bounding_circle = capsule.bounding_circle(isometry);
1012 assert_eq!(bounding_circle.center, translation);
1013 assert_eq!(bounding_circle.radius(), 1.5);
1014 }
1015}