1use core::fmt;
2
3#[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#[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 pub const ORIGIN: Self = Self::new(0, 0, 0);
55
56 #[inline]
58 pub const fn new(x: GridCoordinate, y: GridCoordinate, z: GridCoordinate) -> Self {
59 Self { x, y, z }
60 }
61
62 #[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 #[inline] pub fn lower_bounds(self) -> GridPoint {
94 self.into()
95 }
96
97 #[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 #[inline] 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 #[inline]
129 pub fn grid_aab(self) -> GridAab {
130 GridAab::from_lower_size(self.lower_bounds(), [1, 1, 1])
131 }
132
133 #[inline]
145 pub fn aab(self) -> Aab {
146 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 #[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 #[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 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 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 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 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}