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, 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 (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 pub const ORIGIN: Self = Self::new(0, 0, 0);
66
67 #[inline]
69 pub const fn new(x: GridCoordinate, y: GridCoordinate, z: GridCoordinate) -> Self {
70 Self { x, y, z }
71 }
72
73 #[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 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 #[inline] pub fn lower_bounds(self) -> GridPoint {
119 self.into()
120 }
121
122 #[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 #[inline] 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 #[inline]
154 pub fn grid_aab(self) -> GridAab {
155 GridAab::from_lower_size(self.lower_bounds(), [1, 1, 1])
156 }
157
158 #[inline]
170 pub fn aab(self) -> Aab {
171 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 #[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 #[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 #[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 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 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 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 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}