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}