Skip to main content

steel_utils/
geometry.rs

1//! Geometry primitives shared by registry data, physics, and world queries.
2
3use std::marker::PhantomData;
4use std::ops::{Add, Div, Neg, Sub};
5
6use glam::{DVec3, IVec3};
7
8use crate::{BlockPos, axis::Axis};
9
10const fn ordered_pair(a: f64, b: f64) -> (f64, f64) {
11    if a <= b { (a, b) } else { (b, a) }
12}
13
14const fn ordered_pair_i32(a: i32, b: i32) -> (i32, i32) {
15    if a <= b { (a, b) } else { (b, a) }
16}
17
18/// Encodes the edge semantics of a coordinate space.
19pub trait Space {
20    /// Whether `min == max` on an axis means the box has zero extent.
21    const ZERO_SPAN_IS_EMPTY: bool;
22}
23
24/// Marker type for block-local AABBs.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct BlockLocal;
27
28impl Space for BlockLocal {
29    const ZERO_SPAN_IS_EMPTY: bool = true;
30}
31
32/// Marker type for world-space AABBs.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct World;
35
36impl Space for World {
37    const ZERO_SPAN_IS_EMPTY: bool = true;
38}
39
40/// Marker type for integer bounding boxes (structure pieces).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct Structure;
43
44impl Space for Structure {
45    const ZERO_SPAN_IS_EMPTY: bool = false;
46}
47
48/// Generic axis-aligned bounding box.
49///
50/// `T` is the vector type (e.g. [`DVec3`] or [`IVec3`]) and `I` is a marker
51/// that differentiates coordinate spaces (block-local, world, structure).
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct Aabb<T, I> {
54    /// Minimum corner of the box.
55    min: T,
56    /// Maximum corner of the box.
57    max: T,
58    p: PhantomData<I>,
59}
60
61/// Block-local axis-aligned box used by voxel shapes.
62pub type BlockLocalAabb = Aabb<DVec3, BlockLocal>;
63
64/// World-space axis-aligned box used by entity and collision physics.
65pub type WorldAabb = Aabb<DVec3, World>;
66
67/// Integer axis-aligned bounding box for structure pieces.
68pub type BoundingBox = Aabb<IVec3, Structure>;
69
70/// Vector operations used by generic AABB helpers.
71pub trait AabbVector: Copy + Add<Output = Self> + Sub<Output = Self> {
72    /// Scalar component type.
73    type Scalar: Copy
74        + PartialOrd
75        + Add<Output = Self::Scalar>
76        + Sub<Output = Self::Scalar>
77        + Div<Output = Self::Scalar>
78        + Neg<Output = Self::Scalar>
79        + From<u8>;
80
81    /// Creates a vector from individual components.
82    fn new(x: Self::Scalar, y: Self::Scalar, z: Self::Scalar) -> Self;
83
84    /// X component.
85    fn x(self) -> Self::Scalar;
86
87    /// Y component.
88    fn y(self) -> Self::Scalar;
89
90    /// Z component.
91    fn z(self) -> Self::Scalar;
92
93    /// Per-component minimum.
94    #[must_use]
95    fn min(self, other: Self) -> Self;
96
97    /// Per-component maximum.
98    #[must_use]
99    fn max(self, other: Self) -> Self;
100}
101
102impl AabbVector for DVec3 {
103    type Scalar = f64;
104
105    fn new(x: Self::Scalar, y: Self::Scalar, z: Self::Scalar) -> Self {
106        DVec3::new(x, y, z)
107    }
108
109    fn x(self) -> Self::Scalar {
110        self.x
111    }
112
113    fn y(self) -> Self::Scalar {
114        self.y
115    }
116
117    fn z(self) -> Self::Scalar {
118        self.z
119    }
120
121    fn min(self, other: Self) -> Self {
122        self.min(other)
123    }
124
125    fn max(self, other: Self) -> Self {
126        self.max(other)
127    }
128}
129
130impl AabbVector for IVec3 {
131    type Scalar = i32;
132
133    fn new(x: Self::Scalar, y: Self::Scalar, z: Self::Scalar) -> Self {
134        IVec3::new(x, y, z)
135    }
136
137    fn x(self) -> Self::Scalar {
138        self.x
139    }
140
141    fn y(self) -> Self::Scalar {
142        self.y
143    }
144
145    fn z(self) -> Self::Scalar {
146        self.z
147    }
148
149    fn min(self, other: Self) -> Self {
150        self.min(other)
151    }
152
153    fn max(self, other: Self) -> Self {
154        self.max(other)
155    }
156}
157
158impl<T: AabbVector, I> Aabb<T, I> {
159    /// Returns the minimum coordinate on `axis`.
160    #[must_use]
161    pub fn min(&self, axis: Axis) -> T::Scalar {
162        match axis {
163            Axis::X => self.min.x(),
164            Axis::Y => self.min.y(),
165            Axis::Z => self.min.z(),
166        }
167    }
168
169    /// Returns the maximum coordinate on `axis`.
170    #[must_use]
171    pub fn max(&self, axis: Axis) -> T::Scalar {
172        match axis {
173            Axis::X => self.max.x(),
174            Axis::Y => self.max.y(),
175            Axis::Z => self.max.z(),
176        }
177    }
178
179    /// Creates an AABB ensuring min <= max on every axis.
180    #[must_use]
181    pub fn from_min_max(min: T, max: T) -> Self {
182        Self {
183            min: min.min(max),
184            max: min.max(max),
185            p: PhantomData,
186        }
187    }
188
189    /// Translates the box by a vector.
190    #[must_use]
191    pub fn translate(self, delta: T) -> Self {
192        Self {
193            min: self.min + delta,
194            max: self.max + delta,
195            p: PhantomData,
196        }
197    }
198
199    /// Expands the box in every direction by `amount`.
200    #[must_use]
201    pub fn inflate(self, amount: T::Scalar) -> Self {
202        self.inflate_xyz(amount, amount, amount)
203    }
204
205    /// Expands the box independently on each axis.
206    #[must_use]
207    pub fn inflate_xyz(self, x: T::Scalar, y: T::Scalar, z: T::Scalar) -> Self {
208        let delta = T::new(x, y, z);
209        Self::from_min_max(self.min - delta, self.max + delta)
210    }
211
212    /// Returns the smallest AABB that contains both `a` and `b`.
213    #[must_use]
214    pub fn encapsulating(a: &Self, b: &Self) -> Self {
215        Self {
216            min: a.min.min(b.min),
217            max: a.max.max(b.max),
218            p: PhantomData,
219        }
220    }
221}
222
223impl<I> Aabb<DVec3, I> {
224    /// Returns the minimum corner.
225    #[must_use]
226    pub const fn min_corner(&self) -> DVec3 {
227        self.min
228    }
229
230    /// Returns the maximum corner.
231    #[must_use]
232    pub const fn max_corner(&self) -> DVec3 {
233        self.max
234    }
235
236    /// Returns the minimum X coordinate.
237    #[must_use]
238    pub const fn min_x(&self) -> f64 {
239        self.min.x
240    }
241
242    /// Returns the minimum Y coordinate.
243    #[must_use]
244    pub const fn min_y(&self) -> f64 {
245        self.min.y
246    }
247
248    /// Returns the minimum Z coordinate.
249    #[must_use]
250    pub const fn min_z(&self) -> f64 {
251        self.min.z
252    }
253
254    /// Returns the maximum X coordinate.
255    #[must_use]
256    pub const fn max_x(&self) -> f64 {
257        self.max.x
258    }
259
260    /// Returns the maximum Y coordinate.
261    #[must_use]
262    pub const fn max_y(&self) -> f64 {
263        self.max.y
264    }
265
266    /// Returns the maximum Z coordinate.
267    #[must_use]
268    pub const fn max_z(&self) -> f64 {
269        self.max.z
270    }
271
272    /// Returns the squared distance from `point` to this box.
273    #[must_use]
274    pub fn distance_to_sqr(self, point: DVec3) -> f64 {
275        let dx = f64::max(f64::max(self.min.x - point.x, point.x - self.max.x), 0.0);
276        let dy = f64::max(f64::max(self.min.y - point.y, point.y - self.max.y), 0.0);
277        let dz = f64::max(f64::max(self.min.z - point.z, point.z - self.max.z), 0.0);
278        dx * dx + dy * dy + dz * dz
279    }
280
281    /// Returns the squared distance from `other` to this box.
282    #[must_use]
283    pub fn distance_to_sqr_aabb(self, other: Self) -> f64 {
284        let dx = f64::max(
285            f64::max(self.min.x - other.max.x, other.min.x - self.max.x),
286            0.0,
287        );
288        let dy = f64::max(
289            f64::max(self.min.y - other.max.y, other.min.y - self.max.y),
290            0.0,
291        );
292        let dz = f64::max(
293            f64::max(self.min.z - other.max.z, other.min.z - self.max.z),
294            0.0,
295        );
296        dx * dx + dy * dy + dz * dz
297    }
298
299    /// Returns the closest point inside this box to `point`.
300    ///
301    /// Mirrors the per-box clamp used by vanilla `VoxelShape.closestPointTo`.
302    #[must_use]
303    pub const fn closest_point_to(self, point: DVec3) -> DVec3 {
304        DVec3::new(
305            point.x.clamp(self.min.x, self.max.x),
306            point.y.clamp(self.min.y, self.max.y),
307            point.z.clamp(self.min.z, self.max.z),
308        )
309    }
310}
311
312impl<I> Aabb<IVec3, I> {
313    /// Returns the minimum corner.
314    #[must_use]
315    #[inline]
316    pub const fn min_corner(&self) -> IVec3 {
317        self.min
318    }
319
320    /// Returns the maximum corner.
321    #[must_use]
322    #[inline]
323    pub const fn max_corner(&self) -> IVec3 {
324        self.max
325    }
326
327    /// Returns the minimum X coordinate.
328    #[must_use]
329    #[inline]
330    pub const fn min_x(&self) -> i32 {
331        self.min.x
332    }
333
334    /// Returns the minimum Y coordinate.
335    #[must_use]
336    #[inline]
337    pub const fn min_y(&self) -> i32 {
338        self.min.y
339    }
340
341    /// Returns the minimum Z coordinate.
342    #[must_use]
343    #[inline]
344    pub const fn min_z(&self) -> i32 {
345        self.min.z
346    }
347
348    /// Returns the maximum X coordinate.
349    #[must_use]
350    #[inline]
351    pub const fn max_x(&self) -> i32 {
352        self.max.x
353    }
354
355    /// Returns the maximum Y coordinate.
356    #[must_use]
357    #[inline]
358    pub const fn max_y(&self) -> i32 {
359        self.max.y
360    }
361
362    /// Returns the maximum Z coordinate.
363    #[must_use]
364    #[inline]
365    pub const fn max_z(&self) -> i32 {
366        self.max.z
367    }
368}
369
370impl<T: AabbVector, I> Aabb<T, I> {
371    /// Shrinks the box by `amount` in every direction.
372    #[must_use]
373    pub fn deflate(self, amount: T::Scalar) -> Self {
374        self.inflate(-amount)
375    }
376}
377
378impl<T: AabbVector, I: Space> Aabb<T, I> {
379    #[inline]
380    fn axis_overlaps(min1: T::Scalar, max1: T::Scalar, min2: T::Scalar, max2: T::Scalar) -> bool {
381        if I::ZERO_SPAN_IS_EMPTY {
382            min1 < max2 && max1 > min2
383        } else {
384            min1 <= max2 && max1 >= min2
385        }
386    }
387
388    #[inline]
389    fn axis_contains(min: T::Scalar, max: T::Scalar, v: T::Scalar) -> bool {
390        if I::ZERO_SPAN_IS_EMPTY {
391            v >= min && v < max
392        } else {
393            v >= min && v <= max
394        }
395    }
396
397    /// Returns `true` when this box has no positive volume on at least one axis.
398    pub fn is_empty(&self) -> bool {
399        if I::ZERO_SPAN_IS_EMPTY {
400            self.min.x() >= self.max.x()
401                || self.min.y() >= self.max.y()
402                || self.min.z() >= self.max.z()
403        } else {
404            self.min.x() > self.max.x()
405                || self.min.y() > self.max.y()
406                || self.min.z() > self.max.z()
407        }
408    }
409
410    /// Returns whether this bounding box intersects another.
411    #[must_use]
412    pub fn intersects(self, other: Self) -> bool {
413        self.intersects_bounds(other.min, other.max)
414    }
415
416    /// Returns `true` if this box intersects the given bounds.
417    #[must_use]
418    pub fn intersects_bounds(self, min: T, max: T) -> bool {
419        Self::axis_overlaps(self.min.x(), self.max.x(), min.x(), max.x())
420            && Self::axis_overlaps(self.min.y(), self.max.y(), min.y(), max.y())
421            && Self::axis_overlaps(self.min.z(), self.max.z(), min.z(), max.z())
422    }
423
424    /// Returns whether this bounding box intersects the given XZ range.
425    #[must_use]
426    pub fn intersects_xz(
427        self,
428        min_x: T::Scalar,
429        min_z: T::Scalar,
430        max_x: T::Scalar,
431        max_z: T::Scalar,
432    ) -> bool {
433        Self::axis_overlaps(self.min.x(), self.max.x(), min_x, max_x)
434            && Self::axis_overlaps(self.min.z(), self.max.z(), min_z, max_z)
435    }
436
437    /// Returns whether the given coordinates are inside this bounding box.
438    #[must_use]
439    pub fn contains(self, pos: T) -> bool {
440        self.contains_xyz(pos.x(), pos.y(), pos.z())
441    }
442
443    /// Returns whether the given coordinates are inside this bounding box.
444    #[must_use]
445    pub fn contains_xyz(self, x: T::Scalar, y: T::Scalar, z: T::Scalar) -> bool {
446        Self::axis_contains(self.min.x(), self.max.x(), x)
447            && Self::axis_contains(self.min.y(), self.max.y(), y)
448            && Self::axis_contains(self.min.z(), self.max.z(), z)
449    }
450}
451
452impl<T: AabbVector, I: Space> Aabb<T, I> {
453    #[inline]
454    fn span(raw: T::Scalar) -> T::Scalar {
455        if I::ZERO_SPAN_IS_EMPTY {
456            raw
457        } else {
458            raw + T::Scalar::from(1u8)
459        }
460    }
461
462    /// Get the width of bounding box (X Span)
463    #[must_use]
464    pub fn width(&self) -> T::Scalar {
465        Self::span(self.max.x() - self.min.x())
466    }
467
468    /// Get the height of bounding box (Y Span)
469    #[must_use]
470    pub fn height(&self) -> T::Scalar {
471        Self::span(self.max.y() - self.min.y())
472    }
473
474    /// Get the depth of bounding box (Z Span)
475    #[must_use]
476    pub fn depth(&self) -> T::Scalar {
477        Self::span(self.max.z() - self.min.z())
478    }
479
480    /// Returns the center point of the bounding box.
481    #[must_use]
482    pub fn center(&self) -> T {
483        let two = T::Scalar::from(2u8);
484        T::new(
485            self.min.x() + Self::span(self.max.x() - self.min.x()) / two,
486            self.min.y() + Self::span(self.max.y() - self.min.y()) / two,
487            self.min.z() + Self::span(self.max.z() - self.min.z()) / two,
488        )
489    }
490}
491
492impl<I: Space> Aabb<IVec3, I> {
493    /// Returns whether the given coordinates are inside this bounding box.
494    #[must_use]
495    pub fn contains_blockpos(self, pos: BlockPos) -> bool {
496        self.contains(pos.0)
497    }
498}
499
500impl<I: Space> Aabb<DVec3, I> {
501    /// A full block from `(0, 0, 0)` to `(1, 1, 1)`.
502    pub const FULL_BLOCK: Self = Self::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
503
504    /// A zero-volume box.
505    pub const EMPTY: Self = Self::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
506
507    /// Creates an AABB and normalizes endpoint order like vanilla `AABB`.
508    #[must_use]
509    pub const fn new(
510        min_x: f64,
511        min_y: f64,
512        min_z: f64,
513        max_x: f64,
514        max_y: f64,
515        max_z: f64,
516    ) -> Self {
517        let (min_x, max_x) = ordered_pair(min_x, max_x);
518        let (min_y, max_y) = ordered_pair(min_y, max_y);
519        let (min_z, max_z) = ordered_pair(min_z, max_z);
520        Self {
521            min: DVec3::new(min_x, min_y, min_z),
522            max: DVec3::new(max_x, max_y, max_z),
523            p: PhantomData,
524        }
525    }
526
527    /// Creates a box centered at `center` with the supplied side lengths.
528    ///
529    /// Vanilla equivalent: `AABB.ofSize(center, sizeX, sizeY, sizeZ)`.
530    #[must_use]
531    pub fn of_size(center: DVec3, size_x: f64, size_y: f64, size_z: f64) -> Self {
532        let half_x = size_x / 2.0;
533        let half_y = size_y / 2.0;
534        let half_z = size_z / 2.0;
535        Self::new(
536            center.x - half_x,
537            center.y - half_y,
538            center.z - half_z,
539            center.x + half_x,
540            center.y + half_y,
541            center.z + half_z,
542        )
543    }
544
545    /// Vanilla equivalent: `AABB.getSize()`.
546    #[must_use]
547    pub fn size(self) -> f64 {
548        (self.width() + self.height() + self.depth()) / 3.0
549    }
550
551    /// Expands the box only in the direction of `delta`.
552    #[must_use]
553    pub fn expand_towards(self, delta: DVec3) -> Self {
554        Self {
555            min: self.min + delta.min(DVec3::ZERO),
556            max: self.max + delta.max(DVec3::ZERO),
557            p: PhantomData,
558        }
559    }
560
561    /// Shrinks the box in the direction of `delta`.
562    ///
563    /// Positive components move the maximum edge inward, while negative
564    /// components move the minimum edge inward.
565    #[must_use]
566    pub fn contract(self, delta: DVec3) -> Self {
567        let mut min = self.min;
568        let mut max = self.max;
569
570        if delta.x < 0.0 {
571            min.x -= delta.x;
572        } else if delta.x > 0.0 {
573            max.x -= delta.x;
574        }
575
576        if delta.y < 0.0 {
577            min.y -= delta.y;
578        } else if delta.y > 0.0 {
579            max.y -= delta.y;
580        }
581
582        if delta.z < 0.0 {
583            min.z -= delta.z;
584        } else if delta.z > 0.0 {
585            max.z -= delta.z;
586        }
587
588        Self {
589            min,
590            max,
591            p: PhantomData,
592        }
593    }
594}
595
596impl Aabb<DVec3, BlockLocal> {
597    /// Converts this block-local box to a world-space box at `pos`.
598    #[must_use]
599    pub fn at_block(self, pos: BlockPos) -> Aabb<DVec3, World> {
600        let offset = DVec3::new(f64::from(pos.x()), f64::from(pos.y()), f64::from(pos.z()));
601        Aabb {
602            min: self.min + offset,
603            max: self.max + offset,
604            p: PhantomData,
605        }
606    }
607}
608
609impl Aabb<DVec3, World> {
610    /// Creates an entity bounding box centered on X/Z and using `y` as feet.
611    #[must_use]
612    pub fn entity_box(x: f64, y: f64, z: f64, half_width: f64, height: f64) -> Self {
613        Self::new(
614            x - half_width,
615            y,
616            z - half_width,
617            x + half_width,
618            y + height,
619            z + half_width,
620        )
621    }
622
623    /// Returns `true` if this box intersects the full block at `pos`.
624    #[must_use]
625    pub fn intersects_block(self, pos: BlockPos) -> bool {
626        let min = DVec3::new(f64::from(pos.x()), f64::from(pos.y()), f64::from(pos.z()));
627        let max = min + DVec3::ONE;
628        self.intersects_bounds(min, max)
629    }
630}
631
632impl Aabb<IVec3, Structure> {
633    /// Creates a new bounding box, normalizing so min <= max on each axis.
634    #[must_use]
635    pub const fn new(pos1: IVec3, pos2: IVec3) -> Self {
636        let (min_x, max_x) = ordered_pair_i32(pos1.x, pos2.x);
637        let (min_y, max_y) = ordered_pair_i32(pos1.y, pos2.y);
638        let (min_z, max_z) = ordered_pair_i32(pos1.z, pos2.z);
639        Self {
640            min: IVec3::new(min_x, min_y, min_z),
641            max: IVec3::new(max_x, max_y, max_z),
642            p: PhantomData,
643        }
644    }
645
646    /// Creates a bounding box from two corner block positions.
647    #[must_use]
648    pub const fn from_corners(a: BlockPos, b: BlockPos) -> Self {
649        Self::new(a.0, b.0)
650    }
651
652    /// Returns the squared distance from `point` to this box.
653    #[must_use]
654    pub fn distance_to_sqr(self, point: DVec3) -> f64 {
655        let min_x = f64::from(self.min_x());
656        let min_y = f64::from(self.min_y());
657        let min_z = f64::from(self.min_z());
658        let max_x = f64::from(self.max_x());
659        let max_y = f64::from(self.max_y());
660        let max_z = f64::from(self.max_z());
661
662        let dx = f64::max(f64::max(min_x - point.x, point.x - max_x), 0.0);
663        let dy = f64::max(f64::max(min_y - point.y, point.y - max_y), 0.0);
664        let dz = f64::max(f64::max(min_z - point.z, point.z - max_z), 0.0);
665        dx * dx + dy * dy + dz * dz
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    #[test]
674    fn constructors_normalize_endpoints_like_vanilla() {
675        let aabb = WorldAabb::new(3.0, 4.0, 5.0, 1.0, 2.0, 0.0);
676        assert_eq!(aabb.min_x(), 1.0);
677        assert_eq!(aabb.min_y(), 2.0);
678        assert_eq!(aabb.min_z(), 0.0);
679        assert_eq!(aabb.max_x(), 3.0);
680        assert_eq!(aabb.max_y(), 4.0);
681        assert_eq!(aabb.max_z(), 5.0);
682    }
683
684    #[test]
685    fn inflate_and_deflate_normalize_inverted_bounds() {
686        let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0).deflate(0.75);
687        assert_eq!(aabb.min_corner(), DVec3::splat(0.25));
688        assert_eq!(aabb.max_corner(), DVec3::splat(0.75));
689
690        let bbox = BoundingBox::new(IVec3::ZERO, IVec3::splat(5)).inflate(-4);
691        assert_eq!(bbox.min_corner(), IVec3::splat(1));
692        assert_eq!(bbox.max_corner(), IVec3::splat(4));
693    }
694
695    #[test]
696    fn of_size_builds_vanilla_centered_box() {
697        let aabb = WorldAabb::of_size(DVec3::new(10.0, 64.0, -2.0), 2.0, 4.0, 6.0);
698
699        assert_eq!(aabb.min_corner(), DVec3::new(9.0, 62.0, -5.0));
700        assert_eq!(aabb.max_corner(), DVec3::new(11.0, 66.0, 1.0));
701    }
702
703    #[test]
704    fn block_local_aabb_translates_to_world_space() {
705        let local = BlockLocalAabb::new(0.0, 0.25, 0.0, 1.0, 0.75, 1.0);
706        let world = local.at_block(BlockPos::new(10, 64, -5));
707
708        assert_eq!(world.min_x(), 10.0);
709        assert_eq!(world.min_y(), 64.25);
710        assert_eq!(world.min_z(), -5.0);
711        assert_eq!(world.max_x(), 11.0);
712        assert_eq!(world.max_y(), 64.75);
713        assert_eq!(world.max_z(), -4.0);
714    }
715
716    #[test]
717    fn contains_uses_vanilla_exclusive_max_edge() {
718        let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
719
720        assert!(aabb.contains_xyz(0.0, 0.5, 0.5));
721        assert!(aabb.contains_xyz(0.999, 0.5, 0.5));
722        assert!(!aabb.contains_xyz(1.0, 0.5, 0.5));
723    }
724
725    #[test]
726    fn world_aabb_distance_to_sqr_uses_nearest_surface_point() {
727        let aabb = WorldAabb::new(1.0, 2.0, 3.0, 4.0, 6.0, 8.0);
728
729        assert_eq!(aabb.distance_to_sqr(DVec3::new(2.0, 3.0, 4.0)), 0.0);
730        assert_eq!(aabb.distance_to_sqr(DVec3::new(0.0, 1.0, 1.0)), 6.0);
731        assert_eq!(aabb.distance_to_sqr(DVec3::new(5.0, 7.0, 9.0)), 3.0);
732    }
733
734    #[test]
735    fn distance_to_sqr_aabb_uses_nearest_gap_between_boxes() {
736        let aabb = WorldAabb::new(1.0, 2.0, 3.0, 4.0, 6.0, 8.0);
737
738        // Overlapping boxes have zero distance.
739        assert_eq!(
740            aabb.distance_to_sqr_aabb(WorldAabb::new(2.0, 3.0, 4.0, 5.0, 7.0, 9.0)),
741            0.0
742        );
743        // A box entirely below on every axis: gaps of 1, 1, 2 -> 1+1+4 = 6.
744        assert_eq!(
745            aabb.distance_to_sqr_aabb(WorldAabb::new(-1.0, 0.0, 0.0, 0.0, 1.0, 1.0)),
746            6.0
747        );
748        // A box entirely above on every axis: gaps of 1, 1, 1 -> 3.
749        assert_eq!(
750            aabb.distance_to_sqr_aabb(WorldAabb::new(5.0, 7.0, 9.0, 6.0, 8.0, 10.0)),
751            3.0
752        );
753    }
754
755    #[test]
756    fn closest_point_to_clamps_to_box_bounds() {
757        let aabb = WorldAabb::new(1.0, 2.0, 3.0, 4.0, 6.0, 8.0);
758
759        assert_eq!(
760            aabb.closest_point_to(DVec3::new(0.0, 4.0, 10.0)),
761            DVec3::new(1.0, 4.0, 8.0)
762        );
763        assert_eq!(
764            aabb.closest_point_to(DVec3::new(2.0, 3.0, 4.0)),
765            DVec3::new(2.0, 3.0, 4.0)
766        );
767    }
768
769    #[test]
770    fn expand_towards_covers_start_and_end() {
771        let aabb = WorldAabb::new(1.0, 1.0, 1.0, 2.0, 2.0, 2.0);
772        let swept = aabb.expand_towards(DVec3::new(-0.5, 1.5, 0.0));
773
774        assert_eq!(swept.min_x(), 0.5);
775        assert_eq!(swept.min_y(), 1.0);
776        assert_eq!(swept.min_z(), 1.0);
777        assert_eq!(swept.max_x(), 2.0);
778        assert_eq!(swept.max_y(), 3.5);
779        assert_eq!(swept.max_z(), 2.0);
780    }
781}