all_is_cubes_base/math/
cube.rs

1use core::fmt;
2
3/// Acts as polyfill for float methods
4#[cfg(not(feature = "std"))]
5#[allow(unused_imports)]
6use num_traits::float::FloatCore as _;
7
8use crate::math::{
9    Aab, Face6, FreeCoordinate, FreePoint, FreeVector, GridAab, GridCoordinate, GridPoint,
10    GridVector,
11};
12use crate::util::ConciseDebug;
13
14/// “A cube”, in this documentation, is a unit cube whose corners' coordinates are integers.
15/// This type identifies such a cube by the coordinates of its most negative corner.
16///
17/// The valid coordinate range is that of [`GridCoordinate`].
18/// Note, however, that in most applications, cubes with lower corner coordinates equal to
19/// [`GridCoordinate::MAX`] will not be valid, because their other corners are out of
20/// range. The [`Cube`] type does not enforce this, because it would be unergonomic to
21/// require fallible conversions there. Instead, the conversion from [`Cube`] to its
22/// bounding [`GridAab`] may panic. Generally, this should be avoided by checking
23/// the cube with [`GridAab::contains_cube()`] on some existing [`GridAab`].
24///
25/// Considered in continuous space (real, or floating-point, coordinates), the ranges of
26/// coordinates a cube contains are half-open intervals: lower inclusive and upper exclusive.
27///
28/// # Representation
29///
30/// This struct is guaranteed to be three `i32` without padding, and so may be reinterpreted
31/// as any type of identical layout such as `[i32; 3]`.
32///
33/// # Why have a dedicated type for this?
34///
35/// * Primarily, to avoid confusion between points (zero size) and cubes (nonzero size)
36///   that causes off-by-one errors when rotating objects.
37/// * To provide convenient methods for operations on cubes that aren't natural operations
38///   on points.
39/// * To reduce our dependence on external math libraries as part of our API.
40#[derive(Clone, Copy, Eq, Hash, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
41#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
42#[allow(missing_docs, clippy::exhaustive_structs)]
43#[repr(C)]
44pub struct Cube {
45    pub x: i32,
46    pub y: i32,
47    pub z: i32,
48}
49
50impl Cube {
51    /// Equal to `Cube::new(0, 0, 0)`.
52    ///
53    /// Note that this is not a box _centered_ on the coordinate origin.
54    pub const ORIGIN: Self = Self::new(0, 0, 0);
55
56    /// Construct `Cube { x, y, z }` from the given coordinates.
57    #[inline]
58    pub const fn new(x: GridCoordinate, y: GridCoordinate, z: GridCoordinate) -> Self {
59        Self { x, y, z }
60    }
61
62    /// Convert a point in space to the unit cube that encloses it.
63    ///
64    /// Such cubes are defined to be half-open intervals on each axis; that is,
65    /// an integer coordinate is counted as part of the cube extending positively
66    /// from that coordinate.
67    ///
68    /// If the point coordinates are outside of the numeric range of [`GridCoordinate`],
69    /// returns [`None`].
70    ///
71    /// ```
72    /// # extern crate all_is_cubes_base as all_is_cubes;
73    /// use all_is_cubes::math::{FreePoint, Cube};
74    ///
75    /// assert_eq!(Cube::containing(FreePoint::new(1.0, 1.5, -2.5)), Some(Cube::new(1, 1, -3)));
76    /// ```
77    #[inline]
78    pub fn containing(point: FreePoint) -> Option<Self> {
79        const RANGE: core::ops::Range<FreeCoordinate> =
80            (GridCoordinate::MIN as FreeCoordinate)..(GridCoordinate::MAX as FreeCoordinate + 1.0);
81
82        if RANGE.contains(&point.x) && RANGE.contains(&point.y) && RANGE.contains(&point.z) {
83            Some(Self::from(
84                point.map(|component| component.floor() as GridCoordinate),
85            ))
86        } else {
87            None
88        }
89    }
90
91    /// Returns the corner of this cube with the most negative coordinates.
92    #[inline] // trivial arithmetic
93    pub fn lower_bounds(self) -> GridPoint {
94        self.into()
95    }
96
97    /// Returns the corner of this cube with the most positive coordinates.
98    ///
99    /// Panics if `self` has any coordinates equal to [`GridCoordinate::MAX`].
100    /// Generally, that should be avoided by checking the cube with
101    /// [`GridAab::contains_cube()`] on some existing [`GridAab`] before calling this
102    /// method.
103    #[inline]
104    #[track_caller]
105    pub fn upper_bounds(self) -> GridPoint {
106        self.checked_add(GridVector::new(1, 1, 1))
107            .expect("Cube::upper_bounds() overflowed")
108            .lower_bounds()
109    }
110
111    /// Returns the midpoint of this cube.
112    #[inline] // trivial arithmetic
113    pub fn midpoint(self) -> FreePoint {
114        let Self { x, y, z } = self;
115        FreePoint::new(
116            FreeCoordinate::from(x) + 0.5,
117            FreeCoordinate::from(y) + 0.5,
118            FreeCoordinate::from(z) + 0.5,
119        )
120    }
121
122    /// Constructs a [`GridAab`] with a volume of 1, containing this cube.
123    ///
124    /// Panics if `self` has any coordinates equal to [`GridCoordinate::MAX`].
125    /// Generally, that should be avoided by checking the cube with
126    /// [`GridAab::contains_cube()`] on some existing [`GridAab`] before calling this
127    /// method.
128    #[inline]
129    pub fn grid_aab(self) -> GridAab {
130        GridAab::from_lower_size(self.lower_bounds(), [1, 1, 1])
131    }
132
133    /// Returns the bounding box in floating-point coordinates containing this cube.
134    ///
135    /// ```
136    /// # extern crate all_is_cubes_base as all_is_cubes;
137    /// use all_is_cubes::math::{Aab, Cube};
138    ///
139    /// assert_eq!(
140    ///     Cube::new(10, 20, -30).aab(),
141    ///     Aab::new(10.0, 11.0, 20.0, 21.0, -30.0, -29.0)
142    /// );
143    /// ```
144    #[inline]
145    pub fn aab(self) -> Aab {
146        // Note: this does not use `.upper_bounds()` so that it is non-panicking.
147        let lower = GridPoint::from(self).map(FreeCoordinate::from);
148        Aab::from_lower_upper(lower, lower + FreeVector::new(1.0, 1.0, 1.0))
149    }
150
151    /// Componentwise [`GridCoordinate::checked_add()`].
152    #[must_use]
153    #[inline]
154    pub fn checked_add(self, v: GridVector) -> Option<Self> {
155        Some(Self {
156            x: self.x.checked_add(v.x)?,
157            y: self.y.checked_add(v.y)?,
158            z: self.z.checked_add(v.z)?,
159        })
160    }
161
162    /// Apply a function to each coordinate independently.
163    ///
164    /// If a different return type is desired, use `.lower_bounds().map(f)` instead.
165    #[expect(clippy::return_self_not_must_use)]
166    #[inline]
167    pub fn map(self, mut f: impl FnMut(GridCoordinate) -> GridCoordinate) -> Self {
168        Self {
169            x: f(self.x),
170            y: f(self.y),
171            z: f(self.z),
172        }
173    }
174}
175
176impl fmt::Debug for Cube {
177    #[allow(clippy::missing_inline_in_public_items)]
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        let Self { x, y, z } = self;
180        write!(f, "({x:+.3?}, {y:+.3?}, {z:+.3?})")
181    }
182}
183impl manyfmt::Fmt<ConciseDebug> for Cube {
184    #[allow(clippy::missing_inline_in_public_items)]
185    fn fmt(&self, f: &mut fmt::Formatter<'_>, _: &ConciseDebug) -> fmt::Result {
186        fmt::Debug::fmt(self, f)
187    }
188}
189
190mod arithmetic {
191    use super::*;
192    use crate::math::Axis;
193    use core::ops;
194
195    impl ops::Add<GridVector> for Cube {
196        type Output = Self;
197        #[inline]
198        fn add(self, rhs: GridVector) -> Self::Output {
199            Self::from(self.lower_bounds() + rhs)
200        }
201    }
202    impl ops::AddAssign<GridVector> for Cube {
203        #[inline]
204        fn add_assign(&mut self, rhs: GridVector) {
205            *self = Self::from(self.lower_bounds() + rhs)
206        }
207    }
208
209    impl ops::Sub<GridVector> for Cube {
210        type Output = Self;
211        #[inline]
212        fn sub(self, rhs: GridVector) -> Self::Output {
213            Self::from(self.lower_bounds() - rhs)
214        }
215    }
216    impl ops::SubAssign<GridVector> for Cube {
217        #[inline]
218        fn sub_assign(&mut self, rhs: GridVector) {
219            *self = Self::from(self.lower_bounds() - rhs)
220        }
221    }
222
223    impl ops::Sub<Cube> for Cube {
224        type Output = GridVector;
225        #[inline]
226        fn sub(self, rhs: Cube) -> Self::Output {
227            self.lower_bounds() - rhs.lower_bounds()
228        }
229    }
230
231    impl ops::Add<Face6> for Cube {
232        type Output = Self;
233        #[inline]
234        fn add(self, rhs: Face6) -> Self::Output {
235            self + rhs.normal_vector()
236        }
237    }
238    impl ops::AddAssign<Face6> for Cube {
239        #[inline]
240        fn add_assign(&mut self, rhs: Face6) {
241            *self += rhs.normal_vector()
242        }
243    }
244
245    impl ops::Index<Axis> for Cube {
246        type Output = GridCoordinate;
247        #[inline]
248        fn index(&self, index: Axis) -> &Self::Output {
249            match index {
250                Axis::X => &self.x,
251                Axis::Y => &self.y,
252                Axis::Z => &self.z,
253            }
254        }
255    }
256    impl ops::IndexMut<Axis> for Cube {
257        #[inline]
258        fn index_mut(&mut self, index: Axis) -> &mut Self::Output {
259            match index {
260                Axis::X => &mut self.x,
261                Axis::Y => &mut self.y,
262                Axis::Z => &mut self.z,
263            }
264        }
265    }
266}
267
268mod conversion {
269    use super::*;
270
271    impl AsRef<[GridCoordinate; 3]> for Cube {
272        #[inline]
273        fn as_ref(&self) -> &[GridCoordinate; 3] {
274            bytemuck::must_cast_ref(self)
275        }
276    }
277    impl AsMut<[GridCoordinate; 3]> for Cube {
278        #[inline]
279        fn as_mut(&mut self) -> &mut [GridCoordinate; 3] {
280            bytemuck::must_cast_mut(self)
281        }
282    }
283    impl core::borrow::Borrow<[GridCoordinate; 3]> for Cube {
284        #[inline]
285        fn borrow(&self) -> &[GridCoordinate; 3] {
286            bytemuck::must_cast_ref(self)
287        }
288    }
289    impl core::borrow::BorrowMut<[GridCoordinate; 3]> for Cube {
290        #[inline]
291        fn borrow_mut(&mut self) -> &mut [GridCoordinate; 3] {
292            bytemuck::must_cast_mut(self)
293        }
294    }
295
296    impl From<Cube> for [GridCoordinate; 3] {
297        #[inline]
298        fn from(Cube { x, y, z }: Cube) -> [GridCoordinate; 3] {
299            [x, y, z]
300        }
301    }
302    impl From<Cube> for GridPoint {
303        #[inline]
304        fn from(Cube { x, y, z }: Cube) -> GridPoint {
305            GridPoint::new(x, y, z)
306        }
307    }
308
309    impl From<[GridCoordinate; 3]> for Cube {
310        #[inline]
311        fn from([x, y, z]: [GridCoordinate; 3]) -> Self {
312            Self { x, y, z }
313        }
314    }
315    impl From<GridPoint> for Cube {
316        #[inline]
317        fn from(GridPoint { x, y, z, _unit }: GridPoint) -> Self {
318            Self { x, y, z }
319        }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use euclid::point3;
327
328    #[test]
329    fn containing_simple() {
330        assert_eq!(
331            Cube::containing(point3(1.5, -2.0, -3.5)),
332            Some(Cube::new(1, -2, -4))
333        );
334    }
335
336    #[test]
337    fn containing_inf() {
338        assert_eq!(
339            Cube::containing(point3(FreeCoordinate::INFINITY, 0., 0.)),
340            None
341        );
342        assert_eq!(
343            Cube::containing(point3(-FreeCoordinate::INFINITY, 0., 0.)),
344            None
345        );
346    }
347
348    #[test]
349    fn containing_nan() {
350        assert_eq!(Cube::containing(point3(0., 0., FreeCoordinate::NAN)), None);
351    }
352
353    #[test]
354    fn containing_in_and_out_of_range() {
355        let fmax = FreeCoordinate::from(GridCoordinate::MAX);
356        let fmin = FreeCoordinate::from(GridCoordinate::MIN);
357
358        // min Z
359        assert_eq!(Cube::containing(point3(0., 0., fmin - 0.001)), None);
360        assert_eq!(
361            Cube::containing(point3(0., 0., fmin + 0.001,)),
362            Some(Cube::new(0, 0, GridCoordinate::MIN))
363        );
364
365        // max Z
366        assert_eq!(
367            Cube::containing(point3(0., 0., fmax + 0.999,)),
368            Some(Cube::new(0, 0, GridCoordinate::MAX))
369        );
370        assert_eq!(Cube::containing(point3(0., 0., fmax + 1.001)), None);
371
372        // max Y (exercise more axes)
373        assert_eq!(
374            Cube::containing(point3(0., fmax + 0.999, 0.)),
375            Some(Cube::new(0, GridCoordinate::MAX, 0))
376        );
377        assert_eq!(Cube::containing(point3(0., fmax + 1.001, 0.)), None);
378
379        // max X
380        assert_eq!(
381            Cube::containing(point3(fmax + 0.999, 0., 0.)),
382            Some(Cube::new(GridCoordinate::MAX, 0, 0))
383        );
384        assert_eq!(Cube::containing(point3(fmax + 1.001, 0., 0.)), None);
385    }
386}