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