1use steel_utils::{BlockLocalAabb, axis::Axis};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum BooleanOp {
10 False,
11 NotOr,
12 OnlySecond,
13 NotFirst,
14 OnlyFirst,
15 NotSecond,
16 NotSame,
17 NotAnd,
18 And,
19 Same,
20 Second,
21 Causes,
22 First,
23 CausedBy,
24 Or,
25 True,
26}
27
28impl BooleanOp {
29 #[must_use]
30 pub const fn apply(self, first: bool, second: bool) -> bool {
31 match self {
32 Self::False => false,
33 Self::NotOr => !first && !second,
34 Self::OnlySecond => second && !first,
35 Self::NotFirst => !first,
36 Self::OnlyFirst => first && !second,
37 Self::NotSecond => !second,
38 Self::NotSame => first != second,
39 Self::NotAnd => !first || !second,
40 Self::And => first && second,
41 Self::Same => first == second,
42 Self::Second => second,
43 Self::Causes => !first || second,
44 Self::First => first,
45 Self::CausedBy => first || !second,
46 Self::Or => first || second,
47 Self::True => true,
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct VoxelShape {
59 boxes: &'static [BlockLocalAabb],
60}
61
62impl VoxelShape {
63 pub const EMPTY: Self = Self::from_boxes(&[]);
65
66 pub const FULL_BLOCK: Self = Self::from_boxes(FULL_BLOCK_BOXES);
68
69 #[must_use]
71 pub const fn from_boxes(boxes: &'static [BlockLocalAabb]) -> Self {
72 Self { boxes }
73 }
74
75 #[must_use]
77 pub const fn boxes(self) -> &'static [BlockLocalAabb] {
78 self.boxes
79 }
80
81 pub fn iter(self) -> core::slice::Iter<'static, BlockLocalAabb> {
83 self.boxes.iter()
84 }
85
86 #[must_use]
88 pub const fn len(self) -> usize {
89 self.boxes.len()
90 }
91
92 #[must_use]
94 pub fn is_empty(self) -> bool {
95 self.boxes.iter().all(|aabb| aabb.is_empty())
96 }
97
98 #[must_use]
100 pub fn min(self, axis: Axis) -> f64 {
101 self.boxes
102 .iter()
103 .filter(|aabb| !aabb.is_empty())
104 .map(|aabb| aabb.min(axis))
105 .fold(f64::INFINITY, f64::min)
106 }
107
108 #[must_use]
110 pub fn max(self, axis: Axis) -> f64 {
111 self.boxes
112 .iter()
113 .filter(|aabb| !aabb.is_empty())
114 .map(|aabb| aabb.max(axis))
115 .fold(f64::NEG_INFINITY, f64::max)
116 }
117
118 #[must_use]
120 pub fn bounds(self) -> Option<BlockLocalAabb> {
121 let first = self.boxes.iter().find(|aabb| !aabb.is_empty())?;
122 let mut min_x = first.min_x();
123 let mut min_y = first.min_y();
124 let mut min_z = first.min_z();
125 let mut max_x = first.max_x();
126 let mut max_y = first.max_y();
127 let mut max_z = first.max_z();
128
129 for aabb in self.boxes {
130 if aabb.is_empty() {
131 continue;
132 }
133 min_x = min_x.min(aabb.min_x());
134 min_y = min_y.min(aabb.min_y());
135 min_z = min_z.min(aabb.min_z());
136 max_x = max_x.max(aabb.max_x());
137 max_y = max_y.max(aabb.max_y());
138 max_z = max_z.max(aabb.max_z());
139 }
140
141 Some(BlockLocalAabb::new(
142 min_x, min_y, min_z, max_x, max_y, max_z,
143 ))
144 }
145
146 #[must_use]
151 pub fn has_large_collision_shape(self) -> bool {
152 [Axis::X, Axis::Y, Axis::Z]
153 .into_iter()
154 .any(|axis| self.min(axis) < 0.0 || self.max(axis) > 1.0)
155 }
156}
157
158impl IntoIterator for VoxelShape {
159 type IntoIter = core::slice::Iter<'static, BlockLocalAabb>;
160 type Item = &'static BlockLocalAabb;
161
162 fn into_iter(self) -> Self::IntoIter {
163 self.iter()
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub struct ShapeId(pub u16);
173
174impl ShapeId {
175 pub const EMPTY: ShapeId = ShapeId(0);
177
178 pub const FULL_BLOCK: ShapeId = ShapeId(1);
180}
181
182pub struct ShapeRegistry {
190 shapes: Vec<VoxelShape>,
191 allows_registering: bool,
192}
193
194impl Default for ShapeRegistry {
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl ShapeRegistry {
201 #[must_use]
203 pub fn new() -> Self {
204 let mut registry = Self {
205 shapes: Vec::new(),
206 allows_registering: true,
207 };
208
209 let empty_id = registry.register(VoxelShape::EMPTY);
211 debug_assert_eq!(empty_id, ShapeId::EMPTY);
212
213 let full_id = registry.register(VoxelShape::FULL_BLOCK);
214 debug_assert_eq!(full_id, ShapeId::FULL_BLOCK);
215
216 registry
217 }
218
219 pub fn register(&mut self, shape: VoxelShape) -> ShapeId {
224 assert!(
225 self.allows_registering,
226 "Cannot register shapes after the registry has been frozen"
227 );
228
229 let id = ShapeId(self.shapes.len() as u16);
230 self.shapes.push(shape);
231 id
232 }
233
234 #[must_use]
238 pub fn get(&self, id: ShapeId) -> VoxelShape {
239 self.shapes
240 .get(id.0 as usize)
241 .copied()
242 .unwrap_or(VoxelShape::EMPTY)
243 }
244
245 #[must_use]
247 pub fn len(&self) -> usize {
248 self.shapes.len()
249 }
250
251 #[must_use]
253 pub fn is_empty(&self) -> bool {
254 self.shapes.is_empty()
255 }
256
257 pub fn freeze(&mut self) {
259 self.allows_registering = false;
260 }
261}
262
263const FULL_BLOCK_BOXES: &[BlockLocalAabb] = &[BlockLocalAabb::FULL_BLOCK];
264
265const VOXEL_EPSILON: f64 = 1.0e-7;
266
267#[derive(Debug, Clone, Copy)]
269pub struct BlockShapes {
270 pub collision: VoxelShape,
271 pub support: VoxelShape,
272 pub outline: VoxelShape,
273 pub occlusion: VoxelShape,
274 pub interaction: VoxelShape,
275 pub visual: VoxelShape,
276}
277
278impl BlockShapes {
279 #[must_use]
281 pub const fn new(
282 collision: VoxelShape,
283 support: VoxelShape,
284 outline: VoxelShape,
285 occlusion: VoxelShape,
286 interaction: VoxelShape,
287 visual: VoxelShape,
288 ) -> Self {
289 Self {
290 collision,
291 support,
292 outline,
293 occlusion,
294 interaction,
295 visual,
296 }
297 }
298
299 pub const FULL_BLOCK: BlockShapes = BlockShapes::new(
301 VoxelShape::FULL_BLOCK,
302 VoxelShape::FULL_BLOCK,
303 VoxelShape::FULL_BLOCK,
304 VoxelShape::FULL_BLOCK,
305 VoxelShape::EMPTY,
306 VoxelShape::FULL_BLOCK,
307 );
308
309 pub const EMPTY: BlockShapes = BlockShapes::new(
311 VoxelShape::EMPTY,
312 VoxelShape::EMPTY,
313 VoxelShape::EMPTY,
314 VoxelShape::EMPTY,
315 VoxelShape::EMPTY,
316 VoxelShape::EMPTY,
317 );
318}
319
320use super::properties::Direction;
321
322#[must_use]
326pub fn bounding_box(shape: VoxelShape) -> BlockLocalAabb {
327 match shape.bounds() {
328 Some(bounds) => bounds,
329 None => panic!("bounding_box called on empty shape"),
330 }
331}
332
333#[must_use]
338pub fn is_shape_full_block(shape: VoxelShape) -> bool {
339 !join_is_not_empty(VoxelShape::FULL_BLOCK, shape, BooleanOp::NotSame)
340}
341
342#[must_use]
353pub fn join_is_not_empty(first: VoxelShape, second: VoxelShape, op: BooleanOp) -> bool {
354 if op.apply(false, false) {
355 panic!("join_is_not_empty cannot use an operation that includes empty outside space");
356 }
357
358 let first_empty = first.is_empty();
359 let second_empty = second.is_empty();
360 if first_empty || second_empty {
361 return op.apply(!first_empty, !second_empty);
362 }
363
364 if first == second {
365 return op.apply(true, true);
366 }
367
368 let first_only_matters = op.apply(true, false);
369 let second_only_matters = op.apply(false, true);
370 for axis in [Axis::X, Axis::Y, Axis::Z] {
371 if first.max(axis) < second.min(axis) - VOXEL_EPSILON {
372 return first_only_matters || second_only_matters;
373 }
374 if second.max(axis) < first.min(axis) - VOXEL_EPSILON {
375 return first_only_matters || second_only_matters;
376 }
377 }
378
379 let mut x_edges = shape_edges(first, second, Axis::X);
380 let mut y_edges = shape_edges(first, second, Axis::Y);
381 let mut z_edges = shape_edges(first, second, Axis::Z);
382 sort_and_dedup_voxel_edges(&mut x_edges);
383 sort_and_dedup_voxel_edges(&mut y_edges);
384 sort_and_dedup_voxel_edges(&mut z_edges);
385
386 for x in x_edges.windows(2) {
387 if x[1] - x[0] <= VOXEL_EPSILON {
388 continue;
389 }
390 for y in y_edges.windows(2) {
391 if y[1] - y[0] <= VOXEL_EPSILON {
392 continue;
393 }
394 for z in z_edges.windows(2) {
395 if z[1] - z[0] <= VOXEL_EPSILON {
396 continue;
397 }
398 let first_full = shape_fills_cell(first, x[0], x[1], y[0], y[1], z[0], z[1]);
399 let second_full = shape_fills_cell(second, x[0], x[1], y[0], y[1], z[0], z[1]);
400 if op.apply(first_full, second_full) {
401 return true;
402 }
403 }
404 }
405 }
406
407 false
408}
409
410#[must_use]
419pub fn join_unoptimized_boxes(
420 first: VoxelShape,
421 second: VoxelShape,
422 op: BooleanOp,
423) -> Vec<BlockLocalAabb> {
424 if op.apply(false, false) {
425 panic!("join_unoptimized_boxes cannot use an operation that includes empty outside space");
426 }
427
428 if first.is_empty() && second.is_empty() {
429 return Vec::new();
430 }
431
432 let mut x_edges = shape_edges(first, second, Axis::X);
433 let mut y_edges = shape_edges(first, second, Axis::Y);
434 let mut z_edges = shape_edges(first, second, Axis::Z);
435 sort_and_dedup_voxel_edges(&mut x_edges);
436 sort_and_dedup_voxel_edges(&mut y_edges);
437 sort_and_dedup_voxel_edges(&mut z_edges);
438
439 let mut boxes = Vec::new();
440 for x in x_edges.windows(2) {
441 if x[1] - x[0] <= VOXEL_EPSILON {
442 continue;
443 }
444 for y in y_edges.windows(2) {
445 if y[1] - y[0] <= VOXEL_EPSILON {
446 continue;
447 }
448 for z in z_edges.windows(2) {
449 if z[1] - z[0] <= VOXEL_EPSILON {
450 continue;
451 }
452
453 let first_full = shape_fills_cell(first, x[0], x[1], y[0], y[1], z[0], z[1]);
454 let second_full = shape_fills_cell(second, x[0], x[1], y[0], y[1], z[0], z[1]);
455 if op.apply(first_full, second_full) {
456 boxes.push(BlockLocalAabb::new(x[0], y[0], z[0], x[1], y[1], z[1]));
457 }
458 }
459 }
460 }
461
462 boxes
463}
464
465fn shape_edges(first: VoxelShape, second: VoxelShape, axis: Axis) -> Vec<f64> {
466 let mut edges = Vec::with_capacity((first.len() + second.len()) * 2);
467 for shape in [first, second] {
468 for aabb in shape {
469 if aabb.is_empty() {
470 continue;
471 }
472 edges.push(aabb.min(axis));
473 edges.push(aabb.max(axis));
474 }
475 }
476 edges
477}
478
479fn sort_and_dedup_voxel_edges(edges: &mut Vec<f64>) {
480 edges.sort_by(|a, b| a.total_cmp(b));
481 edges.dedup_by(|a, b| (*a - *b).abs() <= VOXEL_EPSILON);
482}
483
484fn shape_fills_cell(
485 shape: VoxelShape,
486 min_x: f64,
487 max_x: f64,
488 min_y: f64,
489 max_y: f64,
490 min_z: f64,
491 max_z: f64,
492) -> bool {
493 shape.into_iter().any(|aabb| {
494 !aabb.is_empty()
495 && aabb.min_x() <= min_x + VOXEL_EPSILON
496 && aabb.max_x() >= max_x - VOXEL_EPSILON
497 && aabb.min_y() <= min_y + VOXEL_EPSILON
498 && aabb.max_y() >= max_y - VOXEL_EPSILON
499 && aabb.min_z() <= min_z + VOXEL_EPSILON
500 && aabb.max_z() >= max_z - VOXEL_EPSILON
501 })
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
509pub enum SupportType {
510 Full,
513 Center,
516 Rigid,
519}
520
521const CENTER_SUPPORT_MIN: f64 = 7.0 / 16.0;
523const CENTER_SUPPORT_MAX: f64 = 9.0 / 16.0;
524const CENTER_SUPPORT_Y_MAX: f64 = 10.0 / 16.0;
525
526const RIGID_BORDER: f64 = 0.125; #[must_use]
534pub fn is_face_full(shape: VoxelShape, direction: Direction) -> bool {
535 face_rectangles_cover(shape, direction, 0.0, 1.0, 0.0, 1.0)
536}
537
538#[must_use]
542pub fn is_face_center_supported(shape: VoxelShape, direction: Direction) -> bool {
543 if shape.is_empty() {
544 return false;
545 }
546
547 match direction {
548 Direction::Down | Direction::Up => face_rectangles_cover(
549 shape,
550 direction,
551 CENTER_SUPPORT_MIN,
552 CENTER_SUPPORT_MAX,
553 CENTER_SUPPORT_MIN,
554 CENTER_SUPPORT_MAX,
555 ),
556 Direction::North | Direction::South => face_rectangles_cover(
557 shape,
558 direction,
559 CENTER_SUPPORT_MIN,
560 CENTER_SUPPORT_MAX,
561 0.0,
562 CENTER_SUPPORT_Y_MAX,
563 ),
564 Direction::West | Direction::East => face_rectangles_cover(
565 shape,
566 direction,
567 0.0,
568 CENTER_SUPPORT_Y_MAX,
569 CENTER_SUPPORT_MIN,
570 CENTER_SUPPORT_MAX,
571 ),
572 }
573}
574
575#[must_use]
579pub fn is_face_rigid_supported(shape: VoxelShape, direction: Direction) -> bool {
580 if shape.is_empty() {
581 return false;
582 }
583
584 match direction {
585 Direction::Down | Direction::Up => {
586 face_rectangles_cover(shape, direction, 0.0, RIGID_BORDER, 0.0, 1.0)
587 && face_rectangles_cover(shape, direction, 1.0 - RIGID_BORDER, 1.0, 0.0, 1.0)
588 && face_rectangles_cover(
589 shape,
590 direction,
591 RIGID_BORDER,
592 1.0 - RIGID_BORDER,
593 0.0,
594 RIGID_BORDER,
595 )
596 && face_rectangles_cover(
597 shape,
598 direction,
599 RIGID_BORDER,
600 1.0 - RIGID_BORDER,
601 1.0 - RIGID_BORDER,
602 1.0,
603 )
604 }
605 Direction::North | Direction::South | Direction::West | Direction::East => {
606 is_face_full(shape, direction)
607 }
608 }
609}
610
611#[must_use]
613pub fn is_face_sturdy(shape: VoxelShape, direction: Direction, support_type: SupportType) -> bool {
614 match support_type {
615 SupportType::Full => is_face_full(shape, direction),
616 SupportType::Center => is_face_center_supported(shape, direction),
617 SupportType::Rigid => is_face_rigid_supported(shape, direction),
618 }
619}
620
621#[derive(Clone, Copy)]
622struct FaceRect {
623 min_a: f64,
624 max_a: f64,
625 min_b: f64,
626 max_b: f64,
627}
628
629const FACE_EPSILON: f64 = 1.0e-6;
630
631fn face_rectangles_cover(
632 shape: VoxelShape,
633 direction: Direction,
634 target_min_a: f64,
635 target_max_a: f64,
636 target_min_b: f64,
637 target_max_b: f64,
638) -> bool {
639 let mut rects = Vec::new();
640 for aabb in shape {
641 let Some(rect) = face_rect_for_aabb(*aabb, direction) else {
642 continue;
643 };
644 if rect.max_a <= target_min_a
645 || rect.min_a >= target_max_a
646 || rect.max_b <= target_min_b
647 || rect.min_b >= target_max_b
648 {
649 continue;
650 }
651 rects.push(FaceRect {
652 min_a: rect.min_a.max(target_min_a),
653 max_a: rect.max_a.min(target_max_a),
654 min_b: rect.min_b.max(target_min_b),
655 max_b: rect.max_b.min(target_max_b),
656 });
657 }
658
659 if rects.is_empty() {
660 return false;
661 }
662
663 let mut a_edges = vec![target_min_a, target_max_a];
664 let mut b_edges = vec![target_min_b, target_max_b];
665 for rect in &rects {
666 a_edges.push(rect.min_a);
667 a_edges.push(rect.max_a);
668 b_edges.push(rect.min_b);
669 b_edges.push(rect.max_b);
670 }
671 sort_and_dedup_edges(&mut a_edges);
672 sort_and_dedup_edges(&mut b_edges);
673
674 for a_pair in a_edges.windows(2) {
675 if a_pair[1] - a_pair[0] <= FACE_EPSILON {
676 continue;
677 }
678 for b_pair in b_edges.windows(2) {
679 if b_pair[1] - b_pair[0] <= FACE_EPSILON {
680 continue;
681 }
682 let covered = rects.iter().any(|rect| {
683 rect.min_a <= a_pair[0] + FACE_EPSILON
684 && rect.max_a >= a_pair[1] - FACE_EPSILON
685 && rect.min_b <= b_pair[0] + FACE_EPSILON
686 && rect.max_b >= b_pair[1] - FACE_EPSILON
687 });
688 if !covered {
689 return false;
690 }
691 }
692 }
693
694 true
695}
696
697fn face_rect_for_aabb(aabb: BlockLocalAabb, direction: Direction) -> Option<FaceRect> {
698 let rect = match direction {
699 Direction::Down if aabb.min_y() <= FACE_EPSILON => FaceRect {
700 min_a: aabb.min_x(),
701 max_a: aabb.max_x(),
702 min_b: aabb.min_z(),
703 max_b: aabb.max_z(),
704 },
705 Direction::Up if aabb.max_y() >= 1.0 - FACE_EPSILON => FaceRect {
706 min_a: aabb.min_x(),
707 max_a: aabb.max_x(),
708 min_b: aabb.min_z(),
709 max_b: aabb.max_z(),
710 },
711 Direction::North if aabb.min_z() <= FACE_EPSILON => FaceRect {
712 min_a: aabb.min_x(),
713 max_a: aabb.max_x(),
714 min_b: aabb.min_y(),
715 max_b: aabb.max_y(),
716 },
717 Direction::South if aabb.max_z() >= 1.0 - FACE_EPSILON => FaceRect {
718 min_a: aabb.min_x(),
719 max_a: aabb.max_x(),
720 min_b: aabb.min_y(),
721 max_b: aabb.max_y(),
722 },
723 Direction::West if aabb.min_x() <= FACE_EPSILON => FaceRect {
724 min_a: aabb.min_y(),
725 max_a: aabb.max_y(),
726 min_b: aabb.min_z(),
727 max_b: aabb.max_z(),
728 },
729 Direction::East if aabb.max_x() >= 1.0 - FACE_EPSILON => FaceRect {
730 min_a: aabb.min_y(),
731 max_a: aabb.max_y(),
732 min_b: aabb.min_z(),
733 max_b: aabb.max_z(),
734 },
735 _ => return None,
736 };
737
738 if rect.min_a >= rect.max_a || rect.min_b >= rect.max_b {
739 return None;
740 }
741 Some(rect)
742}
743
744fn sort_and_dedup_edges(edges: &mut Vec<f64>) {
745 edges.sort_by(|a, b| a.total_cmp(b));
746 edges.dedup_by(|a, b| (*a - *b).abs() <= FACE_EPSILON);
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 const QUADRANT_TOP_FACE: &[BlockLocalAabb] = &[
754 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.5, 1.0, 0.5),
755 BlockLocalAabb::new(0.5, 0.5, 0.0, 1.0, 1.0, 0.5),
756 BlockLocalAabb::new(0.0, 0.5, 0.5, 0.5, 1.0, 1.0),
757 BlockLocalAabb::new(0.5, 0.5, 0.5, 1.0, 1.0, 1.0),
758 ];
759
760 const GAPPED_TOP_FACE: &[BlockLocalAabb] = &[
761 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.45, 1.0, 1.0),
762 BlockLocalAabb::new(0.55, 0.5, 0.0, 1.0, 1.0, 1.0),
763 ];
764
765 const VANILLA_AZALEA_SHAPE: &[BlockLocalAabb] = &[
766 BlockLocalAabb::new(0.375, 0.0, 0.375, 0.625, 1.0, 0.625),
767 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.375, 1.0, 1.0),
768 BlockLocalAabb::new(0.375, 0.5, 0.0, 1.0, 1.0, 0.375),
769 BlockLocalAabb::new(0.375, 0.5, 0.625, 1.0, 1.0, 1.0),
770 BlockLocalAabb::new(0.625, 0.5, 0.375, 1.0, 1.0, 0.625),
771 ];
772
773 const SPLIT_FULL_BLOCK: &[BlockLocalAabb] = &[
774 BlockLocalAabb::new(0.0, 0.0, 0.0, 0.5, 1.0, 1.0),
775 BlockLocalAabb::new(0.5, 0.0, 0.0, 1.0, 1.0, 1.0),
776 ];
777
778 const LOWER_HALF_BLOCK: &[BlockLocalAabb] =
779 &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.5, 1.0)];
780
781 const UPPER_HALF_BLOCK: &[BlockLocalAabb] =
782 &[BlockLocalAabb::new(0.0, 0.5, 0.0, 1.0, 1.0, 1.0)];
783
784 const OVERLAPPING_HALF_BLOCKS: &[BlockLocalAabb] = &[
785 BlockLocalAabb::new(0.0, 0.0, 0.0, 0.75, 1.0, 1.0),
786 BlockLocalAabb::new(0.25, 0.0, 0.0, 1.0, 1.0, 1.0),
787 ];
788
789 const ZERO_VOLUME_BOX: &[BlockLocalAabb] = &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.0, 1.0)];
790
791 const LARGE_COLLISION_SHAPE: &[BlockLocalAabb] =
792 &[BlockLocalAabb::new(-0.25, 0.0, 0.0, 1.0, 1.0, 1.0)];
793
794 const RIGID_TOP_RING: &[BlockLocalAabb] = &[
795 BlockLocalAabb::new(0.0, 0.0, 0.0, RIGID_BORDER, 1.0, 1.0),
796 BlockLocalAabb::new(1.0 - RIGID_BORDER, 0.0, 0.0, 1.0, 1.0, 1.0),
797 BlockLocalAabb::new(
798 RIGID_BORDER,
799 0.0,
800 0.0,
801 1.0 - RIGID_BORDER,
802 1.0,
803 RIGID_BORDER,
804 ),
805 BlockLocalAabb::new(
806 RIGID_BORDER,
807 0.0,
808 1.0 - RIGID_BORDER,
809 1.0 - RIGID_BORDER,
810 1.0,
811 1.0,
812 ),
813 ];
814
815 const RIGID_CENTER_PANEL: &[BlockLocalAabb] = &[BlockLocalAabb::new(
816 RIGID_BORDER,
817 0.0,
818 RIGID_BORDER,
819 1.0 - RIGID_BORDER,
820 1.0,
821 1.0 - RIGID_BORDER,
822 )];
823
824 const RIGID_WEST_FACE_RING: &[BlockLocalAabb] = &[
825 BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, RIGID_BORDER, 1.0),
826 BlockLocalAabb::new(0.0, 1.0 - RIGID_BORDER, 0.0, 1.0, 1.0, 1.0),
827 BlockLocalAabb::new(
828 0.0,
829 RIGID_BORDER,
830 0.0,
831 1.0,
832 1.0 - RIGID_BORDER,
833 RIGID_BORDER,
834 ),
835 BlockLocalAabb::new(
836 0.0,
837 RIGID_BORDER,
838 1.0 - RIGID_BORDER,
839 1.0,
840 1.0 - RIGID_BORDER,
841 1.0,
842 ),
843 ];
844
845 #[test]
846 fn boolean_op_matches_vanilla_truth_table() {
847 assert!(BooleanOp::OnlyFirst.apply(true, false));
848 assert!(!BooleanOp::OnlyFirst.apply(false, true));
849 assert!(BooleanOp::NotSame.apply(true, false));
850 assert!(!BooleanOp::NotSame.apply(true, true));
851 assert!(BooleanOp::Or.apply(false, true));
852 assert!(!BooleanOp::And.apply(true, false));
853 }
854
855 #[test]
856 fn join_is_not_empty_detects_intersection() {
857 assert!(join_is_not_empty(
858 VoxelShape::from_boxes(OVERLAPPING_HALF_BLOCKS),
859 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
860 BooleanOp::And
861 ));
862 }
863
864 #[test]
865 fn join_is_not_empty_rejects_disjoint_and() {
866 assert!(!join_is_not_empty(
867 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
868 VoxelShape::from_boxes(UPPER_HALF_BLOCK),
869 BooleanOp::And
870 ));
871 }
872
873 #[test]
874 fn join_is_not_empty_detects_only_first_remainder() {
875 assert!(join_is_not_empty(
876 VoxelShape::FULL_BLOCK,
877 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
878 BooleanOp::OnlyFirst
879 ));
880 }
881
882 #[test]
883 fn join_unoptimized_boxes_materializes_only_second_remainder() {
884 let remainder = join_unoptimized_boxes(
885 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
886 VoxelShape::FULL_BLOCK,
887 BooleanOp::OnlySecond,
888 );
889
890 assert_eq!(
891 remainder,
892 vec![BlockLocalAabb::new(0.0, 0.5, 0.0, 1.0, 1.0, 1.0)]
893 );
894 }
895
896 #[test]
897 fn shape_full_block_accepts_tiled_boxes() {
898 assert!(is_shape_full_block(VoxelShape::from_boxes(
899 SPLIT_FULL_BLOCK
900 )));
901 }
902
903 #[test]
904 fn shape_full_block_rejects_partial_boxes() {
905 assert!(!is_shape_full_block(VoxelShape::from_boxes(
906 LOWER_HALF_BLOCK
907 )));
908 }
909
910 #[test]
911 fn zero_volume_boxes_are_empty() {
912 assert!(VoxelShape::from_boxes(ZERO_VOLUME_BOX).is_empty());
913 assert!(!join_is_not_empty(
914 VoxelShape::from_boxes(ZERO_VOLUME_BOX),
915 VoxelShape::FULL_BLOCK,
916 BooleanOp::And
917 ));
918 }
919
920 #[test]
921 fn large_collision_shape_matches_vanilla_bounds_rule() {
922 assert!(!VoxelShape::EMPTY.has_large_collision_shape());
923 assert!(!VoxelShape::FULL_BLOCK.has_large_collision_shape());
924 assert!(VoxelShape::from_boxes(LARGE_COLLISION_SHAPE).has_large_collision_shape());
925 }
926
927 #[test]
928 fn face_full_accepts_union_covering_face() {
929 assert!(is_face_full(
930 VoxelShape::from_boxes(QUADRANT_TOP_FACE),
931 Direction::Up
932 ));
933 }
934
935 #[test]
936 fn face_full_rejects_union_with_gap() {
937 assert!(!is_face_full(
938 VoxelShape::from_boxes(GAPPED_TOP_FACE),
939 Direction::Up
940 ));
941 }
942
943 #[test]
944 fn face_full_accepts_vanilla_azalea_top_shape() {
945 assert!(is_face_full(
946 VoxelShape::from_boxes(VANILLA_AZALEA_SHAPE),
947 Direction::Up
948 ));
949 }
950
951 #[test]
952 fn rigid_support_accepts_border_ring_covered_by_multiple_boxes() {
953 assert!(is_face_rigid_supported(
954 VoxelShape::from_boxes(RIGID_TOP_RING),
955 Direction::Up
956 ));
957 }
958
959 #[test]
960 fn rigid_support_rejects_center_panel_without_border_ring() {
961 assert!(!is_face_rigid_supported(
962 VoxelShape::from_boxes(RIGID_CENTER_PANEL),
963 Direction::Up
964 ));
965 }
966
967 #[test]
968 fn rigid_support_rejects_side_border_ring_without_full_face() {
969 assert!(!is_face_rigid_supported(
970 VoxelShape::from_boxes(RIGID_WEST_FACE_RING),
971 Direction::West
972 ));
973 }
974}