1use core::cmp::Ordering;
2use core::fmt;
3use core::iter::FusedIterator;
4
5use euclid::{Point3D, Size3D, Vector3D};
6use rand::Rng as _;
7
8#[cfg(not(feature = "std"))]
10#[allow(unused_imports)]
11use num_traits::float::FloatCore as _;
12
13use crate::math::{
14 Axis, Cube, Face6, FreeCoordinate, FreePoint, FreeVector, GridAab, GridCoordinate, Octant,
15 lines,
16};
17
18#[doc = include_str!("../serde-warning.md")]
24#[derive(Copy, Clone, PartialEq)]
25pub struct Aab {
26 lower_bounds: FreePoint,
30 upper_bounds: FreePoint,
31}
32
33impl Aab {
34 pub const ZERO: Aab = Aab {
36 lower_bounds: Point3D::new(0., 0., 0.),
37 upper_bounds: Point3D::new(0., 0., 0.),
38 };
39
40 #[inline]
42 #[track_caller]
43 pub fn new(
44 lx: FreeCoordinate,
45 hx: FreeCoordinate,
46 ly: FreeCoordinate,
47 hy: FreeCoordinate,
48 lz: FreeCoordinate,
49 hz: FreeCoordinate,
50 ) -> Self {
51 Self::from_lower_upper(Point3D::new(lx, ly, lz), Point3D::new(hx, hy, hz))
52 }
53
54 #[inline]
58 #[track_caller]
59 pub fn from_lower_upper(
60 lower_bounds: impl Into<FreePoint>,
61 upper_bounds: impl Into<FreePoint>,
62 ) -> Self {
63 let lower_bounds = lower_bounds.into();
64 let upper_bounds = upper_bounds.into();
65 match Self::checked_from_lower_upper(lower_bounds, upper_bounds) {
66 Some(aab) => aab,
67 None => panic!(
68 "invalid AAB points that are misordered or NaN: \
69 lower {lower_bounds:?} upper {upper_bounds:?}"
70 ),
71 }
72 }
73
74 pub(crate) fn checked_from_lower_upper(
79 lower_bounds: FreePoint,
80 upper_bounds: FreePoint,
81 ) -> Option<Self> {
82 if lower_bounds.x <= upper_bounds.x
83 && lower_bounds.y <= upper_bounds.y
84 && lower_bounds.z <= upper_bounds.z
85 {
86 Some(Self {
87 lower_bounds,
88 upper_bounds,
89 })
90 } else {
91 None
92 }
93 }
94
95 #[inline]
97 pub const fn lower_bounds_p(&self) -> FreePoint {
98 self.lower_bounds
99 }
100
101 #[inline]
103 pub const fn upper_bounds_p(&self) -> FreePoint {
104 self.upper_bounds
105 }
106
107 #[inline]
109 pub fn lower_bounds_v(&self) -> FreeVector {
110 self.lower_bounds.to_vector()
111 }
112
113 #[inline]
115 pub fn upper_bounds_v(&self) -> FreeVector {
116 self.upper_bounds.to_vector()
117 }
118
119 #[inline]
125 pub fn face_coordinate(&self, face: Face6) -> FreeCoordinate {
126 match face {
127 Face6::NX => -self.lower_bounds.x,
128 Face6::NY => -self.lower_bounds.y,
129 Face6::NZ => -self.lower_bounds.z,
130 Face6::PX => self.upper_bounds.x,
131 Face6::PY => self.upper_bounds.y,
132 Face6::PZ => self.upper_bounds.z,
133 }
134 }
135
136 #[inline]
146 pub fn size(&self) -> Size3D<FreeCoordinate, Cube> {
147 Size3D::from(self.upper_bounds - self.lower_bounds)
148 }
149
150 #[inline]
160 pub fn center(&self) -> FreePoint {
161 (self.lower_bounds + self.upper_bounds.to_vector()) * 0.5
162 }
163
164 #[inline]
169 #[rustfmt::skip]
170 pub fn corner_point(&self, corner: Octant) -> FreePoint {
171 FreePoint::new(
172 if corner.negative_on_x() { self.lower_bounds.x } else { self.upper_bounds.x },
173 if corner.negative_on_y() { self.lower_bounds.y } else { self.upper_bounds.y },
174 if corner.negative_on_z() { self.lower_bounds.z } else { self.upper_bounds.z },
175 )
176 }
177
178 #[doc(hidden)]
181 #[inline]
182 pub fn corner_points(
183 self,
184 ) -> impl DoubleEndedIterator<Item = FreePoint> + ExactSizeIterator + FusedIterator {
185 let lower = self.lower_bounds;
186 let upper = self.upper_bounds;
187 (0..8).map(move |i| {
190 Point3D::new(
191 if i & 1 == 0 { lower.x } else { upper.x },
192 if i & 2 == 0 { lower.y } else { upper.y },
193 if i & 4 == 0 { lower.z } else { upper.z },
194 )
195 })
196 }
197
198 #[inline]
202 pub fn contains(&self, point: FreePoint) -> bool {
203 for axis in Axis::ALL {
206 if !(self.lower_bounds[axis] <= point[axis] && point[axis] <= self.upper_bounds[axis]) {
207 return false;
208 }
209 }
210 true
211 }
212
213 #[inline]
217 pub fn intersects(&self, other: Aab) -> bool {
218 for axis in Axis::ALL {
219 let intersection_min = self.lower_bounds[axis].max(other.lower_bounds[axis]);
220 let intersection_max = self.upper_bounds[axis].min(other.upper_bounds[axis]);
221 match intersection_min.partial_cmp(&intersection_max) {
222 Some(Ordering::Less | Ordering::Equal) => {}
223 _ => return false,
224 }
225 }
226 true
227 }
228
229 #[inline]
232 #[must_use]
233 pub fn union(self, other: Self) -> Self {
234 Self {
235 lower_bounds: self.lower_bounds.min(other.lower_bounds),
237 upper_bounds: self.upper_bounds.max(other.upper_bounds),
238 }
239 }
240
241 #[inline]
257 #[must_use]
258 pub fn union_point(self, point: FreePoint) -> Self {
259 Self {
260 lower_bounds: point.min(self.lower_bounds),
262 upper_bounds: point.max(self.upper_bounds),
263 }
264 }
265
266 #[allow(clippy::missing_inline_in_public_items)]
269 pub fn random_point(self, rng: &mut impl rand::RngCore) -> FreePoint {
270 FreePoint::new(
271 rng.random_range(self.lower_bounds.x..=self.upper_bounds.x),
272 rng.random_range(self.lower_bounds.y..=self.upper_bounds.y),
273 rng.random_range(self.lower_bounds.z..=self.upper_bounds.z),
274 )
275 }
276
277 #[inline]
281 #[must_use]
282 #[track_caller] pub fn translate(self, offset: FreeVector) -> Self {
284 Self::from_lower_upper(self.lower_bounds + offset, self.upper_bounds + offset)
285 }
286
287 #[must_use]
289 #[inline]
290 pub fn scale(self, scalar: FreeCoordinate) -> Self {
291 Self::from_lower_upper(self.lower_bounds * scalar, self.upper_bounds * scalar)
292 }
293
294 #[must_use]
310 #[inline]
311 pub fn expand(self, distance: FreeCoordinate) -> Self {
312 let distance_vec = Vector3D::splat(distance);
315 match Self::checked_from_lower_upper(
316 self.lower_bounds - distance_vec,
317 self.upper_bounds + distance_vec,
318 ) {
319 Some(aab) => aab,
320 None => {
321 let center = self.center();
322 Aab::from_lower_upper(center, center)
323 }
324 }
325 }
326
327 #[inline]
328 #[doc(hidden)] pub fn leading_corner(&self, direction: FreeVector) -> FreeVector {
330 let mut leading_corner = Vector3D::zero();
331 for axis in Axis::ALL {
332 if direction[axis] >= 0.0 {
333 leading_corner[axis] = self.upper_bounds[axis];
334 } else {
335 leading_corner[axis] = self.lower_bounds[axis];
336 }
337 }
338 leading_corner
339 }
340
341 #[inline]
385 pub fn round_up_to_grid(self) -> GridAab {
386 GridAab::from_lower_upper(
387 self.lower_bounds.map(|c| c.floor() as GridCoordinate),
388 self.upper_bounds.map(|c| c.ceil() as GridCoordinate),
389 )
390 }
391}
392
393impl fmt::Debug for Aab {
394 #[allow(clippy::missing_inline_in_public_items)]
395 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396 let Aab {
397 lower_bounds: l,
398 upper_bounds: u,
399 } = *self;
400 f.debug_tuple("Aab")
401 .field(&(l.x..=u.x))
402 .field(&(l.y..=u.y))
403 .field(&(l.z..=u.z))
404 .finish()
405 }
406}
407
408impl Eq for Aab {}
411
412impl From<Aab> for euclid::Box3D<FreeCoordinate, Cube> {
413 #[inline]
414 fn from(value: Aab) -> Self {
415 euclid::Box3D {
416 min: value.lower_bounds_p(),
417 max: value.upper_bounds_p(),
418 }
419 }
420}
421
422impl lines::Wireframe for Aab {
423 #[inline(never)]
424 fn wireframe_points<E: Extend<[lines::Vertex; 2]>>(&self, output: &mut E) {
425 #[rustfmt::skip]
426 const WIREFRAME: &[[Octant; 2]; 12] = {
427 use Octant::*;
428 &[
429 [Nnn, Nnp], [Npn, Npp], [Pnn, Pnp], [Ppn, Ppp],
430 [Nnn, Npn], [Nnp, Npp], [Pnn, Ppn], [Pnp, Ppp],
431 [Nnn, Pnn], [Nnp, Pnp], [Npn, Ppn], [Npp, Ppp],
432 ]
433 };
434 output.extend(WIREFRAME.iter().map(|&corners| {
435 corners.map(|corner| lines::Vertex {
436 position: self.corner_point(corner),
437 color: None,
438 })
439 }));
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::math::lines::Wireframe as _;
447 use alloc::vec::Vec;
448 use euclid::point3;
449
450 #[test]
451 fn new_wrong_order() {
452 assert_eq!(
453 Aab::checked_from_lower_upper(point3(2., 1., 1.), point3(1., 2., 2.)),
454 None
455 );
456 assert_eq!(
457 Aab::checked_from_lower_upper(point3(1., 2., 1.), point3(2., 1., 2.)),
458 None
459 );
460 assert_eq!(
461 Aab::checked_from_lower_upper(point3(1., 1., 2.), point3(2., 2., 1.)),
462 None
463 );
464 }
465
466 #[test]
467 fn new_nan() {
468 assert_eq!(
469 Aab::checked_from_lower_upper(point3(0., 0., 0.), point3(1., 1., f64::NAN)),
470 None
471 );
472 }
473
474 #[test]
475 #[should_panic = "invalid AAB points that are misordered or NaN: lower (0.0, 0.0, 0.0) upper (1.0, 1.0, NaN)"]
476 fn new_panic_message() {
477 Aab::from_lower_upper([0., 0., 0.], [1., 1., f64::NAN]);
478 }
479
480 #[test]
481 fn debug() {
484 let aab = Aab::new(1.0000001, 2.0, 3.0, 4.0, 5.0, 6.0);
485 assert_eq!(
486 format!("{aab:?}"),
487 "Aab(1.0000001..=2.0, 3.0..=4.0, 5.0..=6.0)"
488 );
489 assert_eq!(
490 format!("{aab:#?}\n"),
491 indoc::indoc! {"
492 Aab(
493 1.0000001..=2.0,
494 3.0..=4.0,
495 5.0..=6.0,
496 )
497 "}
498 );
499 }
500
501 #[test]
502 fn union_point_nan() {
503 assert_eq!(
504 Aab::ZERO.union_point(FreePoint::new(2., f64::NAN, 10.)),
505 Aab::from_lower_upper([0., 0., 0.], [2., 0., 10.]),
506 );
507 }
508
509 #[test]
510 fn expand_nan() {
511 let aab = Aab::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
512 assert_eq!(
513 aab.expand(FreeCoordinate::NAN),
514 Aab::from_lower_upper(aab.center(), aab.center()),
515 );
516 }
517
518 #[test]
519 fn expand_negative_failure() {
520 let aab = Aab::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
521 assert_eq!(
522 aab.expand(-10.0),
523 Aab::from_lower_upper(aab.center(), aab.center()),
524 );
525 }
526
527 #[test]
528 fn expand_negative_success() {
529 let aab = Aab::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
530 assert_eq!(
531 aab.expand(-0.25),
532 Aab::new(1.25, 1.75, 3.25, 3.75, 5.25, 5.75),
533 );
534 }
535
536 #[test]
537 fn expand_inf() {
538 const INF: FreeCoordinate = FreeCoordinate::INFINITY;
539 assert_eq!(
540 Aab::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).expand(INF),
541 Aab::new(-INF, INF, -INF, INF, -INF, INF),
542 );
543 }
544
545 #[test]
546 fn wireframe_smoke_test() {
547 let aab: Aab = Cube::new(1, 2, 3).aab();
548 let mut wireframe: Vec<[lines::Vertex; 2]> = Vec::new();
549 aab.wireframe_points(&mut wireframe);
550 for &lines::Vertex { position, color } in wireframe.iter().flatten() {
551 assert!(color.is_none());
552 assert!(position.x == 1.0 || position.x == 2.0);
553 assert!(position.y == 2.0 || position.y == 3.0);
554 assert!(position.z == 3.0 || position.z == 4.0);
555 }
556 }
557
558 #[test]
559 fn leading_corner_consistency() {
560 let aab = Aab::new(-1.1, 2.2, -3.3, 4.4, -5.5, 6.6);
561 for direction in (-1..=1)
562 .zip(-1..=1)
563 .zip(-1..=1)
564 .map(|((x, y), z)| Vector3D::new(x, y, z).map(FreeCoordinate::from))
565 {
566 let leading_corner = aab.leading_corner(direction);
567
568 for axis in Axis::ALL {
569 assert_eq!(leading_corner[axis].signum(), direction[axis].signum());
572 }
573 }
574 }
575
576 #[test]
579 fn corner_points() {
580 assert_eq!(
581 Cube::new(10, 20, 30).aab().corner_points().collect::<Vec<_>>(),
582 vec![
583 point3(10., 20., 30.),
584 point3(11., 20., 30.),
585 point3(10., 21., 30.),
586 point3(11., 21., 30.),
587 point3(10., 20., 31.),
588 point3(11., 20., 31.),
589 point3(10., 21., 31.),
590 point3(11., 21., 31.),
591 ],
592 );
593 }
594
595 #[test]
596 fn aab_to_euclid_box() {
597 let b = euclid::Box3D::from(Aab::from_lower_upper([1., 2., 3.], [4., 5., 6.]));
598 assert_eq!(
599 b,
600 euclid::Box3D::new(point3(1., 2., 3.), point3(4., 5., 6.))
601 );
602 assert!(!b.is_negative());
603 assert!(!b.is_empty());
604 }
605}