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