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}