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, 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 core::hash::Hash for Cube {
51    #[inline]
52    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
53        // Hashers work on 64-bit quantities.
54        // Therefore, it may be more efficient to provide fewer inputs by packing the data into
55        // chunks of at most 64 bits.
56        (u64::from(self.x.cast_unsigned()) ^ (u64::from(self.y.cast_unsigned()) << 32)).hash(state);
57        self.z.hash(state);
58    }
59}
60
61impl Cube {
62    /// Equal to `Cube::new(0, 0, 0)`.
63    ///
64    /// Note that this is not a box _centered_ on the coordinate origin.
65    pub const ORIGIN: Self = Self::new(0, 0, 0);
66
67    /// Construct `Cube { x, y, z }` from the given coordinates.
68    #[inline]
69    pub const fn new(x: GridCoordinate, y: GridCoordinate, z: GridCoordinate) -> Self {
70        Self { x, y, z }
71    }
72
73    /// Convert a point in space to the unit cube that encloses it.
74    ///
75    /// Such cubes are defined to be half-open intervals on each axis; that is,
76    /// an integer coordinate is counted as part of the cube extending positively
77    /// from that coordinate.
78    ///
79    /// If the point coordinates are outside of the numeric range of [`GridCoordinate`],
80    /// returns [`None`].
81    ///
82    /// ```
83    /// # extern crate all_is_cubes_base as all_is_cubes;
84    /// use all_is_cubes::math::{FreePoint, Cube};
85    ///
86    /// assert_eq!(Cube::containing(FreePoint::new(1.0, 1.5, -2.5)), Some(Cube::new(1, 1, -3)));
87    /// ```
88    //---
89    // This is not a `const fn` because `f64::floor()` is not available in `core` and
90    // `FloatCore::floor()` is not a `const fn`.
91    #[inline]
92    pub fn containing(point: FreePoint) -> Option<Self> {
93        const MIN_INCLUSIVE: FreeCoordinate = GridCoordinate::MIN as FreeCoordinate;
94        const MAX_EXCLUSIVE: FreeCoordinate = GridCoordinate::MAX as FreeCoordinate + 1.0;
95
96        let FreePoint { x, y, z, .. } = point;
97
98        // No short-circuiting because, assuming success is likely, all tests will need to run.
99        if (MIN_INCLUSIVE <= x)
100            & (MIN_INCLUSIVE <= y)
101            & (MIN_INCLUSIVE <= z)
102            & (x < MAX_EXCLUSIVE)
103            & (y < MAX_EXCLUSIVE)
104            & (z < MAX_EXCLUSIVE)
105        {
106            Some(Self {
107                x: x.floor() as GridCoordinate,
108                y: y.floor() as GridCoordinate,
109                z: z.floor() as GridCoordinate,
110            })
111        } else {
112            None
113        }
114    }
115
116    /// Returns the corner of this cube with the most negative coordinates.
117    #[inline] // trivial arithmetic
118    pub fn lower_bounds(self) -> GridPoint {
119        self.into()
120    }
121
122    /// Returns the corner of this cube with the most positive coordinates.
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    #[track_caller]
130    pub fn upper_bounds(self) -> GridPoint {
131        self.checked_add(GridVector::new(1, 1, 1))
132            .expect("Cube::upper_bounds() overflowed")
133            .lower_bounds()
134    }
135
136    /// Returns the center of this cube.
137    #[inline] // trivial arithmetic
138    pub fn center(self) -> FreePoint {
139        let Self { x, y, z } = self;
140        FreePoint::new(
141            FreeCoordinate::from(x) + 0.5,
142            FreeCoordinate::from(y) + 0.5,
143            FreeCoordinate::from(z) + 0.5,
144        )
145    }
146
147    /// Constructs a [`GridAab`] with a volume of 1, containing this cube.
148    ///
149    /// Panics if `self` has any coordinates equal to [`GridCoordinate::MAX`].
150    /// Generally, that should be avoided by checking the cube with
151    /// [`GridAab::contains_cube()`] on some existing [`GridAab`] before calling this
152    /// method.
153    #[inline]
154    pub fn grid_aab(self) -> GridAab {
155        GridAab::from_lower_size(self.lower_bounds(), [1, 1, 1])
156    }
157
158    /// Returns the bounding box in floating-point coordinates containing this cube.
159    ///
160    /// ```
161    /// # extern crate all_is_cubes_base as all_is_cubes;
162    /// use all_is_cubes::math::{Aab, Cube};
163    ///
164    /// assert_eq!(
165    ///     Cube::new(10, 20, -30).aab(),
166    ///     Aab::new(10.0, 11.0, 20.0, 21.0, -30.0, -29.0)
167    /// );
168    /// ```
169    #[inline]
170    pub fn aab(self) -> Aab {
171        // Note: this does not use `.upper_bounds()` so that it is non-panicking.
172        let lower = GridPoint::from(self).map(FreeCoordinate::from);
173        Aab::from_lower_upper(lower, lower + FreeVector::new(1.0, 1.0, 1.0))
174    }
175
176    /// Componentwise [`GridCoordinate::checked_add()`].
177    #[must_use]
178    #[inline]
179    pub fn checked_add(self, v: GridVector) -> Option<Self> {
180        Some(Self {
181            x: self.x.checked_add(v.x)?,
182            y: self.y.checked_add(v.y)?,
183            z: self.z.checked_add(v.z)?,
184        })
185    }
186
187    /// Componentwise [`GridCoordinate::wrapping_add()`].
188    #[must_use]
189    #[inline]
190    pub fn wrapping_add(self, v: GridVector) -> Self {
191        Self {
192            x: self.x.wrapping_add(v.x),
193            y: self.y.wrapping_add(v.y),
194            z: self.z.wrapping_add(v.z),
195        }
196    }
197
198    /// Apply a function to each coordinate independently.
199    ///
200    /// If a different return type is desired, use `.lower_bounds().map(f)` instead.
201    #[expect(clippy::return_self_not_must_use)]
202    #[inline]
203    pub fn map(self, mut f: impl FnMut(GridCoordinate) -> GridCoordinate) -> Self {
204        Self {
205            x: f(self.x),
206            y: f(self.y),
207            z: f(self.z),
208        }
209    }
210}
211
212impl fmt::Debug for Cube {
213    #[allow(clippy::missing_inline_in_public_items)]
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        let Self { x, y, z } = self;
216        write!(f, "({x:+.3?}, {y:+.3?}, {z:+.3?})")
217    }
218}
219impl manyfmt::Fmt<ConciseDebug> for Cube {
220    #[allow(clippy::missing_inline_in_public_items)]
221    fn fmt(&self, f: &mut fmt::Formatter<'_>, _: &ConciseDebug) -> fmt::Result {
222        fmt::Debug::fmt(self, f)
223    }
224}
225
226mod arithmetic {
227    use super::*;
228    use crate::math::Axis;
229    use core::ops;
230
231    impl ops::Add<GridVector> for Cube {
232        type Output = Self;
233        #[inline]
234        fn add(self, rhs: GridVector) -> Self::Output {
235            Self::from(self.lower_bounds() + rhs)
236        }
237    }
238    impl ops::AddAssign<GridVector> for Cube {
239        #[inline]
240        fn add_assign(&mut self, rhs: GridVector) {
241            *self = Self::from(self.lower_bounds() + rhs)
242        }
243    }
244
245    impl ops::Sub<GridVector> for Cube {
246        type Output = Self;
247        #[inline]
248        fn sub(self, rhs: GridVector) -> Self::Output {
249            Self::from(self.lower_bounds() - rhs)
250        }
251    }
252    impl ops::SubAssign<GridVector> for Cube {
253        #[inline]
254        fn sub_assign(&mut self, rhs: GridVector) {
255            *self = Self::from(self.lower_bounds() - rhs)
256        }
257    }
258
259    impl ops::Sub<Cube> for Cube {
260        type Output = GridVector;
261        #[inline]
262        fn sub(self, rhs: Cube) -> Self::Output {
263            self.lower_bounds() - rhs.lower_bounds()
264        }
265    }
266
267    impl ops::Add<Face6> for Cube {
268        type Output = Self;
269        #[inline]
270        fn add(self, rhs: Face6) -> Self::Output {
271            self + rhs.normal_vector()
272        }
273    }
274    impl ops::AddAssign<Face6> for Cube {
275        #[inline]
276        fn add_assign(&mut self, rhs: Face6) {
277            *self += rhs.normal_vector()
278        }
279    }
280
281    impl ops::Index<Axis> for Cube {
282        type Output = GridCoordinate;
283        #[inline]
284        fn index(&self, index: Axis) -> &Self::Output {
285            match index {
286                Axis::X => &self.x,
287                Axis::Y => &self.y,
288                Axis::Z => &self.z,
289            }
290        }
291    }
292    impl ops::IndexMut<Axis> for Cube {
293        #[inline]
294        fn index_mut(&mut self, index: Axis) -> &mut Self::Output {
295            match index {
296                Axis::X => &mut self.x,
297                Axis::Y => &mut self.y,
298                Axis::Z => &mut self.z,
299            }
300        }
301    }
302}
303
304mod conversion {
305    use super::*;
306
307    impl AsRef<[GridCoordinate; 3]> for Cube {
308        #[inline]
309        fn as_ref(&self) -> &[GridCoordinate; 3] {
310            bytemuck::must_cast_ref(self)
311        }
312    }
313    impl AsMut<[GridCoordinate; 3]> for Cube {
314        #[inline]
315        fn as_mut(&mut self) -> &mut [GridCoordinate; 3] {
316            bytemuck::must_cast_mut(self)
317        }
318    }
319
320    impl From<Cube> for [GridCoordinate; 3] {
321        #[inline]
322        fn from(Cube { x, y, z }: Cube) -> [GridCoordinate; 3] {
323            [x, y, z]
324        }
325    }
326    impl From<Cube> for GridPoint {
327        #[inline]
328        fn from(Cube { x, y, z }: Cube) -> GridPoint {
329            GridPoint::new(x, y, z)
330        }
331    }
332
333    impl From<[GridCoordinate; 3]> for Cube {
334        #[inline]
335        fn from([x, y, z]: [GridCoordinate; 3]) -> Self {
336            Self { x, y, z }
337        }
338    }
339    impl From<GridPoint> for Cube {
340        #[inline]
341        fn from(GridPoint { x, y, z, _unit }: GridPoint) -> Self {
342            Self { x, y, z }
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use euclid::point3;
351
352    #[test]
353    fn containing_simple() {
354        assert_eq!(
355            Cube::containing(point3(1.5, -2.0, -3.5)),
356            Some(Cube::new(1, -2, -4))
357        );
358    }
359
360    #[test]
361    fn containing_inf() {
362        assert_eq!(
363            Cube::containing(point3(FreeCoordinate::INFINITY, 0., 0.)),
364            None
365        );
366        assert_eq!(
367            Cube::containing(point3(-FreeCoordinate::INFINITY, 0., 0.)),
368            None
369        );
370    }
371
372    #[test]
373    fn containing_nan() {
374        assert_eq!(Cube::containing(point3(0., 0., FreeCoordinate::NAN)), None);
375    }
376
377    #[test]
378    fn containing_in_and_out_of_range() {
379        let fmax = FreeCoordinate::from(GridCoordinate::MAX);
380        let fmin = FreeCoordinate::from(GridCoordinate::MIN);
381
382        // min Z
383        assert_eq!(Cube::containing(point3(0., 0., fmin - 0.001)), None);
384        assert_eq!(
385            Cube::containing(point3(0., 0., fmin + 0.001,)),
386            Some(Cube::new(0, 0, GridCoordinate::MIN))
387        );
388
389        // max Z
390        assert_eq!(
391            Cube::containing(point3(0., 0., fmax + 0.999,)),
392            Some(Cube::new(0, 0, GridCoordinate::MAX))
393        );
394        assert_eq!(Cube::containing(point3(0., 0., fmax + 1.001)), None);
395
396        // max Y (exercise more axes)
397        assert_eq!(
398            Cube::containing(point3(0., fmax + 0.999, 0.)),
399            Some(Cube::new(0, GridCoordinate::MAX, 0))
400        );
401        assert_eq!(Cube::containing(point3(0., fmax + 1.001, 0.)), None);
402
403        // max X
404        assert_eq!(
405            Cube::containing(point3(fmax + 0.999, 0., 0.)),
406            Some(Cube::new(GridCoordinate::MAX, 0, 0))
407        );
408        assert_eq!(Cube::containing(point3(fmax + 1.001, 0., 0.)), None);
409    }
410}