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