1use 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#[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 pub const FULL_BLOCK: Self = Self::new(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
28
29 pub const EMPTY: Self = Self::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
31
32 #[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 pub const fn min_x(self) -> f64 {
59 self.min_x
60 }
61
62 #[must_use]
63 pub const fn min_y(self) -> f64 {
65 self.min_y
66 }
67
68 #[must_use]
69 pub const fn min_z(self) -> f64 {
71 self.min_z
72 }
73
74 #[must_use]
75 pub const fn max_x(self) -> f64 {
77 self.max_x
78 }
79
80 #[must_use]
81 pub const fn max_y(self) -> f64 {
83 self.max_y
84 }
85
86 #[must_use]
87 pub const fn max_z(self) -> f64 {
89 self.max_z
90 }
91
92 #[must_use]
93 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 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 #[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 pub fn width(self) -> f64 {
121 self.max_x - self.min_x
122 }
123
124 #[must_use]
125 pub fn height(self) -> f64 {
127 self.max_y - self.min_y
128 }
129
130 #[must_use]
131 pub fn depth(self) -> f64 {
133 self.max_z - self.min_z
134 }
135
136 #[must_use]
138 pub fn size(self) -> f64 {
139 (self.width() + self.height() + self.depth()) / 3.0
140 }
141
142 #[must_use]
143 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 pub fn inflate(self, amount: f64) -> Self {
158 self.inflate_xyz(amount, amount, amount)
159 }
160
161 #[must_use]
162 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 pub fn deflate(self, amount: f64) -> Self {
177 self.inflate(-amount)
178 }
179
180 #[must_use]
181 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 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 #[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#[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 #[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 #[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 pub const fn min_x(self) -> f64 {
271 self.min_x
272 }
273
274 #[must_use]
275 pub const fn min_y(self) -> f64 {
277 self.min_y
278 }
279
280 #[must_use]
281 pub const fn min_z(self) -> f64 {
283 self.min_z
284 }
285
286 #[must_use]
287 pub const fn max_x(self) -> f64 {
289 self.max_x
290 }
291
292 #[must_use]
293 pub const fn max_y(self) -> f64 {
295 self.max_y
296 }
297
298 #[must_use]
299 pub const fn max_z(self) -> f64 {
301 self.max_z
302 }
303
304 #[must_use]
305 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 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 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 pub fn width(self) -> f64 {
333 self.max_x - self.min_x
334 }
335
336 #[must_use]
337 pub fn height(self) -> f64 {
339 self.max_y - self.min_y
340 }
341
342 #[must_use]
343 pub fn depth(self) -> f64 {
345 self.max_z - self.min_z
346 }
347
348 #[must_use]
349 pub fn size(self) -> f64 {
351 (self.width() + self.height() + self.depth()) / 3.0
352 }
353
354 #[must_use]
355 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 pub fn move_vec(self, delta: DVec3) -> Self {
370 self.move_by(delta.x, delta.y, delta.z)
371 }
372
373 #[must_use]
374 pub fn inflate(self, amount: f64) -> Self {
376 self.inflate_xyz(amount, amount, amount)
377 }
378
379 #[must_use]
380 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 pub fn deflate(self, amount: f64) -> Self {
395 self.inflate(-amount)
396 }
397
398 #[must_use]
399 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 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 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 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 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}