all_is_cubes_base/math/
grid_aab.rs

1//! Axis-aligned integer-coordinate box volumes ([`GridAab`]), three-dimensional data within those
2//! volumes ([`Vol`]), and related.
3
4use core::fmt;
5use core::ops::Range;
6
7use euclid::{Vector3D, size3};
8use manyfmt::Refmt;
9use rand::Rng as _;
10
11use crate::math::{
12    Aab, Axis, Cube, Face6, FaceMap, FreeCoordinate, FreePoint, GridCoordinate, GridIter,
13    GridPoint, GridSize, GridSizeCoord, GridVector, Gridgid, Vol, sort_two,
14};
15use crate::resolution::Resolution;
16use crate::util::ConciseDebug;
17
18#[allow(missing_docs, reason = "documented in its all-is-cubes reexport")]
19#[derive(Clone, Copy, Eq, Hash, PartialEq)]
20pub struct GridAab {
21    lower_bounds: GridPoint,
22    /// Constructor checks ensure this is not smaller than `lower_bounds`.
23    upper_bounds: GridPoint,
24}
25
26impl GridAab {
27    /// Box containing the unit cube from `[0, 0, 0]` to `[1, 1, 1]`.
28    ///
29    /// This constant is for convenience; there are several other ways that this box could
30    /// be constructed, but they're all kind of verbose:
31    ///
32    /// ```
33    /// # mod all_is_cubes {
34    /// #   pub mod block { pub use all_is_cubes_base::resolution::Resolution; }
35    /// #   pub use all_is_cubes_base::math;
36    /// # }
37    /// use all_is_cubes::block::Resolution;
38    /// use all_is_cubes::math::{GridAab, Cube};
39    ///
40    /// assert_eq!(GridAab::ORIGIN_CUBE, GridAab::from_lower_upper([0, 0, 0], [1, 1, 1]));
41    ///
42    /// // Note that GridAab::for_block() is const too.
43    /// assert_eq!(GridAab::ORIGIN_CUBE, GridAab::for_block(Resolution::R1));
44    ///
45    /// assert_eq!(GridAab::ORIGIN_CUBE, GridAab::single_cube(Cube::ORIGIN));
46    /// ```
47    ///
48    pub const ORIGIN_CUBE: GridAab = GridAab::for_block(Resolution::R1);
49
50    /// Box of zero size at `[0, 0, 0]`.
51    ///
52    /// Use this box as the canonical placeholder “nothing” value when it is necessary to
53    /// have *some* box.
54    pub const ORIGIN_EMPTY: GridAab = GridAab {
55        lower_bounds: GridPoint::new(0, 0, 0),
56        upper_bounds: GridPoint::new(0, 0, 0),
57    };
58
59    /// Box that covers everything every other box does.
60    pub const EVERYWHERE: GridAab = GridAab {
61        lower_bounds: GridPoint::new(
62            GridCoordinate::MIN,
63            GridCoordinate::MIN,
64            GridCoordinate::MIN,
65        ),
66        upper_bounds: GridPoint::new(
67            GridCoordinate::MAX,
68            GridCoordinate::MAX,
69            GridCoordinate::MAX,
70        ),
71    };
72
73    /// Constructs a [`GridAab`] from coordinate lower bounds and sizes.
74    ///
75    /// For example, if on one axis the lower bound is 5 and the size is 10,
76    /// then the positions where blocks can exist are numbered 5 through 14
77    /// (inclusive) and the occupied volume (from a perspective of continuous
78    /// rather than discrete coordinates) spans 5 to 15.
79    ///
80    /// Panics if the sizes are negative or the resulting range would cause
81    /// numeric overflow. Use [`GridAab::checked_from_lower_upper`] to avoid panics.
82    //---
83    // TODO: It would be more convenient for callers if `sizes` accepted `Size3D<GridCoordinate>`
84    // and other such alternate numeric types. There would be no disadvantage since this is a
85    // range-checked operation anyway. However, we'd need a custom conversion trait to handle that.
86    #[track_caller]
87    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
88    pub fn from_lower_size(lower_bounds: impl Into<GridPoint>, sizes: impl Into<GridSize>) -> Self {
89        Self::checked_from_lower_size(lower_bounds.into(), sizes.into())
90            .expect("GridAab::from_lower_size")
91    }
92
93    /// Constructs a [`GridAab`] from inclusive lower bounds and exclusive upper bounds.
94    ///
95    /// For example, if on one axis the lower bound is 5 and the upper bound is 10,
96    /// then the positions where blocks can exist are numbered 5 through 9
97    /// (inclusive) and the occupied volume (from a perspective of continuous
98    /// rather than discrete coordinates) spans 5 to 10.
99    ///
100    /// Returns [`Err`] if any of the `upper_bounds` are less than the `lower_bounds`.
101    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
102    pub fn checked_from_lower_upper(
103        lower_bounds: impl Into<GridPoint>,
104        upper_bounds: impl Into<GridPoint>,
105    ) -> Result<Self, GridOverflowError> {
106        Self::const_checked_from_lower_upper(lower_bounds.into(), upper_bounds.into())
107    }
108
109    pub(crate) const fn const_checked_from_lower_upper(
110        lower_bounds: GridPoint,
111        upper_bounds: GridPoint,
112    ) -> Result<GridAab, GridOverflowError> {
113        if upper_bounds.x < lower_bounds.x
114            || upper_bounds.y < lower_bounds.y
115            || upper_bounds.z < lower_bounds.z
116        {
117            return Err(GridOverflowError(OverflowKind::Inverted {
118                lower_bounds,
119                upper_bounds,
120            }));
121        }
122
123        Ok(GridAab {
124            lower_bounds,
125            upper_bounds,
126        })
127    }
128
129    /// Constructs a [`GridAab`] from inclusive lower bounds and exclusive upper bounds.
130    ///
131    /// For example, if on one axis the lower bound is 5 and the upper bound is 10,
132    /// then the positions where blocks can exist are numbered 5 through 9
133    /// (inclusive) and the occupied volume (from a perspective of continuous
134    /// rather than discrete coordinates) spans 5 to 10.
135    ///
136    /// Panics if any of the `upper_bounds` are less than the `lower_bounds`.
137    #[track_caller]
138    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
139    pub fn from_lower_upper(
140        lower_bounds: impl Into<GridPoint>,
141        upper_bounds: impl Into<GridPoint>,
142    ) -> GridAab {
143        Self::checked_from_lower_upper(lower_bounds.into(), upper_bounds.into())
144            .expect("GridAab::from_lower_upper")
145    }
146
147    /// Constructs a [`GridAab`] from [`Range`]s.
148    ///
149    /// This is identical to [`GridAab::from_lower_upper()`] except for the input type.
150    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
151    #[track_caller]
152    pub fn from_ranges(ranges: impl Into<Vector3D<Range<GridCoordinate>, Cube>>) -> GridAab {
153        let ranges = ranges.into();
154        GridAab::from_lower_upper(
155            ranges.clone().map(|r| r.start).to_point(),
156            ranges.map(|r| r.end).to_point(),
157        )
158    }
159
160    /// Constructs a [`GridAab`] from coordinate lower bounds and sizes.
161    ///
162    /// Returns [`Err`] if the `size` is negative or adding it to `lower_bounds` overflows.
163    #[track_caller]
164    #[allow(clippy::missing_inline_in_public_items, reason = "is generic already")]
165    pub fn checked_from_lower_size(
166        lower_bounds: impl Into<GridPoint>,
167        size: impl Into<GridSize>,
168    ) -> Result<Self, GridOverflowError> {
169        #[inline]
170        fn inner(lower_bounds: GridPoint, size: GridSize) -> Result<GridAab, GridOverflowError> {
171            let upper_bounds = (|| {
172                Some(GridPoint::new(
173                    lower_bounds.x.checked_add_unsigned(size.width)?,
174                    lower_bounds.y.checked_add_unsigned(size.height)?,
175                    lower_bounds.z.checked_add_unsigned(size.depth)?,
176                ))
177            })()
178            .ok_or(GridOverflowError(OverflowKind::OverflowedSize {
179                lower_bounds,
180                size,
181            }))?;
182            GridAab::checked_from_lower_upper(lower_bounds, upper_bounds)
183        }
184
185        inner(lower_bounds.into(), size.into())
186    }
187
188    /// Constructs a [`GridAab`] with a volume of 1, containing the specified cube.
189    ///
190    /// Panics if `cube` has any coordinates equal to [`GridCoordinate::MAX`]
191    /// since that is not valid, as per [`GridAab::from_lower_size()`].
192    ///
193    /// This function is identical to [`Cube::grid_aab()`].
194    #[inline]
195    pub fn single_cube(cube: Cube) -> GridAab {
196        cube.grid_aab()
197    }
198
199    /// Constructs a [`GridAab`] with a cubical volume in the positive octant, as is used
200    /// for recursive blocks.
201    ///
202    /// If you need such a box at a position other than the origin, use
203    /// [`GridAab::translate()`].
204    #[inline]
205    pub const fn for_block(resolution: Resolution) -> GridAab {
206        let size = resolution.to_grid();
207        GridAab {
208            lower_bounds: GridPoint::new(0, 0, 0),
209            upper_bounds: GridPoint::new(size, size, size),
210        }
211    }
212
213    /// Constructs a [`GridAab`] from 8-bit integers that cannot overflow.
214    ///
215    /// This constructor is limited so that it is `const` and infallible.
216    /// It always behaves identically to [`GridAab::from_lower_size()`].
217    ///
218    /// See also [`GridAab::for_block()`].
219    #[inline]
220    pub const fn tiny(
221        lower_bounds: euclid::Point3D<i8, Cube>,
222        size: euclid::Size3D<u8, Cube>,
223    ) -> Self {
224        GridAab {
225            lower_bounds: GridPoint::new(
226                lower_bounds.x as i32,
227                lower_bounds.y as i32,
228                lower_bounds.z as i32,
229            ),
230            upper_bounds: GridPoint::new(
231                lower_bounds.x as i32 + size.width as i32,
232                lower_bounds.y as i32 + size.height as i32,
233                lower_bounds.z as i32 + size.depth as i32,
234            ),
235        }
236    }
237
238    /// Computes the volume of this box in cubes, i.e. the product of all sizes.
239    ///
240    /// Returns [`None`] if the volume does not fit in a `usize`.
241    /// (If this fallibility is undesirable, consider using a [`Vol<()>`][Vol] instead of
242    /// [`GridAab`].)
243    ///
244    /// ```
245    /// # extern crate all_is_cubes_base as all_is_cubes;
246    /// use all_is_cubes::math::GridAab;
247    ///
248    /// let a = GridAab::from_lower_size([-10, 3, 7], [100, 200, 300]);
249    /// assert_eq!(a.volume(), Some(6_000_000));
250    ///
251    /// let b = GridAab::from_lower_size([0, 0, 0], [100, 200, 0]);
252    /// assert_eq!(b.volume(), Some(0));
253    /// ```
254    //---
255    // TODO: add doctest example of failure
256    #[inline]
257    pub const fn volume(&self) -> Option<usize> {
258        // Convert size values to usize.
259        // These conversions cannot overflow and do not need to be checked,
260        // because we only build on platforms where usize is 32 bits.
261        // This is checked elsewhere but let's assert it locally too.
262        const {
263            assert!(size_of::<GridSizeCoord>() <= size_of::<usize>());
264        }
265        let sizes = self.size();
266        let width = sizes.width as usize;
267        let height = sizes.height as usize;
268        let depth = sizes.depth as usize;
269
270        // Checked multiplication of width * height * depth.
271        let Some(area) = width.checked_mul(height) else {
272            return None;
273        };
274        area.checked_mul(depth)
275    }
276
277    /// Computes the approximate volume of this box in cubes, i.e. the product of all sizes
278    /// converted to [`f64`].
279    #[inline]
280    pub fn volume_f64(&self) -> f64 {
281        self.size().to_f64().volume()
282    }
283
284    /// Computes the surface area of this box; 1 unit of area = 1 cube-face.
285    ///
286    /// Returns `f64` to avoid needing overflow considerations, and because all internal uses
287    /// want float anyway.
288    #[inline]
289    pub fn surface_area_f64(&self) -> f64 {
290        let size = self.size().to_f64();
291        size.width * size.height * 2. + size.width * size.depth * 2. + size.height * size.depth * 2.
292    }
293
294    /// Returns whether the box contains no cubes (its volume is zero).
295    ///
296    /// This does not necessarily mean that its size is zero on all axes.
297    #[inline]
298    pub fn is_empty(&self) -> bool {
299        self.size().is_empty()
300    }
301
302    /// Inclusive upper bounds on cube coordinates, or the most negative corner of the
303    /// box.
304    #[inline]
305    pub fn lower_bounds(&self) -> GridPoint {
306        self.lower_bounds
307    }
308
309    /// Exclusive upper bounds on cube coordinates, or the most positive corner of the
310    /// box.
311    #[inline]
312    pub fn upper_bounds(&self) -> GridPoint {
313        // Cannot overflow due to constructor-enforced invariants,
314        // so always use un-checked arithmetic
315        self.upper_bounds
316    }
317
318    /// Size of the box in each axis; equivalent to
319    /// `self.upper_bounds() - self.lower_bounds()`, except that the result is
320    /// unsigned (which is necessary so that it cannot overflow).
321    #[inline]
322    pub const fn size(&self) -> GridSize {
323        size3(
324            // Two’s complement arithmetic trick: If the subtraction overflows and wraps, the
325            // following conversion to u32 will give us the right answer anyway.
326            //
327            // Declaring the parameter type ensures that if we ever decide to change the numeric
328            // type of `GridCoordinate`, this will fail to compile.
329            i32::wrapping_sub(self.upper_bounds.x, self.lower_bounds.x).cast_unsigned(),
330            i32::wrapping_sub(self.upper_bounds.y, self.lower_bounds.y).cast_unsigned(),
331            i32::wrapping_sub(self.upper_bounds.z, self.lower_bounds.z).cast_unsigned(),
332        )
333    }
334
335    /// The range of X coordinates for unit cubes within the box.
336    #[inline]
337    pub fn x_range(&self) -> Range<GridCoordinate> {
338        self.axis_range(Axis::X)
339    }
340
341    /// The range of Y coordinates for unit cubes within the box.
342    #[inline]
343    pub fn y_range(&self) -> Range<GridCoordinate> {
344        self.axis_range(Axis::Y)
345    }
346
347    /// The range of Z coordinates for unit cubes within the box.
348    #[inline]
349    pub fn z_range(&self) -> Range<GridCoordinate> {
350        self.axis_range(Axis::Z)
351    }
352
353    /// The range of coordinates for cubes within the box along the given axis.
354    #[inline]
355    pub fn axis_range(&self, axis: Axis) -> Range<GridCoordinate> {
356        (self.lower_bounds()[axis])..(self.upper_bounds()[axis])
357    }
358
359    /// The center of the enclosed volume. Returns [`FreeCoordinate`]s since the center
360    /// may be at a half-block position.
361    ///
362    /// ```
363    /// # extern crate all_is_cubes_base as all_is_cubes;
364    /// use all_is_cubes::math::{FreePoint, GridAab};
365    ///
366    /// let b = GridAab::from_lower_size([0, 0, -2], [10, 3, 4]);
367    /// assert_eq!(b.center(), FreePoint::new(5.0, 1.5, 0.0));
368    /// ```
369    #[inline]
370    pub fn center(&self) -> FreePoint {
371        (self.lower_bounds.map(FreeCoordinate::from)
372            + self.upper_bounds.map(FreeCoordinate::from).to_vector())
373            / 2.
374    }
375
376    /// Iterate over all cubes that this contains.
377    ///
378    /// The order of iteration is deterministic, but not guaranteed to be anything in particular,
379    /// and may change in later versions. If order matters, use [`Vol::iter_cubes()`] instead.
380    ///
381    /// ```
382    /// # extern crate all_is_cubes_base as all_is_cubes;
383    /// use all_is_cubes::math::{GridAab, Cube};
384    ///
385    /// let b = GridAab::from_lower_size([10, 20, 30], [1, 2, 3]);
386    /// assert_eq!(
387    ///     b.interior_iter().collect::<Vec<Cube>>(),
388    ///     &[
389    ///         Cube::new(10, 20, 30),
390    ///         Cube::new(10, 20, 31),
391    ///         Cube::new(10, 20, 32),
392    ///         Cube::new(10, 21, 30),
393    ///         Cube::new(10, 21, 31),
394    ///         Cube::new(10, 21, 32),
395    ///     ])
396    /// ```
397    #[inline]
398    pub fn interior_iter(self) -> GridIter {
399        GridIter::new(self)
400    }
401
402    /// Returns whether the box includes the given cube position in its volume.
403    ///
404    /// ```
405    /// # extern crate all_is_cubes_base as all_is_cubes;
406    /// use all_is_cubes::math::{GridAab, Cube};
407    ///
408    /// let b = GridAab::from_lower_size([4, 4, 4], [6, 6, 6]);
409    /// assert!(!b.contains_cube([3, 5, 5].into()));
410    /// assert!(b.contains_cube([4, 5, 5].into()));
411    /// assert!(b.contains_cube([9, 5, 5].into()));
412    /// assert!(!b.contains_cube([10, 5, 5].into()));
413    /// ```
414    #[inline]
415    pub fn contains_cube(&self, cube: Cube) -> bool {
416        let self_upper = self.upper_bounds();
417        let cube_lower = cube.lower_bounds();
418        Axis::ALL.into_iter().all(|axis| {
419            cube_lower[axis] >= self.lower_bounds[axis] && cube_lower[axis] < self_upper[axis]
420        })
421    }
422
423    /// Returns whether this box includes every cube in the other box.
424    ///
425    /// TODO: Precisely define the behavior on zero volume boxes.
426    ///
427    /// ```
428    /// # extern crate all_is_cubes_base as all_is_cubes;
429    /// use all_is_cubes::math::GridAab;
430    /// let b46 = GridAab::from_lower_size([4, 4, 4], [6, 6, 6]);
431    /// assert!(b46.contains_box(b46));
432    /// assert!(!b46.contains_box(GridAab::from_lower_size([4, 4, 4], [7, 6, 6])));
433    /// assert!(!GridAab::from_lower_size((0, 0, 0), (6, 6, 6)).contains_box(b46));
434    /// ```
435    #[inline]
436    pub fn contains_box(&self, other: GridAab) -> bool {
437        let self_upper = self.upper_bounds();
438        let other_upper = other.upper_bounds();
439        for axis in Axis::ALL {
440            if other.lower_bounds[axis] < self.lower_bounds[axis]
441                || other_upper[axis] > self_upper[axis]
442            {
443                return false;
444            }
445        }
446        true
447    }
448
449    /// Returns the intersection of `self` and `other`, defined as the box which contains
450    /// every cube that both `self` and `other` do, and no others.
451    ///
452    /// Returns [`None`] if there are no such cubes.
453    /// In other words, if a box is returned, then its volume will always be nonzero;
454    /// this definition of intersection is suitable when the intent is to take action on
455    /// the intersecting cubes. For applications which are more concerned with preserving
456    /// the box coordinates, call [`GridAab::intersection_box()`] instead.
457    ///
458    /// ```
459    /// # extern crate all_is_cubes_base as all_is_cubes;
460    /// use all_is_cubes::math::GridAab;
461    ///
462    /// // Simple example of an intersection.
463    /// assert_eq!(
464    ///     GridAab::from_lower_size([0, 0, 0], [2, 2, 2])
465    ///         .intersection_cubes(GridAab::from_lower_size([1, 0, 0], [2, 1, 2])),
466    ///     Some(GridAab::from_lower_size([1, 0, 0], [1, 1, 2])),
467    /// );
468    ///
469    /// // A box's intersection with itself is equal to itself...
470    /// let b = GridAab::from_lower_size([1, 2, 3], [4, 5, 6]);
471    /// assert_eq!(b.intersection_cubes(b), Some(b));
472    /// // ...unless it has zero volume.
473    /// let bz = GridAab::from_lower_size([1, 2, 3], [4, 5, 0]);
474    /// assert_eq!(bz.intersection_cubes(bz), None);
475    ///
476    /// // Boxes which only touch on their faces are not considered to intersect.
477    /// assert_eq!(
478    ///     GridAab::from_lower_size([0, 0, 0], [2, 2, 2])
479    ///         .intersection_cubes(GridAab::from_lower_size([2, 0, 0], [2, 1, 2])),
480    ///     None,
481    /// );
482    /// ```
483    #[inline]
484    #[must_use]
485    pub fn intersection_cubes(self, other: GridAab) -> Option<GridAab> {
486        let lower = self.lower_bounds().max(other.lower_bounds());
487        let upper = self.upper_bounds().min(other.upper_bounds());
488        for axis in Axis::ALL {
489            if upper[axis] <= lower[axis] {
490                return None;
491            }
492        }
493        Some(GridAab::from_lower_upper(lower, upper))
494    }
495
496    /// Returns the intersection of `self` and `other`, defined as the box which is as large as
497    /// possible while not extending beyond the bounds of `self` or the bounds of `other`.
498    ///
499    /// Returns [`None`] if that is impossible, i.e. if the two boxes do not touch.
500    ///
501    /// This definition of intersection is suitable when the intent is to constrain the bounds
502    /// of a box to fit in another, while preserving their coordinates as much as possible.
503    /// For applications which are more concerned with processing the overlapping volume when there
504    /// is overlap, call [`GridAab::intersection_cubes()`] instead for a tighter bound.
505    ///
506    /// ```
507    /// # extern crate all_is_cubes_base as all_is_cubes;
508    /// use all_is_cubes::math::GridAab;
509    ///
510    /// // Simple example of an intersection.
511    /// assert_eq!(
512    ///     GridAab::from_lower_size([0, 0, 0], [2, 2, 2])
513    ///         .intersection_box(GridAab::from_lower_size([1, 0, 0], [2, 1, 2])),
514    ///     Some(GridAab::from_lower_size([1, 0, 0], [1, 1, 2])),
515    /// );
516    ///
517    /// // A box's intersection with itself is always equal to itself...
518    /// let b = GridAab::from_lower_size([1, 2, 3], [4, 5, 6]);
519    /// assert_eq!(b.intersection_box(b), Some(b));
520    /// // ...even when it has zero volume.
521    /// let bz = GridAab::from_lower_size([1, 2, 3], [4, 5, 0]);
522    /// assert_eq!(bz.intersection_box(bz), Some(bz));
523    ///
524    /// // Boxes which only touch on their faces yield their shared boundary surface.
525    /// assert_eq!(
526    ///     GridAab::from_lower_size([0, 0, 0], [2, 2, 2])
527    ///         .intersection_box(GridAab::from_lower_size([2, 0, 0], [2, 1, 2])),
528    ///     Some(GridAab::from_lower_size([2, 0, 0], [0, 1, 2])),
529    /// );
530    /// ```
531    #[inline]
532    #[must_use]
533    pub fn intersection_box(self, other: GridAab) -> Option<GridAab> {
534        let lower = self.lower_bounds().max(other.lower_bounds());
535        let upper = self.upper_bounds().min(other.upper_bounds());
536        for axis in Axis::ALL {
537            if upper[axis] < lower[axis] {
538                return None;
539            }
540        }
541        Some(GridAab::from_lower_upper(lower, upper))
542    }
543
544    /// Returns the smallest [`GridAab`] which fully encloses the two inputs' cubes.
545    ///
546    /// The boundaries of empty boxes are ignored.
547    /// If this is not desired, call [`GridAab::union_box()`] instead.
548    /// If both inputs are empty, then `self` is returned.
549    ///
550    /// ```
551    /// # extern crate all_is_cubes_base as all_is_cubes;
552    /// use all_is_cubes::math::GridAab;
553    ///
554    /// let g1 = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
555    /// assert_eq!(g1.union_cubes(g1), g1);
556    ///
557    /// let g2 = GridAab::from_lower_size([4, 7, 11], [1, 1, 1]);
558    /// assert_eq!(g1.union_cubes(g2), GridAab::from_lower_upper([1, 2, 3], [5, 8, 12]));
559    ///
560    /// // Empty boxes (any size equal to zero) have no effect.
561    /// let empty = GridAab::from_lower_size([0, 0, 0], [0, 1, 7]);
562    /// assert_eq!(g1.union_cubes(empty), g1);
563    /// ```
564    #[inline]
565    #[must_use]
566    pub fn union_cubes(self, other: Self) -> Self {
567        if other.is_empty() {
568            self
569        } else if self.is_empty() {
570            other
571        } else {
572            self.union_box(other)
573        }
574    }
575    /// Returns the smallest [`GridAab`] which fully encloses the two inputs' boundaries.
576    ///
577    /// The boundaries of empty boxes are included.
578    /// If this is not desired, call [`GridAab::union_cubes()`] instead for a tighter bound.
579    ///
580    /// ```
581    /// # extern crate all_is_cubes_base as all_is_cubes;
582    /// use all_is_cubes::math::GridAab;
583    ///
584    /// let g1 = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
585    /// assert_eq!(g1.union_box(g1), g1);
586    ///
587    /// let g2 = GridAab::from_lower_size([4, 7, 11], [1, 1, 1]);
588    /// assert_eq!(g1.union_box(g2), GridAab::from_lower_upper([1, 2, 3], [5, 8, 12]));
589    ///
590    /// // Empty boxes (any size equal to zero) are included even though they contain no cubes.
591    /// let empty = GridAab::from_lower_size([0, 0, 0], [0, 1, 7]);
592    /// assert_eq!(g1.union_box(empty), GridAab::from_lower_upper([0, 0, 0], [2, 3, 7]));
593    ///
594    /// // A union of empty boxes can become non-empty by including the volume within.
595    /// assert_eq!(
596    ///     empty.union_box(empty.translate([3, 0, 0])),
597    ///     GridAab::from_lower_upper([0, 0, 0], [3, 1, 7]),
598    /// )
599    /// ```
600    #[inline]
601    #[must_use]
602    pub fn union_box(self, other: Self) -> Self {
603        let lower = self.lower_bounds().min(other.lower_bounds());
604        let upper = self.upper_bounds().max(other.upper_bounds());
605        // Subtraction and construction should not fail.
606        Self::from_lower_size(lower, (upper - lower).to_u32())
607    }
608
609    /// Extend the bounds of `self` as needed to enclose `other`.
610    ///
611    /// Equivalent to `self.union_box(GridAab::single_cube(other))`.
612    /// Note in particular that it does not discard the bounds of an empty `self`,
613    /// like [`GridAab::union_cubes()`] would.
614    ///
615    ///
616    /// ```
617    /// # extern crate all_is_cubes_base as all_is_cubes;
618    /// use all_is_cubes::math::{Cube, GridAab};
619    ///
620    /// let accumulation =
621    ///     GridAab::single_cube(Cube::new(1, 10, 7))
622    ///         .union_cube(Cube::new(2, 5, 10));
623    /// assert_eq!(accumulation, GridAab::from_lower_upper([1, 5, 7], [3, 11, 11]));
624    /// ```
625    #[inline]
626    #[must_use]
627    pub fn union_cube(self, other: Cube) -> GridAab {
628        Self {
629            lower_bounds: self.lower_bounds().min(other.lower_bounds()),
630            upper_bounds: self.upper_bounds().max(other.upper_bounds()),
631        }
632    }
633
634    #[doc(hidden)] // TODO: good public API?
635    #[inline]
636    pub fn minkowski_sum(self, other: GridAab) -> Result<GridAab, GridOverflowError> {
637        // TODO: needs checked sums
638        Self::checked_from_lower_size(
639            self.lower_bounds() + other.lower_bounds().to_vector(),
640            self.size() + other.size(),
641        )
642    }
643
644    /// Returns a random cube contained by the box, if there are any.
645    ///
646    /// ```
647    /// # extern crate all_is_cubes_base as all_is_cubes;
648    /// use all_is_cubes::math::GridAab;
649    /// use rand::SeedableRng;
650    ///
651    /// let mut rng = &mut rand_xoshiro::Xoshiro256Plus::seed_from_u64(0);
652    ///
653    /// let b = GridAab::from_lower_size([4, 4, 4], [6, 6, 6]);
654    /// for _ in 0..50 {
655    ///     assert!(b.contains_cube(b.random_cube(rng).unwrap()));
656    /// }
657    ///
658    /// let empty = GridAab::from_lower_size([1, 2, 3], [0, 9, 9]);
659    /// assert_eq!(empty.random_cube(rng), None);
660    /// ```
661    #[allow(clippy::missing_inline_in_public_items)]
662    pub fn random_cube(&self, rng: &mut impl rand::RngCore) -> Option<Cube> {
663        if self.is_empty() {
664            None
665        } else {
666            let _upper_bounds = self.upper_bounds();
667            Some(Cube::new(
668                rng.random_range(self.x_range()),
669                rng.random_range(self.y_range()),
670                rng.random_range(self.z_range()),
671            ))
672        }
673    }
674
675    /// Creates a [`Vol`] with `self` as the bounds and no data.
676    ///
677    /// This introduces a particular linear ordering of the cubes in the volume.
678    ///
679    /// Returns an error if the volume of `self` is greater than [`usize::MAX`].
680    #[inline]
681    pub fn to_vol<O: Default>(self) -> Result<Vol<(), O>, crate::math::VolLengthError> {
682        Vol::new_dataless(self, O::default())
683    }
684
685    /// Converts this box to floating-point coordinates.
686    ///
687    /// This conversion is also available via the [`From`] trait.
688    #[inline]
689    pub fn to_free(self) -> Aab {
690        Aab::from_lower_upper(
691            self.lower_bounds().map(FreeCoordinate::from),
692            self.upper_bounds().map(FreeCoordinate::from),
693        )
694    }
695
696    /// Displaces the box by the given `offset`, leaving its size unchanged
697    /// (unless that is impossible due to numeric overflow).
698    ///
699    /// ```
700    /// # extern crate all_is_cubes_base as all_is_cubes;
701    /// use all_is_cubes::math::GridAab;
702    ///
703    /// assert_eq!(
704    ///     GridAab::from_lower_size([0, 0, 0], [10, 20, 30]).translate([-10, 0, 0]),
705    ///     GridAab::from_lower_size([-10, 0, 0], [10, 20, 30]),
706    /// );
707    /// ```
708    #[must_use]
709    #[allow(clippy::missing_inline_in_public_items, reason = "already generic")]
710    pub fn translate(&self, offset: impl Into<GridVector>) -> Self {
711        fn inner(this: &GridAab, offset: GridVector) -> GridAab {
712            let offset = offset.to_point();
713            let new_lb = this.lower_bounds().zip(offset, GridCoordinate::saturating_add).to_point();
714            let new_ub = this.upper_bounds().zip(offset, GridCoordinate::saturating_add).to_point();
715            GridAab::from_lower_upper(new_lb, new_ub)
716        }
717
718        inner(self, offset.into())
719    }
720
721    /// Translate and rotate the box according to the given transform.
722    ///
723    /// TODO: Fail nicely on numeric overflow.
724    /// The `Option` return is not currently used.
725    #[must_use]
726    #[inline]
727    #[expect(
728        clippy::unnecessary_wraps,
729        reason = "TODO: fail nicely on numeric overflow"
730    )]
731    pub fn transform(self, transform: Gridgid) -> Option<Self> {
732        let mut p1 = transform.transform_point(self.lower_bounds());
733        let mut p2 = transform.transform_point(self.upper_bounds());
734
735        // Swap coordinates in case of rotation or reflection.
736        for axis in Axis::ALL {
737            sort_two(&mut p1[axis], &mut p2[axis]);
738        }
739        Some(Self::from_lower_upper(p1, p2))
740    }
741
742    /// Scales the box down by the given factor, rounding outward.
743    ///
744    /// For example, this may be used to convert from voxels (subcubes) to blocks or
745    /// blocks to chunks.
746    ///
747    /// Panics if the divisor is not positive.
748    ///
749    /// ```
750    /// # extern crate all_is_cubes_base as all_is_cubes;
751    /// use all_is_cubes::math::GridAab;
752    ///
753    /// assert_eq!(
754    ///     GridAab::from_lower_size([-10, -10, -10], [20, 20, 20]).divide(10),
755    ///     GridAab::from_lower_size([-1, -1, -1], [2, 2, 2]),
756    /// );
757    /// assert_eq!(
758    ///     GridAab::from_lower_size([-10, -10, -10], [21, 21, 21]).divide(10),
759    ///     GridAab::from_lower_size([-1, -1, -1], [3, 3, 3]),
760    /// );
761    /// assert_eq!(
762    ///     GridAab::from_lower_size([-11, -11, -11], [20, 20, 20]).divide(10),
763    ///     GridAab::from_lower_size([-2, -2, -2], [3, 3, 3]),
764    /// );
765    /// ```
766    #[inline]
767    #[track_caller]
768    #[must_use]
769    pub fn divide(self, divisor: GridCoordinate) -> Self {
770        assert!(
771            divisor > 0,
772            "GridAab::divide: divisor must be > 0, not {divisor}"
773        );
774        let upper_bounds = self.upper_bounds();
775        Self::from_lower_upper(
776            [
777                self.lower_bounds.x.div_euclid(divisor),
778                self.lower_bounds.y.div_euclid(divisor),
779                self.lower_bounds.z.div_euclid(divisor),
780            ],
781            [
782                (upper_bounds.x + divisor - 1).div_euclid(divisor),
783                (upper_bounds.y + divisor - 1).div_euclid(divisor),
784                (upper_bounds.z + divisor - 1).div_euclid(divisor),
785            ],
786        )
787    }
788
789    /// Scales the box up by the given factor.
790    ///
791    /// Panics on numeric overflow.
792    ///
793    /// ```
794    /// # extern crate all_is_cubes_base as all_is_cubes;
795    /// # use all_is_cubes::math::GridAab;
796    /// assert_eq!(
797    ///     GridAab::from_lower_size([-1, 2, 3], [4, 5, 6]).multiply(10),
798    ///     GridAab::from_lower_size([-10, 20, 30], [40, 50, 60]),
799    /// );
800    /// ```
801    #[inline]
802    #[track_caller]
803    #[must_use]
804    pub fn multiply(self, scale: GridCoordinate) -> Self {
805        // TODO: this should use checked multiplications to guarantee panic
806        Self::from_lower_upper(self.lower_bounds * scale, self.upper_bounds * scale)
807    }
808
809    /// Moves all bounds outward by the specified distances.
810    ///
811    /// If the result’s coordinates would overflow, they are as large as possible instead.
812    ///
813    /// ```
814    /// # extern crate all_is_cubes_base as all_is_cubes;
815    /// use all_is_cubes::math::{GridAab, FaceMap};
816    ///
817    /// assert_eq!(
818    ///     GridAab::from_lower_upper([10, 10, 10], [20, 20, 20])
819    ///         .expand(FaceMap {
820    ///             nx: 1, ny: 2, nz: 3,
821    ///             px: 4, py: 5, pz: 6,
822    ///         }),
823    ///     GridAab::from_lower_upper([9, 8, 7], [24, 25, 26]),
824    /// );
825    /// ```
826    #[inline]
827    #[must_use]
828    pub fn expand(self, deltas: FaceMap<GridSizeCoord>) -> Self {
829        let lower = self.lower_bounds();
830        let upper = self.upper_bounds();
831        Self::from_lower_upper(
832            [
833                lower.x.saturating_sub_unsigned(deltas.nx),
834                lower.y.saturating_sub_unsigned(deltas.ny),
835                lower.z.saturating_sub_unsigned(deltas.nz),
836            ],
837            [
838                upper.x.saturating_add_unsigned(deltas.px),
839                upper.y.saturating_add_unsigned(deltas.py),
840                upper.z.saturating_add_unsigned(deltas.pz),
841            ],
842        )
843    }
844
845    /// Moves all bounds inward by the specified distances.
846    ///
847    /// Returns `None` if the result would have less than zero size.
848    ///
849    /// ```
850    /// # extern crate all_is_cubes_base as all_is_cubes;
851    /// use all_is_cubes::math::{GridAab, FaceMap};
852    ///
853    /// assert_eq!(
854    ///     GridAab::from_lower_upper([10, 10, 10], [20, 20, 20])
855    ///         .shrink(FaceMap {
856    ///             nx: 1, ny: 2, nz: 3,
857    ///             px: 4, py: 5, pz: 6,
858    ///         }),
859    ///     Some(GridAab::from_lower_upper([11, 12, 13], [16, 15, 14])),
860    /// );
861    /// ```
862    #[inline]
863    #[must_use]
864    pub fn shrink(self, deltas: FaceMap<GridSizeCoord>) -> Option<Self> {
865        let lower = self.lower_bounds();
866        let upper = self.upper_bounds();
867        Self::checked_from_lower_upper(
868            [
869                lower.x.checked_add_unsigned(deltas.nx)?,
870                lower.y.checked_add_unsigned(deltas.ny)?,
871                lower.z.checked_add_unsigned(deltas.nz)?,
872            ],
873            [
874                upper.x.checked_sub_unsigned(deltas.px)?,
875                upper.y.checked_sub_unsigned(deltas.py)?,
876                upper.z.checked_sub_unsigned(deltas.pz)?,
877            ],
878        )
879        .ok()
880    }
881
882    /// Returns a [`GridAab`] which includes the volume between the given `face` rectangle
883    /// of `self` and the same rectangle translated `thickness` cubes outward from it
884    /// (inward if negative).
885    ///
886    /// Edge cases:
887    /// * If `thickness` is negative and greater than the size of the input, it is clamped
888    ///   (so that the returned [`GridAab`] never extends beyond the opposite face of
889    ///   `self`).
890    ///
891    /// For example, it may be used to construct the walls of a room:
892    ///
893    /// ```
894    /// # extern crate all_is_cubes_base as all_is_cubes;
895    /// use all_is_cubes::math::{GridAab, Face6};
896    ///
897    /// let interior = GridAab::from_lower_upper([10, 10, 10], [20, 20, 20]);
898    /// let left_wall = interior.abut(Face6::NX, 2)?;
899    /// let right_wall = interior.abut(Face6::PX, 2)?;
900    ///
901    /// assert_eq!(left_wall, GridAab::from_lower_upper([8, 10, 10], [10, 20, 20]));
902    /// assert_eq!(right_wall, GridAab::from_lower_upper([20, 10, 10], [22, 20, 20]));
903    /// # Ok::<(), all_is_cubes::math::GridOverflowError>(())
904    /// ```
905    ///
906    /// Example of negative thickness:
907    ///
908    /// ```
909    /// # extern crate all_is_cubes_base as all_is_cubes;
910    /// # use all_is_cubes::math::{GridAab, Face6};
911    ///
912    /// let b = GridAab::from_lower_upper([10, 10, 10], [20, 20, 20]);
913    /// assert_eq!(
914    ///     b.abut(Face6::PX, -3)?,
915    ///     GridAab::from_lower_upper([17, 10, 10], [20, 20, 20]),
916    /// );
917    /// assert_eq!(
918    ///     // Thicker than the input, therefore clamped.
919    ///     b.abut(Face6::PX, -30)?,
920    ///     GridAab::from_lower_upper([10, 10, 10], [20, 20, 20]),
921    /// );
922    /// # Ok::<(), all_is_cubes::math::GridOverflowError>(())
923    /// ```
924    #[inline]
925    pub fn abut(self, face: Face6, thickness: GridCoordinate) -> Result<Self, GridOverflowError> {
926        let axis = face.axis();
927
928        // Apply change in size.
929        let mut size = self.size();
930        size[axis] = match GridSizeCoord::try_from(thickness) {
931            // If thickness is nonnegative, the new size is defined by it directly.
932            Ok(positive) => positive,
933            Err(_) => {
934                // If negative, the new size cannot be larger than the old size.
935                // The tricky part is handling GridCoordinate::MIN, which cannot be
936                // directly negated without overflow -- so we use unsigned_abs() to do it.
937                thickness.unsigned_abs().min(size[axis])
938            }
939        };
940
941        // Coordinate on the axis that the two boxes share
942        let abutting_coordinate = if face.is_positive() {
943            self.upper_bounds()[axis]
944        } else {
945            // TODO: better error message
946            self.lower_bounds[axis].checked_sub(thickness).ok_or(GridOverflowError(
947                OverflowKind::OverflowedAbut {
948                    original: self,
949                    face,
950                    thickness,
951                },
952            ))?
953        };
954
955        let mut lower_bounds = self.lower_bounds();
956        let new_lower_bound = if thickness.is_positive() {
957            abutting_coordinate
958        } else {
959            // Cannot overflow because we already min()ed it.
960            abutting_coordinate.wrapping_sub_unsigned(size[axis])
961        };
962        lower_bounds[axis] = new_lower_bound;
963
964        GridAab::checked_from_lower_size(lower_bounds, size)
965    }
966}
967
968impl fmt::Debug for GridAab {
969    #[allow(clippy::missing_inline_in_public_items)]
970    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
971        f.debug_tuple("GridAab")
972            .field(&RangeWithLength(self.x_range()))
973            .field(&RangeWithLength(self.y_range()))
974            .field(&RangeWithLength(self.z_range()))
975            .finish()
976    }
977}
978
979impl From<GridAab> for Aab {
980    /// Converts `value` to floating-point coordinates.
981    ///
982    /// This conversion is also available as [`GridAab::to_free()`],
983    /// which may be more convenient in a method chain.
984    #[inline]
985    fn from(value: GridAab) -> Self {
986        value.to_free()
987    }
988}
989
990impl From<GridAab> for euclid::Box3D<GridCoordinate, Cube> {
991    #[inline]
992    fn from(aab: GridAab) -> Self {
993        Self {
994            min: aab.lower_bounds(),
995            max: aab.upper_bounds(),
996        }
997    }
998}
999
1000#[cfg(feature = "arbitrary")]
1001#[mutants::skip]
1002impl<'a> arbitrary::Arbitrary<'a> for GridAab {
1003    #[allow(clippy::missing_inline_in_public_items)]
1004    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
1005        Ok(Vol::<()>::arbitrary_with_max_volume(u, usize::MAX)?.bounds())
1006    }
1007
1008    #[allow(clippy::missing_inline_in_public_items)]
1009    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
1010        crate::math::vol::vol_arb::ARBITRARY_BOUNDS_SIZE_HINT
1011    }
1012}
1013
1014/// Error when a [`GridAab`] or [`Cube`] cannot be constructed from the given input.
1015#[derive(Clone, Copy, Debug, displaydoc::Display, Eq, PartialEq)]
1016#[displaydoc("{0}")]
1017pub struct GridOverflowError(OverflowKind);
1018
1019/// Error details for [`GridOverflowError`].
1020#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1021enum OverflowKind {
1022    Inverted {
1023        lower_bounds: GridPoint,
1024        upper_bounds: GridPoint,
1025    },
1026    OverflowedSize {
1027        lower_bounds: GridPoint,
1028        size: GridSize,
1029    },
1030    // TODO: implement this specific error
1031    // NegativeSize {
1032    //     lower_bounds: GridPoint,
1033    //     size: GridSize,
1034    // },
1035    OverflowedAbut {
1036        original: GridAab,
1037        face: Face6,
1038        thickness: GridCoordinate,
1039    },
1040}
1041
1042impl fmt::Display for OverflowKind {
1043    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1044        match self {
1045            OverflowKind::Inverted {
1046                lower_bounds,
1047                upper_bounds,
1048            } => {
1049                write!(
1050                    f,
1051                    "GridAab's lower bounds {} were greater than upper bounds {}",
1052                    lower_bounds.refmt(&ConciseDebug),
1053                    upper_bounds.refmt(&ConciseDebug)
1054                )
1055            }
1056            OverflowKind::OverflowedSize { lower_bounds, size } => {
1057                write!(
1058                    f,
1059                    "GridAab's size {size} plus lower bounds {lower_bounds} \
1060                        produced {upper_bounds} which overflows",
1061                    lower_bounds = lower_bounds.refmt(&ConciseDebug),
1062                    // Do the math in i64, which is big enough not to overflow.
1063                    upper_bounds = (lower_bounds.to_i64() + size.to_i64()).refmt(&ConciseDebug),
1064                    size = size.refmt(&ConciseDebug),
1065                )
1066            }
1067            // OverflowKind::NegativeSize {
1068            //     lower_bounds: _,
1069            //     size,
1070            // } => {
1071            //     write!(
1072            //         f,
1073            //         "GridAab's size {size} cannot be negative",
1074            //         size = size.refmt(&ConciseDebug),
1075            //     )
1076            // }
1077            OverflowKind::OverflowedAbut {
1078                original,
1079                face,
1080                thickness,
1081            } => {
1082                write!(
1083                    f,
1084                    // TODO: don't use Debug format here
1085                    "extending {face:?} face of {original:?} by {thickness:+} overflowed",
1086                )
1087            }
1088        }
1089    }
1090}
1091
1092impl core::error::Error for GridOverflowError {}
1093
1094/// `Debug`-formatting helper
1095struct RangeWithLength(Range<GridCoordinate>);
1096impl fmt::Debug for RangeWithLength {
1097    #[allow(clippy::missing_inline_in_public_items)]
1098    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1099        let range = &self.0;
1100        if f.alternate() {
1101            write!(
1102                f,
1103                "{range:?} ({len})",
1104                len = i64::from(range.end) - i64::from(range.start)
1105            )
1106        } else {
1107            range.fmt(f)
1108        }
1109    }
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115    use crate::math::{GridRotation, ZMaj};
1116    use crate::resolution::Resolution::*;
1117    use alloc::string::ToString as _;
1118    use euclid::{point3, size3};
1119    use indoc::indoc;
1120
1121    #[test]
1122    fn zero_is_valid() {
1123        assert_eq!(
1124            GridAab::from_lower_size([1, 2, 3], [0, 1, 1]),
1125            GridAab::from_lower_upper([1, 2, 3], [1, 3, 4]),
1126        );
1127
1128        assert_eq!(
1129            GridAab::from_lower_size([1, 2, 3], [0, 1, 1]).volume(),
1130            Some(0)
1131        );
1132    }
1133
1134    #[test]
1135    fn for_block() {
1136        assert_eq!(
1137            GridAab::for_block(R1),
1138            GridAab::from_lower_size([0, 0, 0], [1, 1, 1])
1139        );
1140        assert_eq!(
1141            GridAab::for_block(R16),
1142            GridAab::from_lower_size([0, 0, 0], [16, 16, 16])
1143        );
1144        assert_eq!(
1145            GridAab::for_block(R128),
1146            GridAab::from_lower_size([0, 0, 0], [128, 128, 128])
1147        );
1148    }
1149
1150    #[test]
1151    fn tiny() {
1152        assert_eq!(
1153            GridAab::tiny(point3(-10, 0, 10), size3(0, 1, 255)),
1154            GridAab::from_lower_size(point3(-10, 0, 10), size3(0, 1, 255))
1155        )
1156    }
1157
1158    #[test]
1159    fn divide_to_one_cube() {
1160        assert_eq!(
1161            GridAab::from_lower_size([11, 22, 33], [1, 1, 1]).divide(10),
1162            GridAab::from_lower_size([1, 2, 3], [1, 1, 1]),
1163        );
1164    }
1165
1166    #[test]
1167    #[should_panic(expected = "GridAab::divide: divisor must be > 0, not 0")]
1168    fn divide_by_zero() {
1169        let _ = GridAab::from_lower_size([-10, -10, -10], [20, 20, 20]).divide(0);
1170    }
1171
1172    #[test]
1173    #[should_panic(expected = "GridAab::divide: divisor must be > 0, not -10")]
1174    fn divide_by_negative() {
1175        let _ = GridAab::from_lower_size([-10, -10, -10], [20, 20, 20]).divide(-10);
1176    }
1177
1178    #[test]
1179    fn to_vol_error() {
1180        let big = GridAab::from_lower_size([0, 0, 0], GridSize::splat(i32::MAX.cast_unsigned()));
1181        assert_eq!(
1182            big.to_vol::<ZMaj>().unwrap_err().to_string(),
1183            "GridAab(0..2147483647, 0..2147483647, 0..2147483647) has a volume of \
1184                9903520300447984150353281023, which is too large to be linearized"
1185        );
1186    }
1187
1188    #[test]
1189    fn transform_general() {
1190        assert_eq!(
1191            GridAab::from_lower_size([1, 2, 3], [10, 20, 30]).transform(Gridgid {
1192                rotation: GridRotation::RYXz,
1193                translation: GridVector::new(100, 100, 100),
1194            }),
1195            Some(GridAab::from_lower_size([102, 101, 67], [20, 10, 30]))
1196        );
1197    }
1198
1199    // TODO: test transform() on more cases
1200
1201    /// Translation overflowing to partially outside of the numeric range
1202    /// clips the box's size to the range.
1203    #[test]
1204    fn translate_overflow_partial() {
1205        assert_eq!(
1206            GridAab::from_lower_size([0, 0, 0], [100, 20, 30]).translate([
1207                GridCoordinate::MAX - 50,
1208                0,
1209                0
1210            ]),
1211            GridAab::from_lower_size([GridCoordinate::MAX - 50, 0, 0], [50, 20, 30])
1212        );
1213        assert_eq!(
1214            GridAab::from_lower_size([-100, 0, 0], [100, 20, 30]).translate([
1215                GridCoordinate::MIN + 50,
1216                0,
1217                0
1218            ]),
1219            GridAab::from_lower_size([GridCoordinate::MIN, 0, 0], [50, 20, 30])
1220        );
1221    }
1222
1223    /// Translation overflowing to completely outside of the numeric range
1224    /// becomes a zero-volume “squashed” box.
1225    #[test]
1226    fn translate_overflow_total() {
1227        assert_eq!(
1228            GridAab::from_lower_size([100, 0, 0], [100, 20, 30]).translate([
1229                GridCoordinate::MAX - 50,
1230                0,
1231                0
1232            ]),
1233            GridAab::from_lower_size([GridCoordinate::MAX, 0, 0], [0, 20, 30])
1234        );
1235        assert_eq!(
1236            GridAab::from_lower_size([-200, 0, 0], [100, 20, 30]).translate([
1237                GridCoordinate::MIN + 50,
1238                0,
1239                0
1240            ]),
1241            GridAab::from_lower_size([GridCoordinate::MIN, 0, 0], [0, 20, 30])
1242        );
1243    }
1244
1245    /// Test `Debug` formatting. Note this should be similar to the [`Aab`] formatting.
1246    #[test]
1247    fn debug() {
1248        let b = GridAab::from_lower_size([1, 2, 3], [10, 20, 30]);
1249        println!("{b:#?}");
1250        assert_eq!(format!("{b:?}"), "GridAab(1..11, 2..22, 3..33)");
1251        assert_eq!(
1252            format!("{b:#?}\n"),
1253            indoc! {"
1254                GridAab(
1255                    1..11 (10),
1256                    2..22 (20),
1257                    3..33 (30),
1258                )
1259            "}
1260        );
1261    }
1262
1263    // TODO: test overflow error formatting
1264}