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