1use crate::{
4 primitives::{
5 Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment,
6 Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus,
7 Segment2d, Triangle2d,
8 },
9 Dir2, Mat2, Rot2, Vec2,
10};
11use std::f32::consts::{FRAC_PI_2, PI, TAU};
12
13use smallvec::SmallVec;
14
15use super::{Aabb2d, Bounded2d, BoundingCircle};
16
17impl Bounded2d for Circle {
18 fn aabb_2d(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> Aabb2d {
19 Aabb2d::new(translation, Vec2::splat(self.radius))
20 }
21
22 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
23 BoundingCircle::new(translation, self.radius)
24 }
25}
26
27#[inline]
30fn arc_bounding_points(arc: Arc2d, rotation: impl Into<Rot2>) -> SmallVec<[Vec2; 7]> {
31 let mut bounds = SmallVec::<[Vec2; 7]>::new();
34 let rotation = rotation.into();
35 bounds.push(rotation * arc.left_endpoint());
36 bounds.push(rotation * arc.right_endpoint());
37
38 let left_angle = (FRAC_PI_2 + arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
42 let right_angle = (FRAC_PI_2 - arc.half_angle + rotation.as_radians()).rem_euclid(TAU);
43 let inverted = left_angle < right_angle;
44 for extremum in [Vec2::X, Vec2::Y, Vec2::NEG_X, Vec2::NEG_Y] {
45 let angle = extremum.to_angle().rem_euclid(TAU);
46 #[allow(clippy::nonminimal_bool)]
50 if !inverted && angle >= right_angle && angle <= left_angle
51 || inverted && (angle >= right_angle || angle <= left_angle)
52 {
53 bounds.push(extremum * arc.radius);
54 }
55 }
56 bounds
57}
58
59impl Bounded2d for Arc2d {
60 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
61 if self.half_angle >= PI {
63 return Circle::new(self.radius).aabb_2d(translation, rotation);
64 }
65
66 Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation))
67 }
68
69 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
70 if self.is_major() {
72 BoundingCircle::new(translation, self.radius)
75 } else {
76 let center = rotation.into() * self.chord_midpoint();
79 BoundingCircle::new(center + translation, self.half_chord_length())
80 }
81 }
82}
83
84impl Bounded2d for CircularSector {
85 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
86 if self.half_angle() >= PI {
88 return Circle::new(self.radius()).aabb_2d(translation, rotation);
89 }
90
91 let mut bounds = arc_bounding_points(self.arc, rotation);
93 bounds.push(Vec2::ZERO);
94
95 Aabb2d::from_point_cloud(translation, 0.0, &bounds)
96 }
97
98 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
99 if self.arc.is_major() {
100 BoundingCircle::new(translation, self.arc.radius)
103 } else {
104 Triangle2d::new(
110 Vec2::ZERO,
111 self.arc.left_endpoint(),
112 self.arc.right_endpoint(),
113 )
114 .bounding_circle(translation, rotation)
115 }
116 }
117}
118
119impl Bounded2d for CircularSegment {
120 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
121 self.arc.aabb_2d(translation, rotation)
122 }
123
124 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
125 self.arc.bounding_circle(translation, rotation)
126 }
127}
128
129impl Bounded2d for Ellipse {
130 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
131 let rotation: Rot2 = rotation.into();
132
133 let (hw, hh) = (self.half_size.x, self.half_size.y);
143
144 let (alpha_sin, alpha_cos) = rotation.sin_cos();
146
147 let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin);
151
152 let (ux, uy) = (hw * alpha_cos, hw * alpha_sin);
154 let (vx, vy) = (hh * beta_cos, hh * beta_sin);
155
156 let half_size = Vec2::new(ux.hypot(vx), uy.hypot(vy));
157
158 Aabb2d::new(translation, half_size)
159 }
160
161 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
162 BoundingCircle::new(translation, self.semi_major())
163 }
164}
165
166impl Bounded2d for Rhombus {
167 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
168 let rotation_mat = rotation.into();
169
170 let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [
171 rotation_mat * Vec2::new(self.half_diagonals.x, 0.0),
172 rotation_mat * Vec2::new(0.0, self.half_diagonals.y),
173 ];
174 let aabb_half_extent = rotated_x_half_diagonal
175 .abs()
176 .max(rotated_y_half_diagonal.abs());
177
178 Aabb2d {
179 min: -aabb_half_extent + translation,
180 max: aabb_half_extent + translation,
181 }
182 }
183
184 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
185 BoundingCircle::new(translation, self.circumradius())
186 }
187}
188
189impl Bounded2d for Plane2d {
190 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
191 let rotation: Rot2 = rotation.into();
192 let normal = rotation * *self.normal;
193 let facing_x = normal == Vec2::X || normal == Vec2::NEG_X;
194 let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y;
195
196 let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 };
199 let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 };
200 let half_size = Vec2::new(half_width, half_height);
201
202 Aabb2d::new(translation, half_size)
203 }
204
205 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
206 BoundingCircle::new(translation, f32::MAX / 2.0)
207 }
208}
209
210impl Bounded2d for Line2d {
211 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
212 let rotation: Rot2 = rotation.into();
213 let direction = rotation * *self.direction;
214
215 let max = f32::MAX / 2.0;
218 let half_width = if direction.x == 0.0 { 0.0 } else { max };
219 let half_height = if direction.y == 0.0 { 0.0 } else { max };
220 let half_size = Vec2::new(half_width, half_height);
221
222 Aabb2d::new(translation, half_size)
223 }
224
225 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
226 BoundingCircle::new(translation, f32::MAX / 2.0)
227 }
228}
229
230impl Bounded2d for Segment2d {
231 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
232 let rotation: Rot2 = rotation.into();
234 let direction = rotation * *self.direction;
235 let half_size = (self.half_length * direction).abs();
236
237 Aabb2d::new(translation, half_size)
238 }
239
240 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
241 BoundingCircle::new(translation, self.half_length)
242 }
243}
244
245impl<const N: usize> Bounded2d for Polyline2d<N> {
246 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
247 Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
248 }
249
250 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
251 BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
252 }
253}
254
255impl Bounded2d for BoxedPolyline2d {
256 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
257 Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
258 }
259
260 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
261 BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
262 }
263}
264
265impl Bounded2d for Triangle2d {
266 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
267 let rotation: Rot2 = rotation.into();
268 let [a, b, c] = self.vertices.map(|vtx| rotation * vtx);
269
270 let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y));
271 let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y));
272
273 Aabb2d {
274 min: min + translation,
275 max: max + translation,
276 }
277 }
278
279 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
280 let rotation: Rot2 = rotation.into();
281 let [a, b, c] = self.vertices;
282
283 let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 {
285 Some((b, c))
286 } else if (c - b).dot(a - b) <= 0.0 {
287 Some((c, a))
288 } else if (a - c).dot(b - c) <= 0.0 {
289 Some((a, b))
290 } else {
291 None
293 };
294
295 if let Some((point1, point2)) = side_opposite_to_non_acute {
298 let (segment, center) = Segment2d::from_points(point1, point2);
301 segment.bounding_circle(rotation * center + translation, rotation)
302 } else {
303 let (Circle { radius }, circumcenter) = self.circumcircle();
305 BoundingCircle::new(rotation * circumcenter + translation, radius)
306 }
307 }
308}
309
310impl Bounded2d for Rectangle {
311 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
312 let rotation: Rot2 = rotation.into();
313
314 let (sin, cos) = rotation.sin_cos();
317 let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]);
318 let half_size = abs_rot_mat * self.half_size;
319
320 Aabb2d::new(translation, half_size)
321 }
322
323 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
324 let radius = self.half_size.length();
325 BoundingCircle::new(translation, radius)
326 }
327}
328
329impl<const N: usize> Bounded2d for Polygon<N> {
330 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
331 Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
332 }
333
334 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
335 BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
336 }
337}
338
339impl Bounded2d for BoxedPolygon {
340 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
341 Aabb2d::from_point_cloud(translation, rotation, &self.vertices)
342 }
343
344 fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rot2>) -> BoundingCircle {
345 BoundingCircle::from_point_cloud(translation, rotation, &self.vertices)
346 }
347}
348
349impl Bounded2d for RegularPolygon {
350 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
351 let rotation: Rot2 = rotation.into();
352
353 let mut min = Vec2::ZERO;
354 let mut max = Vec2::ZERO;
355
356 for vertex in self.vertices(rotation.as_radians()) {
357 min = min.min(vertex);
358 max = max.max(vertex);
359 }
360
361 Aabb2d {
362 min: min + translation,
363 max: max + translation,
364 }
365 }
366
367 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
368 BoundingCircle::new(translation, self.circumcircle.radius)
369 }
370}
371
372impl Bounded2d for Capsule2d {
373 fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rot2>) -> Aabb2d {
374 let rotation: Rot2 = rotation.into();
375
376 let segment = Segment2d {
378 direction: rotation * Dir2::Y,
380 half_length: self.half_length,
381 };
382 let (a, b) = (segment.point1(), segment.point2());
383
384 let min = a.min(b) - Vec2::splat(self.radius);
386 let max = a.max(b) + Vec2::splat(self.radius);
387
388 Aabb2d {
389 min: min + translation,
390 max: max + translation,
391 }
392 }
393
394 fn bounding_circle(&self, translation: Vec2, _rotation: impl Into<Rot2>) -> BoundingCircle {
395 BoundingCircle::new(translation, self.radius + self.half_length)
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU};
402
403 use approx::assert_abs_diff_eq;
404 use glam::Vec2;
405
406 use crate::{
407 bounding::Bounded2d,
408 primitives::{
409 Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d,
410 Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d,
411 },
412 Dir2,
413 };
414
415 #[test]
416 fn circle() {
417 let circle = Circle { radius: 1.0 };
418 let translation = Vec2::new(2.0, 1.0);
419
420 let aabb = circle.aabb_2d(translation, 0.0);
421 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
422 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
423
424 let bounding_circle = circle.bounding_circle(translation, 0.0);
425 assert_eq!(bounding_circle.center, translation);
426 assert_eq!(bounding_circle.radius(), 1.0);
427 }
428
429 #[test]
430 fn arc_and_segment() {
432 struct TestCase {
433 name: &'static str,
434 arc: Arc2d,
435 translation: Vec2,
436 rotation: f32,
437 aabb_min: Vec2,
438 aabb_max: Vec2,
439 bounding_circle_center: Vec2,
440 bounding_circle_radius: f32,
441 }
442
443 let apothem = f32::sqrt(3.0) / 2.0;
445 let tests = [
446 TestCase {
448 name: "1/6th circle untransformed",
449 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
450 translation: Vec2::ZERO,
451 rotation: 0.0,
452 aabb_min: Vec2::new(-0.5, apothem),
453 aabb_max: Vec2::new(0.5, 1.0),
454 bounding_circle_center: Vec2::new(0.0, apothem),
455 bounding_circle_radius: 0.5,
456 },
457 TestCase {
459 name: "1/6th circle with radius 0.5",
460 arc: Arc2d::from_radians(0.5, FRAC_PI_3),
461 translation: Vec2::ZERO,
462 rotation: 0.0,
463 aabb_min: Vec2::new(-0.25, apothem / 2.0),
464 aabb_max: Vec2::new(0.25, 0.5),
465 bounding_circle_center: Vec2::new(0.0, apothem / 2.0),
466 bounding_circle_radius: 0.25,
467 },
468 TestCase {
470 name: "1/6th circle with radius 2.0",
471 arc: Arc2d::from_radians(2.0, FRAC_PI_3),
472 translation: Vec2::ZERO,
473 rotation: 0.0,
474 aabb_min: Vec2::new(-1.0, 2.0 * apothem),
475 aabb_max: Vec2::new(1.0, 2.0),
476 bounding_circle_center: Vec2::new(0.0, 2.0 * apothem),
477 bounding_circle_radius: 1.0,
478 },
479 TestCase {
481 name: "1/6th circle translated",
482 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
483 translation: Vec2::new(2.0, 3.0),
484 rotation: 0.0,
485 aabb_min: Vec2::new(1.5, 3.0 + apothem),
486 aabb_max: Vec2::new(2.5, 4.0),
487 bounding_circle_center: Vec2::new(2.0, 3.0 + apothem),
488 bounding_circle_radius: 0.5,
489 },
490 TestCase {
492 name: "1/6th circle rotated",
493 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
494 translation: Vec2::ZERO,
495 rotation: FRAC_PI_6,
497 aabb_min: Vec2::new(-apothem, 0.5),
498 aabb_max: Vec2::new(0.0, 1.0),
499 bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.powi(2)),
503 bounding_circle_radius: 0.5,
504 },
505 TestCase {
507 name: "1/4er circle rotated to be axis-aligned",
508 arc: Arc2d::from_radians(1.0, FRAC_PI_2),
509 translation: Vec2::ZERO,
510 rotation: -FRAC_PI_4,
512 aabb_min: Vec2::ZERO,
513 aabb_max: Vec2::splat(1.0),
514 bounding_circle_center: Vec2::splat(0.5),
515 bounding_circle_radius: f32::sqrt(2.0) / 2.0,
516 },
517 TestCase {
519 name: "5/6th circle untransformed",
520 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
521 translation: Vec2::ZERO,
522 rotation: 0.0,
523 aabb_min: Vec2::new(-1.0, -apothem),
524 aabb_max: Vec2::new(1.0, 1.0),
525 bounding_circle_center: Vec2::ZERO,
526 bounding_circle_radius: 1.0,
527 },
528 TestCase {
530 name: "5/6th circle translated",
531 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
532 translation: Vec2::new(2.0, 3.0),
533 rotation: 0.0,
534 aabb_min: Vec2::new(1.0, 3.0 - apothem),
535 aabb_max: Vec2::new(3.0, 4.0),
536 bounding_circle_center: Vec2::new(2.0, 3.0),
537 bounding_circle_radius: 1.0,
538 },
539 TestCase {
541 name: "5/6th circle rotated",
542 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
543 translation: Vec2::ZERO,
544 rotation: FRAC_PI_6,
546 aabb_min: Vec2::new(-1.0, -1.0),
547 aabb_max: Vec2::new(1.0, 1.0),
548 bounding_circle_center: Vec2::ZERO,
549 bounding_circle_radius: 1.0,
550 },
551 ];
552
553 for test in tests {
554 println!("subtest case: {}", test.name);
555 let segment: CircularSegment = test.arc.into();
556
557 let arc_aabb = test.arc.aabb_2d(test.translation, test.rotation);
558 assert_abs_diff_eq!(test.aabb_min, arc_aabb.min);
559 assert_abs_diff_eq!(test.aabb_max, arc_aabb.max);
560 let segment_aabb = segment.aabb_2d(test.translation, test.rotation);
561 assert_abs_diff_eq!(test.aabb_min, segment_aabb.min);
562 assert_abs_diff_eq!(test.aabb_max, segment_aabb.max);
563
564 let arc_bounding_circle = test.arc.bounding_circle(test.translation, test.rotation);
565 assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center);
566 assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius());
567 let segment_bounding_circle = segment.bounding_circle(test.translation, test.rotation);
568 assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center);
569 assert_abs_diff_eq!(
570 test.bounding_circle_radius,
571 segment_bounding_circle.radius()
572 );
573 }
574 }
575
576 #[test]
577 fn circular_sector() {
578 struct TestCase {
579 name: &'static str,
580 arc: Arc2d,
581 translation: Vec2,
582 rotation: f32,
583 aabb_min: Vec2,
584 aabb_max: Vec2,
585 bounding_circle_center: Vec2,
586 bounding_circle_radius: f32,
587 }
588
589 let apothem = f32::sqrt(3.0) / 2.0;
591 let inv_sqrt_3 = f32::sqrt(3.0).recip();
592 let tests = [
593 TestCase {
595 name: "1/3rd circle",
596 arc: Arc2d::from_radians(1.0, TAU / 3.0),
597 translation: Vec2::ZERO,
598 rotation: 0.0,
599 aabb_min: Vec2::new(-apothem, 0.0),
600 aabb_max: Vec2::new(apothem, 1.0),
601 bounding_circle_center: Vec2::new(0.0, 0.5),
602 bounding_circle_radius: apothem,
603 },
604 TestCase {
606 name: "1/6th circle untransformed",
607 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
608 translation: Vec2::ZERO,
609 rotation: 0.0,
610 aabb_min: Vec2::new(-0.5, 0.0),
611 aabb_max: Vec2::new(0.5, 1.0),
612 bounding_circle_center: Vec2::new(0.0, inv_sqrt_3),
615 bounding_circle_radius: inv_sqrt_3,
616 },
617 TestCase {
618 name: "1/6th circle with radius 0.5",
619 arc: Arc2d::from_radians(0.5, FRAC_PI_3),
620 translation: Vec2::ZERO,
621 rotation: 0.0,
622 aabb_min: Vec2::new(-0.25, 0.0),
623 aabb_max: Vec2::new(0.25, 0.5),
624 bounding_circle_center: Vec2::new(0.0, inv_sqrt_3 / 2.0),
625 bounding_circle_radius: inv_sqrt_3 / 2.0,
626 },
627 TestCase {
628 name: "1/6th circle with radius 2.0",
629 arc: Arc2d::from_radians(2.0, FRAC_PI_3),
630 translation: Vec2::ZERO,
631 rotation: 0.0,
632 aabb_min: Vec2::new(-1.0, 0.0),
633 aabb_max: Vec2::new(1.0, 2.0),
634 bounding_circle_center: Vec2::new(0.0, 2.0 * inv_sqrt_3),
635 bounding_circle_radius: 2.0 * inv_sqrt_3,
636 },
637 TestCase {
638 name: "1/6th circle translated",
639 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
640 translation: Vec2::new(2.0, 3.0),
641 rotation: 0.0,
642 aabb_min: Vec2::new(1.5, 3.0),
643 aabb_max: Vec2::new(2.5, 4.0),
644 bounding_circle_center: Vec2::new(2.0, 3.0 + inv_sqrt_3),
645 bounding_circle_radius: inv_sqrt_3,
646 },
647 TestCase {
648 name: "1/6th circle rotated",
649 arc: Arc2d::from_radians(1.0, FRAC_PI_3),
650 translation: Vec2::ZERO,
651 rotation: FRAC_PI_6,
653 aabb_min: Vec2::new(-apothem, 0.0),
654 aabb_max: Vec2::new(0.0, 1.0),
655 bounding_circle_center: Vec2::new(-inv_sqrt_3 / 2.0, 0.5),
657 bounding_circle_radius: inv_sqrt_3,
658 },
659 TestCase {
660 name: "1/4er circle rotated to be axis-aligned",
661 arc: Arc2d::from_radians(1.0, FRAC_PI_2),
662 translation: Vec2::ZERO,
663 rotation: -FRAC_PI_4,
665 aabb_min: Vec2::ZERO,
666 aabb_max: Vec2::splat(1.0),
667 bounding_circle_center: Vec2::splat(0.5),
668 bounding_circle_radius: f32::sqrt(2.0) / 2.0,
669 },
670 TestCase {
671 name: "5/6th circle untransformed",
672 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
673 translation: Vec2::ZERO,
674 rotation: 0.0,
675 aabb_min: Vec2::new(-1.0, -apothem),
676 aabb_max: Vec2::new(1.0, 1.0),
677 bounding_circle_center: Vec2::ZERO,
678 bounding_circle_radius: 1.0,
679 },
680 TestCase {
681 name: "5/6th circle translated",
682 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
683 translation: Vec2::new(2.0, 3.0),
684 rotation: 0.0,
685 aabb_min: Vec2::new(1.0, 3.0 - apothem),
686 aabb_max: Vec2::new(3.0, 4.0),
687 bounding_circle_center: Vec2::new(2.0, 3.0),
688 bounding_circle_radius: 1.0,
689 },
690 TestCase {
691 name: "5/6th circle rotated",
692 arc: Arc2d::from_radians(1.0, 5.0 * FRAC_PI_3),
693 translation: Vec2::ZERO,
694 rotation: FRAC_PI_6,
696 aabb_min: Vec2::new(-1.0, -1.0),
697 aabb_max: Vec2::new(1.0, 1.0),
698 bounding_circle_center: Vec2::ZERO,
699 bounding_circle_radius: 1.0,
700 },
701 ];
702
703 for test in tests {
704 println!("subtest case: {}", test.name);
705 let sector: CircularSector = test.arc.into();
706
707 let aabb = sector.aabb_2d(test.translation, test.rotation);
708 assert_abs_diff_eq!(test.aabb_min, aabb.min);
709 assert_abs_diff_eq!(test.aabb_max, aabb.max);
710
711 let bounding_circle = sector.bounding_circle(test.translation, test.rotation);
712 assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center);
713 assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius());
714 }
715 }
716
717 #[test]
718 fn ellipse() {
719 let ellipse = Ellipse::new(1.0, 0.5);
720 let translation = Vec2::new(2.0, 1.0);
721
722 let aabb = ellipse.aabb_2d(translation, 0.0);
723 assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
724 assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
725
726 let bounding_circle = ellipse.bounding_circle(translation, 0.0);
727 assert_eq!(bounding_circle.center, translation);
728 assert_eq!(bounding_circle.radius(), 1.0);
729 }
730
731 #[test]
732 fn rhombus() {
733 let rhombus = Rhombus::new(2.0, 1.0);
734 let translation = Vec2::new(2.0, 1.0);
735
736 let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
737 assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323));
738 assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068));
739
740 let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4);
741 assert_eq!(bounding_circle.center, translation);
742 assert_eq!(bounding_circle.radius(), 1.0);
743
744 let rhombus = Rhombus::new(0.0, 0.0);
745 let translation = Vec2::new(0.0, 0.0);
746
747 let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
748 assert_eq!(aabb.min, Vec2::new(0.0, 0.0));
749 assert_eq!(aabb.max, Vec2::new(0.0, 0.0));
750
751 let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4);
752 assert_eq!(bounding_circle.center, translation);
753 assert_eq!(bounding_circle.radius(), 0.0);
754 }
755
756 #[test]
757 fn plane() {
758 let translation = Vec2::new(2.0, 1.0);
759
760 let aabb1 = Plane2d::new(Vec2::X).aabb_2d(translation, 0.0);
761 assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
762 assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
763
764 let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(translation, 0.0);
765 assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
766 assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
767
768 let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(translation, 0.0);
769 assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
770 assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
771
772 let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0);
773 assert_eq!(bounding_circle.center, translation);
774 assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
775 }
776
777 #[test]
778 fn line() {
779 let translation = Vec2::new(2.0, 1.0);
780
781 let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(translation, 0.0);
782 assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0));
783 assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0));
784
785 let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(translation, 0.0);
786 assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0));
787 assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0));
788
789 let aabb3 = Line2d {
790 direction: Dir2::from_xy(1.0, 1.0).unwrap(),
791 }
792 .aabb_2d(translation, 0.0);
793 assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0));
794 assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0));
795
796 let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(translation, 0.0);
797 assert_eq!(bounding_circle.center, translation);
798 assert_eq!(bounding_circle.radius(), f32::MAX / 2.0);
799 }
800
801 #[test]
802 fn segment() {
803 let translation = Vec2::new(2.0, 1.0);
804 let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0;
805
806 let aabb = segment.aabb_2d(translation, 0.0);
807 assert_eq!(aabb.min, Vec2::new(1.0, 0.5));
808 assert_eq!(aabb.max, Vec2::new(3.0, 1.5));
809
810 let bounding_circle = segment.bounding_circle(translation, 0.0);
811 assert_eq!(bounding_circle.center, translation);
812 assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5));
813 }
814
815 #[test]
816 fn polyline() {
817 let polyline = Polyline2d::<4>::new([
818 Vec2::ONE,
819 Vec2::new(-1.0, 1.0),
820 Vec2::NEG_ONE,
821 Vec2::new(1.0, -1.0),
822 ]);
823 let translation = Vec2::new(2.0, 1.0);
824
825 let aabb = polyline.aabb_2d(translation, 0.0);
826 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
827 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
828
829 let bounding_circle = polyline.bounding_circle(translation, 0.0);
830 assert_eq!(bounding_circle.center, translation);
831 assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2);
832 }
833
834 #[test]
835 fn acute_triangle() {
836 let acute_triangle =
837 Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0));
838 let translation = Vec2::new(2.0, 1.0);
839
840 let aabb = acute_triangle.aabb_2d(translation, 0.0);
841 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
842 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
843
844 let (Circle { radius }, circumcenter) = acute_triangle.circumcircle();
846 let bounding_circle = acute_triangle.bounding_circle(translation, 0.0);
847 assert_eq!(bounding_circle.center, circumcenter + translation);
848 assert_eq!(bounding_circle.radius(), radius);
849 }
850
851 #[test]
852 fn obtuse_triangle() {
853 let obtuse_triangle = Triangle2d::new(
854 Vec2::new(0.0, 1.0),
855 Vec2::new(-10.0, -1.0),
856 Vec2::new(10.0, -1.0),
857 );
858 let translation = Vec2::new(2.0, 1.0);
859
860 let aabb = obtuse_triangle.aabb_2d(translation, 0.0);
861 assert_eq!(aabb.min, Vec2::new(-8.0, 0.0));
862 assert_eq!(aabb.max, Vec2::new(12.0, 2.0));
863
864 let bounding_circle = obtuse_triangle.bounding_circle(translation, 0.0);
866 assert_eq!(bounding_circle.center, translation - Vec2::Y);
867 assert_eq!(bounding_circle.radius(), 10.0);
868 }
869
870 #[test]
871 fn rectangle() {
872 let rectangle = Rectangle::new(2.0, 1.0);
873 let translation = Vec2::new(2.0, 1.0);
874
875 let aabb = rectangle.aabb_2d(translation, std::f32::consts::FRAC_PI_4);
876 let expected_half_size = Vec2::splat(1.0606601);
877 assert_eq!(aabb.min, translation - expected_half_size);
878 assert_eq!(aabb.max, translation + expected_half_size);
879
880 let bounding_circle = rectangle.bounding_circle(translation, 0.0);
881 assert_eq!(bounding_circle.center, translation);
882 assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5));
883 }
884
885 #[test]
886 fn polygon() {
887 let polygon = Polygon::<4>::new([
888 Vec2::ONE,
889 Vec2::new(-1.0, 1.0),
890 Vec2::NEG_ONE,
891 Vec2::new(1.0, -1.0),
892 ]);
893 let translation = Vec2::new(2.0, 1.0);
894
895 let aabb = polygon.aabb_2d(translation, 0.0);
896 assert_eq!(aabb.min, Vec2::new(1.0, 0.0));
897 assert_eq!(aabb.max, Vec2::new(3.0, 2.0));
898
899 let bounding_circle = polygon.bounding_circle(translation, 0.0);
900 assert_eq!(bounding_circle.center, translation);
901 assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2);
902 }
903
904 #[test]
905 fn regular_polygon() {
906 let regular_polygon = RegularPolygon::new(1.0, 5);
907 let translation = Vec2::new(2.0, 1.0);
908
909 let aabb = regular_polygon.aabb_2d(translation, 0.0);
910 assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6);
911 assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6);
912
913 let bounding_circle = regular_polygon.bounding_circle(translation, 0.0);
914 assert_eq!(bounding_circle.center, translation);
915 assert_eq!(bounding_circle.radius(), 1.0);
916 }
917
918 #[test]
919 fn capsule() {
920 let capsule = Capsule2d::new(0.5, 2.0);
921 let translation = Vec2::new(2.0, 1.0);
922
923 let aabb = capsule.aabb_2d(translation, 0.0);
924 assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5));
925 assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5));
926
927 let bounding_circle = capsule.bounding_circle(translation, 0.0);
928 assert_eq!(bounding_circle.center, translation);
929 assert_eq!(bounding_circle.radius(), 1.5);
930 }
931}