Skip to main content

steel_utils/
geometry.rs

1//! Geometry primitives shared by registry data, physics, and world queries.
2
3use glam::DVec3;
4
5use crate::{BlockPos, axis::Axis};
6
7const fn ordered_pair(a: f64, b: f64) -> (f64, f64) {
8    if a <= b { (a, b) } else { (b, a) }
9}
10
11/// Block-local axis-aligned box used by voxel shapes.
12///
13/// Coordinates are relative to a block position. Vanilla block shapes are
14/// usually in 0.0..=1.0 space, but some shapes extend outside that range.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct BlockLocalAabb {
17    min_x: f64,
18    min_y: f64,
19    min_z: f64,
20    max_x: f64,
21    max_y: f64,
22    max_z: f64,
23}
24
25impl BlockLocalAabb {
26    /// A full block from `(0, 0, 0)` to `(1, 1, 1)`.
27    pub const FULL_BLOCK: Self = Self::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
28
29    /// A zero-volume box. Empty voxel shapes should prefer an empty box slice.
30    pub const EMPTY: Self = Self::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
31
32    /// Creates a block-local AABB and normalizes endpoint order like vanilla
33    /// `AABB`.
34    #[must_use]
35    pub const fn new(
36        min_x: f64,
37        min_y: f64,
38        min_z: f64,
39        max_x: f64,
40        max_y: f64,
41        max_z: f64,
42    ) -> Self {
43        let (min_x, max_x) = ordered_pair(min_x, max_x);
44        let (min_y, max_y) = ordered_pair(min_y, max_y);
45        let (min_z, max_z) = ordered_pair(min_z, max_z);
46        Self {
47            min_x,
48            min_y,
49            min_z,
50            max_x,
51            max_y,
52            max_z,
53        }
54    }
55
56    #[must_use]
57    /// Returns the minimum X coordinate.
58    pub const fn min_x(self) -> f64 {
59        self.min_x
60    }
61
62    #[must_use]
63    /// Returns the minimum Y coordinate.
64    pub const fn min_y(self) -> f64 {
65        self.min_y
66    }
67
68    #[must_use]
69    /// Returns the minimum Z coordinate.
70    pub const fn min_z(self) -> f64 {
71        self.min_z
72    }
73
74    #[must_use]
75    /// Returns the maximum X coordinate.
76    pub const fn max_x(self) -> f64 {
77        self.max_x
78    }
79
80    #[must_use]
81    /// Returns the maximum Y coordinate.
82    pub const fn max_y(self) -> f64 {
83        self.max_y
84    }
85
86    #[must_use]
87    /// Returns the maximum Z coordinate.
88    pub const fn max_z(self) -> f64 {
89        self.max_z
90    }
91
92    #[must_use]
93    /// Returns the minimum coordinate on `axis`.
94    pub const fn min(self, axis: Axis) -> f64 {
95        match axis {
96            Axis::X => self.min_x,
97            Axis::Y => self.min_y,
98            Axis::Z => self.min_z,
99        }
100    }
101
102    #[must_use]
103    /// Returns the maximum coordinate on `axis`.
104    pub const fn max(self, axis: Axis) -> f64 {
105        match axis {
106            Axis::X => self.max_x,
107            Axis::Y => self.max_y,
108            Axis::Z => self.max_z,
109        }
110    }
111
112    /// Returns true when this box has no positive volume on at least one axis.
113    #[must_use]
114    pub const fn is_empty(self) -> bool {
115        self.min_x >= self.max_x || self.min_y >= self.max_y || self.min_z >= self.max_z
116    }
117
118    #[must_use]
119    /// Returns the X size.
120    pub fn width(self) -> f64 {
121        self.max_x - self.min_x
122    }
123
124    #[must_use]
125    /// Returns the Y size.
126    pub fn height(self) -> f64 {
127        self.max_y - self.min_y
128    }
129
130    #[must_use]
131    /// Returns the Z size.
132    pub fn depth(self) -> f64 {
133        self.max_z - self.min_z
134    }
135
136    /// Vanilla equivalent: `AABB.getSize()`.
137    #[must_use]
138    pub fn size(self) -> f64 {
139        (self.width() + self.height() + self.depth()) / 3.0
140    }
141
142    #[must_use]
143    /// Returns this box translated by the given delta.
144    pub fn move_by(self, dx: f64, dy: f64, dz: f64) -> Self {
145        Self::new(
146            self.min_x + dx,
147            self.min_y + dy,
148            self.min_z + dz,
149            self.max_x + dx,
150            self.max_y + dy,
151            self.max_z + dz,
152        )
153    }
154
155    #[must_use]
156    /// Returns this box expanded by `amount` in every direction.
157    pub fn inflate(self, amount: f64) -> Self {
158        self.inflate_xyz(amount, amount, amount)
159    }
160
161    #[must_use]
162    /// Returns this box expanded independently on each axis.
163    pub fn inflate_xyz(self, x: f64, y: f64, z: f64) -> Self {
164        Self::new(
165            self.min_x - x,
166            self.min_y - y,
167            self.min_z - z,
168            self.max_x + x,
169            self.max_y + y,
170            self.max_z + z,
171        )
172    }
173
174    #[must_use]
175    /// Returns this box shrunk by `amount` in every direction.
176    pub fn deflate(self, amount: f64) -> Self {
177        self.inflate(-amount)
178    }
179
180    #[must_use]
181    /// Returns true if this box intersects `other`.
182    pub fn intersects(self, other: Self) -> bool {
183        self.min_x < other.max_x
184            && self.max_x > other.min_x
185            && self.min_y < other.max_y
186            && self.max_y > other.min_y
187            && self.min_z < other.max_z
188            && self.max_z > other.min_z
189    }
190
191    #[must_use]
192    /// Returns true if the point lies inside this box.
193    pub fn contains(self, x: f64, y: f64, z: f64) -> bool {
194        x >= self.min_x
195            && x < self.max_x
196            && y >= self.min_y
197            && y < self.max_y
198            && z >= self.min_z
199            && z < self.max_z
200    }
201
202    /// Converts this block-local box to a world-space box at `pos`.
203    #[must_use]
204    pub fn at_block(self, pos: BlockPos) -> WorldAabb {
205        let x = f64::from(pos.x());
206        let y = f64::from(pos.y());
207        let z = f64::from(pos.z());
208        WorldAabb::new(
209            x + self.min_x,
210            y + self.min_y,
211            z + self.min_z,
212            x + self.max_x,
213            y + self.max_y,
214            z + self.max_z,
215        )
216    }
217}
218
219/// World-space axis-aligned box used by entity and collision physics.
220#[derive(Debug, Clone, Copy, PartialEq)]
221pub struct WorldAabb {
222    min_x: f64,
223    min_y: f64,
224    min_z: f64,
225    max_x: f64,
226    max_y: f64,
227    max_z: f64,
228}
229
230impl WorldAabb {
231    /// Creates a world-space AABB and normalizes endpoint order like vanilla
232    /// `AABB`.
233    #[must_use]
234    pub const fn new(
235        min_x: f64,
236        min_y: f64,
237        min_z: f64,
238        max_x: f64,
239        max_y: f64,
240        max_z: f64,
241    ) -> Self {
242        let (min_x, max_x) = ordered_pair(min_x, max_x);
243        let (min_y, max_y) = ordered_pair(min_y, max_y);
244        let (min_z, max_z) = ordered_pair(min_z, max_z);
245        Self {
246            min_x,
247            min_y,
248            min_z,
249            max_x,
250            max_y,
251            max_z,
252        }
253    }
254
255    /// Creates an entity bounding box centered on X/Z and using `y` as feet.
256    #[must_use]
257    pub fn entity_box(x: f64, y: f64, z: f64, half_width: f64, height: f64) -> Self {
258        Self::new(
259            x - half_width,
260            y,
261            z - half_width,
262            x + half_width,
263            y + height,
264            z + half_width,
265        )
266    }
267
268    #[must_use]
269    /// Returns the minimum X coordinate.
270    pub const fn min_x(self) -> f64 {
271        self.min_x
272    }
273
274    #[must_use]
275    /// Returns the minimum Y coordinate.
276    pub const fn min_y(self) -> f64 {
277        self.min_y
278    }
279
280    #[must_use]
281    /// Returns the minimum Z coordinate.
282    pub const fn min_z(self) -> f64 {
283        self.min_z
284    }
285
286    #[must_use]
287    /// Returns the maximum X coordinate.
288    pub const fn max_x(self) -> f64 {
289        self.max_x
290    }
291
292    #[must_use]
293    /// Returns the maximum Y coordinate.
294    pub const fn max_y(self) -> f64 {
295        self.max_y
296    }
297
298    #[must_use]
299    /// Returns the maximum Z coordinate.
300    pub const fn max_z(self) -> f64 {
301        self.max_z
302    }
303
304    #[must_use]
305    /// Returns the minimum coordinate on `axis`.
306    pub const fn min(self, axis: Axis) -> f64 {
307        match axis {
308            Axis::X => self.min_x,
309            Axis::Y => self.min_y,
310            Axis::Z => self.min_z,
311        }
312    }
313
314    #[must_use]
315    /// Returns the maximum coordinate on `axis`.
316    pub const fn max(self, axis: Axis) -> f64 {
317        match axis {
318            Axis::X => self.max_x,
319            Axis::Y => self.max_y,
320            Axis::Z => self.max_z,
321        }
322    }
323
324    #[must_use]
325    /// Returns true when this box has no positive volume on at least one axis.
326    pub const fn is_empty(self) -> bool {
327        self.min_x >= self.max_x || self.min_y >= self.max_y || self.min_z >= self.max_z
328    }
329
330    #[must_use]
331    /// Returns the X size.
332    pub fn width(self) -> f64 {
333        self.max_x - self.min_x
334    }
335
336    #[must_use]
337    /// Returns the Y size.
338    pub fn height(self) -> f64 {
339        self.max_y - self.min_y
340    }
341
342    #[must_use]
343    /// Returns the Z size.
344    pub fn depth(self) -> f64 {
345        self.max_z - self.min_z
346    }
347
348    #[must_use]
349    /// Vanilla equivalent: `AABB.getSize()`.
350    pub fn size(self) -> f64 {
351        (self.width() + self.height() + self.depth()) / 3.0
352    }
353
354    #[must_use]
355    /// Returns this box translated by the given delta.
356    pub fn move_by(self, dx: f64, dy: f64, dz: f64) -> Self {
357        Self::new(
358            self.min_x + dx,
359            self.min_y + dy,
360            self.min_z + dz,
361            self.max_x + dx,
362            self.max_y + dy,
363            self.max_z + dz,
364        )
365    }
366
367    #[must_use]
368    /// Returns this box translated by `delta`.
369    pub fn move_vec(self, delta: DVec3) -> Self {
370        self.move_by(delta.x, delta.y, delta.z)
371    }
372
373    #[must_use]
374    /// Returns this box expanded by `amount` in every direction.
375    pub fn inflate(self, amount: f64) -> Self {
376        self.inflate_xyz(amount, amount, amount)
377    }
378
379    #[must_use]
380    /// Returns this box expanded independently on each axis.
381    pub fn inflate_xyz(self, x: f64, y: f64, z: f64) -> Self {
382        Self::new(
383            self.min_x - x,
384            self.min_y - y,
385            self.min_z - z,
386            self.max_x + x,
387            self.max_y + y,
388            self.max_z + z,
389        )
390    }
391
392    #[must_use]
393    /// Returns this box shrunk by `amount` in every direction.
394    pub fn deflate(self, amount: f64) -> Self {
395        self.inflate(-amount)
396    }
397
398    #[must_use]
399    /// Returns this box expanded only in the direction of `delta`.
400    pub fn expand_towards(self, delta: DVec3) -> Self {
401        Self::new(
402            if delta.x < 0.0 {
403                self.min_x + delta.x
404            } else {
405                self.min_x
406            },
407            if delta.y < 0.0 {
408                self.min_y + delta.y
409            } else {
410                self.min_y
411            },
412            if delta.z < 0.0 {
413                self.min_z + delta.z
414            } else {
415                self.min_z
416            },
417            if delta.x > 0.0 {
418                self.max_x + delta.x
419            } else {
420                self.max_x
421            },
422            if delta.y > 0.0 {
423                self.max_y + delta.y
424            } else {
425                self.max_y
426            },
427            if delta.z > 0.0 {
428                self.max_z + delta.z
429            } else {
430                self.max_z
431            },
432        )
433    }
434
435    #[must_use]
436    /// Returns true if this box intersects `other`.
437    pub fn intersects(self, other: Self) -> bool {
438        self.intersects_coords(
439            other.min_x,
440            other.min_y,
441            other.min_z,
442            other.max_x,
443            other.max_y,
444            other.max_z,
445        )
446    }
447
448    #[must_use]
449    /// Returns true if this box intersects the given raw coordinate bounds.
450    pub fn intersects_coords(
451        self,
452        min_x: f64,
453        min_y: f64,
454        min_z: f64,
455        max_x: f64,
456        max_y: f64,
457        max_z: f64,
458    ) -> bool {
459        self.min_x < max_x
460            && self.max_x > min_x
461            && self.min_y < max_y
462            && self.max_y > min_y
463            && self.min_z < max_z
464            && self.max_z > min_z
465    }
466
467    #[must_use]
468    /// Returns true if this box intersects the full block at `pos`.
469    pub fn intersects_block(self, pos: BlockPos) -> bool {
470        self.intersects_coords(
471            f64::from(pos.x()),
472            f64::from(pos.y()),
473            f64::from(pos.z()),
474            f64::from(pos.x()) + 1.0,
475            f64::from(pos.y()) + 1.0,
476            f64::from(pos.z()) + 1.0,
477        )
478    }
479
480    #[must_use]
481    /// Returns true if the point lies inside this box.
482    pub fn contains(self, x: f64, y: f64, z: f64) -> bool {
483        x >= self.min_x
484            && x < self.max_x
485            && y >= self.min_y
486            && y < self.max_y
487            && z >= self.min_z
488            && z < self.max_z
489    }
490}
491
492#[cfg(test)]
493#[expect(
494    clippy::float_cmp,
495    reason = "geometry constructors use exact test values"
496)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn constructors_normalize_endpoints_like_vanilla() {
502        let aabb = WorldAabb::new(3.0, 4.0, 5.0, 1.0, 2.0, 0.0);
503        assert_eq!(aabb.min_x(), 1.0);
504        assert_eq!(aabb.min_y(), 2.0);
505        assert_eq!(aabb.min_z(), 0.0);
506        assert_eq!(aabb.max_x(), 3.0);
507        assert_eq!(aabb.max_y(), 4.0);
508        assert_eq!(aabb.max_z(), 5.0);
509    }
510
511    #[test]
512    fn block_local_aabb_translates_to_world_space() {
513        let local = BlockLocalAabb::new(0.0, 0.25, 0.0, 1.0, 0.75, 1.0);
514        let world = local.at_block(BlockPos::new(10, 64, -5));
515
516        assert_eq!(world.min_x(), 10.0);
517        assert_eq!(world.min_y(), 64.25);
518        assert_eq!(world.min_z(), -5.0);
519        assert_eq!(world.max_x(), 11.0);
520        assert_eq!(world.max_y(), 64.75);
521        assert_eq!(world.max_z(), -4.0);
522    }
523
524    #[test]
525    fn contains_uses_vanilla_exclusive_max_edge() {
526        let aabb = WorldAabb::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
527
528        assert!(aabb.contains(0.0, 0.5, 0.5));
529        assert!(aabb.contains(0.999, 0.5, 0.5));
530        assert!(!aabb.contains(1.0, 0.5, 0.5));
531    }
532
533    #[test]
534    fn expand_towards_covers_start_and_end() {
535        let aabb = WorldAabb::new(1.0, 1.0, 1.0, 2.0, 2.0, 2.0);
536        let swept = aabb.expand_towards(DVec3::new(-0.5, 1.5, 0.0));
537
538        assert_eq!(swept.min_x(), 0.5);
539        assert_eq!(swept.min_y(), 1.0);
540        assert_eq!(swept.min_z(), 1.0);
541        assert_eq!(swept.max_x(), 2.0);
542        assert_eq!(swept.max_y(), 3.5);
543        assert_eq!(swept.max_z(), 2.0);
544    }
545}