1use glam::DVec3;
2use smallvec::{SmallVec, smallvec};
3use steel_utils::{BlockLocalAabb, axis::Axis};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BooleanOp {
12 False,
13 NotOr,
14 OnlySecond,
15 NotFirst,
16 OnlyFirst,
17 NotSecond,
18 NotSame,
19 NotAnd,
20 And,
21 Same,
22 Second,
23 Causes,
24 First,
25 CausedBy,
26 Or,
27 True,
28}
29
30impl BooleanOp {
31 #[must_use]
32 pub const fn apply(self, first: bool, second: bool) -> bool {
33 match self {
34 Self::False => false,
35 Self::NotOr => !first && !second,
36 Self::OnlySecond => second && !first,
37 Self::NotFirst => !first,
38 Self::OnlyFirst => first && !second,
39 Self::NotSecond => !second,
40 Self::NotSame => first != second,
41 Self::NotAnd => !first || !second,
42 Self::And => first && second,
43 Self::Same => first == second,
44 Self::Second => second,
45 Self::Causes => !first || second,
46 Self::First => first,
47 Self::CausedBy => first || !second,
48 Self::Or => first || second,
49 Self::True => true,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct VoxelShape {
61 boxes: &'static [BlockLocalAabb],
62}
63
64impl VoxelShape {
65 pub const EMPTY: Self = Self::from_boxes(&[]);
67
68 pub const FULL_BLOCK: Self = Self::from_boxes(FULL_BLOCK_BOXES);
70
71 #[must_use]
73 pub const fn from_boxes(boxes: &'static [BlockLocalAabb]) -> Self {
74 Self { boxes }
75 }
76
77 #[must_use]
79 pub const fn boxes(self) -> &'static [BlockLocalAabb] {
80 self.boxes
81 }
82
83 pub fn iter(self) -> core::slice::Iter<'static, BlockLocalAabb> {
85 self.boxes.iter()
86 }
87
88 #[must_use]
90 pub const fn len(self) -> usize {
91 self.boxes.len()
92 }
93
94 #[must_use]
96 pub fn is_empty(self) -> bool {
97 self.boxes.iter().all(steel_utils::geometry::Aabb::is_empty)
98 }
99
100 #[must_use]
102 pub fn min(self, axis: Axis) -> f64 {
103 self.boxes
104 .iter()
105 .filter(|aabb| !aabb.is_empty())
106 .map(|aabb| aabb.min(axis))
107 .fold(f64::INFINITY, f64::min)
108 }
109
110 #[must_use]
112 pub fn max(self, axis: Axis) -> f64 {
113 self.boxes
114 .iter()
115 .filter(|aabb| !aabb.is_empty())
116 .map(|aabb| aabb.max(axis))
117 .fold(f64::NEG_INFINITY, f64::max)
118 }
119
120 #[must_use]
122 pub fn bounds(self) -> Option<BlockLocalAabb> {
123 let first = self.boxes.iter().find(|aabb| !aabb.is_empty())?;
124 let mut min_x = first.min_x();
125 let mut min_y = first.min_y();
126 let mut min_z = first.min_z();
127 let mut max_x = first.max_x();
128 let mut max_y = first.max_y();
129 let mut max_z = first.max_z();
130
131 for aabb in self.boxes {
132 if aabb.is_empty() {
133 continue;
134 }
135 min_x = min_x.min(aabb.min_x());
136 min_y = min_y.min(aabb.min_y());
137 min_z = min_z.min(aabb.min_z());
138 max_x = max_x.max(aabb.max_x());
139 max_y = max_y.max(aabb.max_y());
140 max_z = max_z.max(aabb.max_z());
141 }
142
143 Some(BlockLocalAabb::new(
144 min_x, min_y, min_z, max_x, max_y, max_z,
145 ))
146 }
147
148 #[must_use]
153 pub fn has_large_collision_shape(self) -> bool {
154 [Axis::X, Axis::Y, Axis::Z]
155 .into_iter()
156 .any(|axis| self.min(axis) < 0.0 || self.max(axis) > 1.0)
157 }
158}
159
160impl IntoIterator for VoxelShape {
161 type IntoIter = core::slice::Iter<'static, BlockLocalAabb>;
162 type Item = &'static BlockLocalAabb;
163
164 fn into_iter(self) -> Self::IntoIter {
165 self.iter()
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq)]
171pub struct OffsetVoxelShape {
172 shape: VoxelShape,
173 offset: DVec3,
174}
175
176impl OffsetVoxelShape {
177 #[must_use]
178 pub const fn new(shape: VoxelShape, offset: DVec3) -> Self {
179 Self { shape, offset }
180 }
181
182 #[must_use]
183 pub const fn without_offset(shape: VoxelShape) -> Self {
184 Self {
185 shape,
186 offset: DVec3::ZERO,
187 }
188 }
189
190 #[must_use]
191 pub const fn shape(self) -> VoxelShape {
192 self.shape
193 }
194
195 #[must_use]
196 pub const fn offset(self) -> DVec3 {
197 self.offset
198 }
199
200 #[must_use]
201 pub fn is_empty(self) -> bool {
202 self.shape.is_empty()
203 }
204
205 pub fn iter(self) -> impl Iterator<Item = BlockLocalAabb> {
206 self.shape
207 .into_iter()
208 .map(move |aabb| aabb.translate(self.offset))
209 }
210
211 #[must_use]
212 pub fn min(self, axis: Axis) -> f64 {
213 self.shape.min(axis) + axis_offset(self.offset, axis)
214 }
215
216 #[must_use]
217 pub fn max(self, axis: Axis) -> f64 {
218 self.shape.max(axis) + axis_offset(self.offset, axis)
219 }
220
221 #[must_use]
222 pub fn bounds(self) -> Option<BlockLocalAabb> {
223 self.shape
224 .bounds()
225 .map(|bounds| bounds.translate(self.offset))
226 }
227
228 #[must_use]
229 pub fn has_large_collision_shape(self) -> bool {
230 [Axis::X, Axis::Y, Axis::Z]
231 .into_iter()
232 .any(|axis| self.min(axis) < 0.0 || self.max(axis) > 1.0)
233 }
234}
235
236const fn axis_offset(offset: DVec3, axis: Axis) -> f64 {
237 match axis {
238 Axis::X => offset.x,
239 Axis::Y => offset.y,
240 Axis::Z => offset.z,
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct ShapeId(pub u16);
250
251impl ShapeId {
252 pub const EMPTY: ShapeId = ShapeId(0);
254
255 pub const FULL_BLOCK: ShapeId = ShapeId(1);
257}
258
259pub struct ShapeRegistry {
267 shapes: Vec<VoxelShape>,
268 allows_registering: bool,
269}
270
271impl Default for ShapeRegistry {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl ShapeRegistry {
278 #[must_use]
280 pub fn new() -> Self {
281 let mut registry = Self {
282 shapes: Vec::new(),
283 allows_registering: true,
284 };
285
286 let empty_id = registry.register(VoxelShape::EMPTY);
288 debug_assert_eq!(empty_id, ShapeId::EMPTY);
289
290 let full_id = registry.register(VoxelShape::FULL_BLOCK);
291 debug_assert_eq!(full_id, ShapeId::FULL_BLOCK);
292
293 registry
294 }
295
296 pub fn register(&mut self, shape: VoxelShape) -> ShapeId {
301 assert!(
302 self.allows_registering,
303 "Cannot register shapes after the registry has been frozen"
304 );
305
306 let id = ShapeId(self.shapes.len() as u16);
307 self.shapes.push(shape);
308 id
309 }
310
311 #[must_use]
315 pub fn get(&self, id: ShapeId) -> VoxelShape {
316 self.shapes
317 .get(id.0 as usize)
318 .copied()
319 .unwrap_or(VoxelShape::EMPTY)
320 }
321
322 #[must_use]
324 pub const fn len(&self) -> usize {
325 self.shapes.len()
326 }
327
328 #[must_use]
330 pub const fn is_empty(&self) -> bool {
331 self.shapes.is_empty()
332 }
333
334 pub const fn freeze(&mut self) {
336 self.allows_registering = false;
337 }
338}
339
340const FULL_BLOCK_BOXES: &[BlockLocalAabb] = &[BlockLocalAabb::FULL_BLOCK];
341
342const VOXEL_EPSILON: f64 = 1.0e-7;
343
344#[derive(Debug, Clone, Copy)]
346pub struct BlockShapes {
347 pub collision: VoxelShape,
348 pub support: VoxelShape,
349 pub outline: VoxelShape,
350 pub occlusion: VoxelShape,
351 pub interaction: VoxelShape,
352 pub visual: VoxelShape,
353}
354
355impl BlockShapes {
356 #[must_use]
358 pub const fn new(
359 collision: VoxelShape,
360 support: VoxelShape,
361 outline: VoxelShape,
362 occlusion: VoxelShape,
363 interaction: VoxelShape,
364 visual: VoxelShape,
365 ) -> Self {
366 Self {
367 collision,
368 support,
369 outline,
370 occlusion,
371 interaction,
372 visual,
373 }
374 }
375
376 pub const FULL_BLOCK: BlockShapes = BlockShapes::new(
378 VoxelShape::FULL_BLOCK,
379 VoxelShape::FULL_BLOCK,
380 VoxelShape::FULL_BLOCK,
381 VoxelShape::FULL_BLOCK,
382 VoxelShape::EMPTY,
383 VoxelShape::FULL_BLOCK,
384 );
385
386 pub const EMPTY: BlockShapes = BlockShapes::new(
388 VoxelShape::EMPTY,
389 VoxelShape::EMPTY,
390 VoxelShape::EMPTY,
391 VoxelShape::EMPTY,
392 VoxelShape::EMPTY,
393 VoxelShape::EMPTY,
394 );
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum ShapeChannel {
400 Collision,
401 Support,
402 Outline,
403 Occlusion,
404 Interaction,
405 Visual,
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct ShapeOffsetFlags {
411 collision: bool,
412 support: bool,
413 outline: bool,
414 occlusion: bool,
415 interaction: bool,
416 visual: bool,
417}
418
419impl ShapeOffsetFlags {
420 pub const NONE: Self = Self::new(false, false, false, false, false, false);
421
422 #[must_use]
423 pub const fn new(
424 collision: bool,
425 support: bool,
426 outline: bool,
427 occlusion: bool,
428 interaction: bool,
429 visual: bool,
430 ) -> Self {
431 Self {
432 collision,
433 support,
434 outline,
435 occlusion,
436 interaction,
437 visual,
438 }
439 }
440
441 #[must_use]
442 pub const fn uses_offset(self, channel: ShapeChannel) -> bool {
443 match channel {
444 ShapeChannel::Collision => self.collision,
445 ShapeChannel::Support => self.support,
446 ShapeChannel::Outline => self.outline,
447 ShapeChannel::Occlusion => self.occlusion,
448 ShapeChannel::Interaction => self.interaction,
449 ShapeChannel::Visual => self.visual,
450 }
451 }
452}
453
454use super::properties::Direction;
455
456#[must_use]
460pub fn bounding_box(shape: VoxelShape) -> BlockLocalAabb {
461 match shape.bounds() {
462 Some(bounds) => bounds,
463 None => panic!("bounding_box called on empty shape"),
464 }
465}
466
467#[must_use]
472pub fn is_shape_full_block(shape: VoxelShape) -> bool {
473 !join_is_not_empty(VoxelShape::FULL_BLOCK, shape, BooleanOp::NotSame)
474}
475
476#[must_use]
477pub fn is_offset_shape_full_block(shape: OffsetVoxelShape) -> bool {
478 if shape.offset == DVec3::ZERO {
479 return is_shape_full_block(shape.shape);
480 }
481
482 if shape.is_empty()
483 || shape.min(Axis::X) > VOXEL_EPSILON
484 || shape.max(Axis::X) < 1.0 - VOXEL_EPSILON
485 || shape.min(Axis::Y) > VOXEL_EPSILON
486 || shape.max(Axis::Y) < 1.0 - VOXEL_EPSILON
487 || shape.min(Axis::Z) > VOXEL_EPSILON
488 || shape.max(Axis::Z) < 1.0 - VOXEL_EPSILON
489 {
490 return false;
491 }
492
493 let mut x_edges = vec![0.0, 1.0];
494 let mut y_edges = vec![0.0, 1.0];
495 let mut z_edges = vec![0.0, 1.0];
496 for aabb in shape.iter() {
497 if aabb.is_empty() {
498 continue;
499 }
500 if aabb.max_x() > VOXEL_EPSILON && aabb.min_x() < 1.0 - VOXEL_EPSILON {
501 x_edges.push(aabb.min_x().clamp(0.0, 1.0));
502 x_edges.push(aabb.max_x().clamp(0.0, 1.0));
503 }
504 if aabb.max_y() > VOXEL_EPSILON && aabb.min_y() < 1.0 - VOXEL_EPSILON {
505 y_edges.push(aabb.min_y().clamp(0.0, 1.0));
506 y_edges.push(aabb.max_y().clamp(0.0, 1.0));
507 }
508 if aabb.max_z() > VOXEL_EPSILON && aabb.min_z() < 1.0 - VOXEL_EPSILON {
509 z_edges.push(aabb.min_z().clamp(0.0, 1.0));
510 z_edges.push(aabb.max_z().clamp(0.0, 1.0));
511 }
512 }
513 sort_and_dedup_voxel_edges(&mut x_edges);
514 sort_and_dedup_voxel_edges(&mut y_edges);
515 sort_and_dedup_voxel_edges(&mut z_edges);
516
517 for x in x_edges.windows(2) {
518 if x[1] - x[0] <= VOXEL_EPSILON {
519 continue;
520 }
521 for y in y_edges.windows(2) {
522 if y[1] - y[0] <= VOXEL_EPSILON {
523 continue;
524 }
525 for z in z_edges.windows(2) {
526 if z[1] - z[0] <= VOXEL_EPSILON {
527 continue;
528 }
529 if !offset_shape_fills_cell(shape, x[0], x[1], y[0], y[1], z[0], z[1]) {
530 return false;
531 }
532 }
533 }
534 }
535
536 true
537}
538
539#[must_use]
550pub fn join_is_not_empty(first: VoxelShape, second: VoxelShape, op: BooleanOp) -> bool {
551 assert!(
552 !op.apply(false, false),
553 "join_is_not_empty cannot use an operation that includes empty outside space"
554 );
555
556 let first_empty = first.is_empty();
557 let second_empty = second.is_empty();
558 if first_empty || second_empty {
559 return op.apply(!first_empty, !second_empty);
560 }
561
562 if first == second {
563 return op.apply(true, true);
564 }
565
566 let first_only_matters = op.apply(true, false);
567 let second_only_matters = op.apply(false, true);
568 for axis in [Axis::X, Axis::Y, Axis::Z] {
569 if first.max(axis) < second.min(axis) - VOXEL_EPSILON {
570 return first_only_matters || second_only_matters;
571 }
572 if second.max(axis) < first.min(axis) - VOXEL_EPSILON {
573 return first_only_matters || second_only_matters;
574 }
575 }
576
577 let mut x_edges = shape_edges(first, second, Axis::X);
578 let mut y_edges = shape_edges(first, second, Axis::Y);
579 let mut z_edges = shape_edges(first, second, Axis::Z);
580 sort_and_dedup_voxel_edges(&mut x_edges);
581 sort_and_dedup_voxel_edges(&mut y_edges);
582 sort_and_dedup_voxel_edges(&mut z_edges);
583
584 for x in x_edges.windows(2) {
585 if x[1] - x[0] <= VOXEL_EPSILON {
586 continue;
587 }
588 for y in y_edges.windows(2) {
589 if y[1] - y[0] <= VOXEL_EPSILON {
590 continue;
591 }
592 for z in z_edges.windows(2) {
593 if z[1] - z[0] <= VOXEL_EPSILON {
594 continue;
595 }
596 let first_full = shape_fills_cell(first, x[0], x[1], y[0], y[1], z[0], z[1]);
597 let second_full = shape_fills_cell(second, x[0], x[1], y[0], y[1], z[0], z[1]);
598 if op.apply(first_full, second_full) {
599 return true;
600 }
601 }
602 }
603 }
604
605 false
606}
607
608#[must_use]
617pub fn join_unoptimized_boxes(
618 first: VoxelShape,
619 second: VoxelShape,
620 op: BooleanOp,
621) -> Vec<BlockLocalAabb> {
622 assert!(
623 !op.apply(false, false),
624 "join_unoptimized_boxes cannot use an operation that includes empty outside space"
625 );
626
627 if first.is_empty() && second.is_empty() {
628 return Vec::new();
629 }
630
631 let mut x_edges = shape_edges(first, second, Axis::X);
632 let mut y_edges = shape_edges(first, second, Axis::Y);
633 let mut z_edges = shape_edges(first, second, Axis::Z);
634 sort_and_dedup_voxel_edges(&mut x_edges);
635 sort_and_dedup_voxel_edges(&mut y_edges);
636 sort_and_dedup_voxel_edges(&mut z_edges);
637
638 let mut boxes = Vec::new();
639 for x in x_edges.windows(2) {
640 if x[1] - x[0] <= VOXEL_EPSILON {
641 continue;
642 }
643 for y in y_edges.windows(2) {
644 if y[1] - y[0] <= VOXEL_EPSILON {
645 continue;
646 }
647 for z in z_edges.windows(2) {
648 if z[1] - z[0] <= VOXEL_EPSILON {
649 continue;
650 }
651
652 let first_full = shape_fills_cell(first, x[0], x[1], y[0], y[1], z[0], z[1]);
653 let second_full = shape_fills_cell(second, x[0], x[1], y[0], y[1], z[0], z[1]);
654 if op.apply(first_full, second_full) {
655 boxes.push(BlockLocalAabb::new(x[0], y[0], z[0], x[1], y[1], z[1]));
656 }
657 }
658 }
659 }
660
661 boxes
662}
663
664fn shape_edges(first: VoxelShape, second: VoxelShape, axis: Axis) -> Vec<f64> {
665 let mut edges = Vec::with_capacity((first.len() + second.len()) * 2);
666 for shape in [first, second] {
667 for aabb in shape {
668 if aabb.is_empty() {
669 continue;
670 }
671 edges.push(aabb.min(axis));
672 edges.push(aabb.max(axis));
673 }
674 }
675 edges
676}
677
678fn sort_and_dedup_voxel_edges(edges: &mut Vec<f64>) {
679 edges.sort_by(f64::total_cmp);
680 edges.dedup_by(|a, b| (*a - *b).abs() <= VOXEL_EPSILON);
681}
682
683fn shape_fills_cell(
684 shape: VoxelShape,
685 min_x: f64,
686 max_x: f64,
687 min_y: f64,
688 max_y: f64,
689 min_z: f64,
690 max_z: f64,
691) -> bool {
692 shape.into_iter().any(|aabb| {
693 !aabb.is_empty()
694 && aabb.min_x() <= min_x + VOXEL_EPSILON
695 && aabb.max_x() >= max_x - VOXEL_EPSILON
696 && aabb.min_y() <= min_y + VOXEL_EPSILON
697 && aabb.max_y() >= max_y - VOXEL_EPSILON
698 && aabb.min_z() <= min_z + VOXEL_EPSILON
699 && aabb.max_z() >= max_z - VOXEL_EPSILON
700 })
701}
702
703fn offset_shape_fills_cell(
704 shape: OffsetVoxelShape,
705 min_x: f64,
706 max_x: f64,
707 min_y: f64,
708 max_y: f64,
709 min_z: f64,
710 max_z: f64,
711) -> bool {
712 shape.iter().any(|aabb| {
713 !aabb.is_empty()
714 && aabb.min_x() <= min_x + VOXEL_EPSILON
715 && aabb.max_x() >= max_x - VOXEL_EPSILON
716 && aabb.min_y() <= min_y + VOXEL_EPSILON
717 && aabb.max_y() >= max_y - VOXEL_EPSILON
718 && aabb.min_z() <= min_z + VOXEL_EPSILON
719 && aabb.max_z() >= max_z - VOXEL_EPSILON
720 })
721}
722
723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub enum SupportType {
729 Full,
732 Center,
735 Rigid,
738}
739
740const CENTER_SUPPORT_MIN: f64 = 7.0 / 16.0;
742const CENTER_SUPPORT_MAX: f64 = 9.0 / 16.0;
743const CENTER_SUPPORT_Y_MAX: f64 = 10.0 / 16.0;
744
745const RIGID_BORDER: f64 = 0.125; #[must_use]
753pub fn is_face_full(shape: VoxelShape, direction: Direction) -> bool {
754 face_rectangles_cover(shape, direction, 0.0, 1.0, 0.0, 1.0)
755}
756
757#[must_use]
758pub fn is_offset_face_full(shape: OffsetVoxelShape, direction: Direction) -> bool {
759 offset_face_rectangles_cover(shape, direction, 0.0, 1.0, 0.0, 1.0)
760}
761
762#[must_use]
766pub fn is_face_center_supported(shape: VoxelShape, direction: Direction) -> bool {
767 if shape.is_empty() {
768 return false;
769 }
770
771 match direction {
772 Direction::Down | Direction::Up => face_rectangles_cover(
773 shape,
774 direction,
775 CENTER_SUPPORT_MIN,
776 CENTER_SUPPORT_MAX,
777 CENTER_SUPPORT_MIN,
778 CENTER_SUPPORT_MAX,
779 ),
780 Direction::North | Direction::South => face_rectangles_cover(
781 shape,
782 direction,
783 CENTER_SUPPORT_MIN,
784 CENTER_SUPPORT_MAX,
785 0.0,
786 CENTER_SUPPORT_Y_MAX,
787 ),
788 Direction::West | Direction::East => face_rectangles_cover(
789 shape,
790 direction,
791 0.0,
792 CENTER_SUPPORT_Y_MAX,
793 CENTER_SUPPORT_MIN,
794 CENTER_SUPPORT_MAX,
795 ),
796 }
797}
798
799#[must_use]
800pub fn is_offset_face_center_supported(shape: OffsetVoxelShape, direction: Direction) -> bool {
801 if shape.is_empty() {
802 return false;
803 }
804
805 match direction {
806 Direction::Down | Direction::Up => offset_face_rectangles_cover(
807 shape,
808 direction,
809 CENTER_SUPPORT_MIN,
810 CENTER_SUPPORT_MAX,
811 CENTER_SUPPORT_MIN,
812 CENTER_SUPPORT_MAX,
813 ),
814 Direction::North | Direction::South => offset_face_rectangles_cover(
815 shape,
816 direction,
817 CENTER_SUPPORT_MIN,
818 CENTER_SUPPORT_MAX,
819 0.0,
820 CENTER_SUPPORT_Y_MAX,
821 ),
822 Direction::West | Direction::East => offset_face_rectangles_cover(
823 shape,
824 direction,
825 0.0,
826 CENTER_SUPPORT_Y_MAX,
827 CENTER_SUPPORT_MIN,
828 CENTER_SUPPORT_MAX,
829 ),
830 }
831}
832
833#[must_use]
837pub fn is_face_rigid_supported(shape: VoxelShape, direction: Direction) -> bool {
838 if shape.is_empty() {
839 return false;
840 }
841
842 match direction {
843 Direction::Down | Direction::Up => {
844 face_rectangles_cover(shape, direction, 0.0, RIGID_BORDER, 0.0, 1.0)
845 && face_rectangles_cover(shape, direction, 1.0 - RIGID_BORDER, 1.0, 0.0, 1.0)
846 && face_rectangles_cover(
847 shape,
848 direction,
849 RIGID_BORDER,
850 1.0 - RIGID_BORDER,
851 0.0,
852 RIGID_BORDER,
853 )
854 && face_rectangles_cover(
855 shape,
856 direction,
857 RIGID_BORDER,
858 1.0 - RIGID_BORDER,
859 1.0 - RIGID_BORDER,
860 1.0,
861 )
862 }
863 Direction::North | Direction::South | Direction::West | Direction::East => {
864 is_face_full(shape, direction)
865 }
866 }
867}
868
869#[must_use]
870pub fn is_offset_face_rigid_supported(shape: OffsetVoxelShape, direction: Direction) -> bool {
871 if shape.is_empty() {
872 return false;
873 }
874
875 match direction {
876 Direction::Down | Direction::Up => {
877 offset_face_rectangles_cover(shape, direction, 0.0, RIGID_BORDER, 0.0, 1.0)
878 && offset_face_rectangles_cover(shape, direction, 1.0 - RIGID_BORDER, 1.0, 0.0, 1.0)
879 && offset_face_rectangles_cover(
880 shape,
881 direction,
882 RIGID_BORDER,
883 1.0 - RIGID_BORDER,
884 0.0,
885 RIGID_BORDER,
886 )
887 && offset_face_rectangles_cover(
888 shape,
889 direction,
890 RIGID_BORDER,
891 1.0 - RIGID_BORDER,
892 1.0 - RIGID_BORDER,
893 1.0,
894 )
895 }
896 Direction::North | Direction::South | Direction::West | Direction::East => {
897 is_offset_face_full(shape, direction)
898 }
899 }
900}
901
902#[must_use]
904pub fn is_face_sturdy(shape: VoxelShape, direction: Direction, support_type: SupportType) -> bool {
905 match support_type {
906 SupportType::Full => is_face_full(shape, direction),
907 SupportType::Center => is_face_center_supported(shape, direction),
908 SupportType::Rigid => is_face_rigid_supported(shape, direction),
909 }
910}
911
912#[must_use]
913pub fn is_offset_face_sturdy(
914 shape: OffsetVoxelShape,
915 direction: Direction,
916 support_type: SupportType,
917) -> bool {
918 match support_type {
919 SupportType::Full => is_offset_face_full(shape, direction),
920 SupportType::Center => is_offset_face_center_supported(shape, direction),
921 SupportType::Rigid => is_offset_face_rigid_supported(shape, direction),
922 }
923}
924
925#[must_use]
931pub fn is_block_local_face_sturdy(
932 boxes: &[BlockLocalAabb],
933 direction: Direction,
934 support_type: SupportType,
935) -> bool {
936 match support_type {
937 SupportType::Full => {
938 block_local_face_rectangles_cover(boxes, direction, 0.0, 1.0, 0.0, 1.0)
939 }
940 SupportType::Center => match direction {
941 Direction::Down | Direction::Up => block_local_face_rectangles_cover(
942 boxes,
943 direction,
944 CENTER_SUPPORT_MIN,
945 CENTER_SUPPORT_MAX,
946 CENTER_SUPPORT_MIN,
947 CENTER_SUPPORT_MAX,
948 ),
949 Direction::North | Direction::South => block_local_face_rectangles_cover(
950 boxes,
951 direction,
952 CENTER_SUPPORT_MIN,
953 CENTER_SUPPORT_MAX,
954 0.0,
955 CENTER_SUPPORT_Y_MAX,
956 ),
957 Direction::West | Direction::East => block_local_face_rectangles_cover(
958 boxes,
959 direction,
960 0.0,
961 CENTER_SUPPORT_Y_MAX,
962 CENTER_SUPPORT_MIN,
963 CENTER_SUPPORT_MAX,
964 ),
965 },
966 SupportType::Rigid => match direction {
967 Direction::Down | Direction::Up => {
968 block_local_face_rectangles_cover(boxes, direction, 0.0, RIGID_BORDER, 0.0, 1.0)
969 && block_local_face_rectangles_cover(
970 boxes,
971 direction,
972 1.0 - RIGID_BORDER,
973 1.0,
974 0.0,
975 1.0,
976 )
977 && block_local_face_rectangles_cover(
978 boxes,
979 direction,
980 RIGID_BORDER,
981 1.0 - RIGID_BORDER,
982 0.0,
983 RIGID_BORDER,
984 )
985 && block_local_face_rectangles_cover(
986 boxes,
987 direction,
988 RIGID_BORDER,
989 1.0 - RIGID_BORDER,
990 1.0 - RIGID_BORDER,
991 1.0,
992 )
993 }
994 Direction::North | Direction::South | Direction::West | Direction::East => {
995 block_local_face_rectangles_cover(boxes, direction, 0.0, 1.0, 0.0, 1.0)
996 }
997 },
998 }
999}
1000
1001#[derive(Clone, Copy)]
1002struct FaceRect {
1003 min_a: f64,
1004 max_a: f64,
1005 min_b: f64,
1006 max_b: f64,
1007}
1008
1009const FACE_EPSILON: f64 = 1.0e-6;
1010
1011type FaceRectList = SmallVec<[FaceRect; 8]>;
1012type EdgeList = SmallVec<[f64; 18]>;
1013
1014#[must_use]
1015pub fn face_rectangles_cover(
1016 shape: VoxelShape,
1017 direction: Direction,
1018 target_min_a: f64,
1019 target_max_a: f64,
1020 target_min_b: f64,
1021 target_max_b: f64,
1022) -> bool {
1023 block_local_face_rectangles_cover(
1024 shape.boxes(),
1025 direction,
1026 target_min_a,
1027 target_max_a,
1028 target_min_b,
1029 target_max_b,
1030 )
1031}
1032
1033fn block_local_face_rectangles_cover(
1034 boxes: &[BlockLocalAabb],
1035 direction: Direction,
1036 target_min_a: f64,
1037 target_max_a: f64,
1038 target_min_b: f64,
1039 target_max_b: f64,
1040) -> bool {
1041 let mut rects = FaceRectList::new();
1042 for &aabb in boxes {
1043 let Some(rect) = face_rect_for_aabb(aabb, direction) else {
1044 continue;
1045 };
1046 if rect.max_a <= target_min_a
1047 || rect.min_a >= target_max_a
1048 || rect.max_b <= target_min_b
1049 || rect.min_b >= target_max_b
1050 {
1051 continue;
1052 }
1053 rects.push(FaceRect {
1054 min_a: rect.min_a.max(target_min_a),
1055 max_a: rect.max_a.min(target_max_a),
1056 min_b: rect.min_b.max(target_min_b),
1057 max_b: rect.max_b.min(target_max_b),
1058 });
1059 }
1060
1061 face_rects_cover_target(
1062 rects,
1063 target_min_a,
1064 target_max_a,
1065 target_min_b,
1066 target_max_b,
1067 )
1068}
1069
1070#[must_use]
1071pub fn offset_face_rectangles_cover(
1072 shape: OffsetVoxelShape,
1073 direction: Direction,
1074 target_min_a: f64,
1075 target_max_a: f64,
1076 target_min_b: f64,
1077 target_max_b: f64,
1078) -> bool {
1079 let mut rects = FaceRectList::new();
1080 for aabb in shape.iter() {
1081 let Some(rect) = face_rect_for_aabb(aabb, direction) else {
1082 continue;
1083 };
1084 if rect.max_a <= target_min_a
1085 || rect.min_a >= target_max_a
1086 || rect.max_b <= target_min_b
1087 || rect.min_b >= target_max_b
1088 {
1089 continue;
1090 }
1091 rects.push(FaceRect {
1092 min_a: rect.min_a.max(target_min_a),
1093 max_a: rect.max_a.min(target_max_a),
1094 min_b: rect.min_b.max(target_min_b),
1095 max_b: rect.max_b.min(target_max_b),
1096 });
1097 }
1098
1099 face_rects_cover_target(
1100 rects,
1101 target_min_a,
1102 target_max_a,
1103 target_min_b,
1104 target_max_b,
1105 )
1106}
1107
1108fn face_rects_cover_target(
1109 rects: FaceRectList,
1110 target_min_a: f64,
1111 target_max_a: f64,
1112 target_min_b: f64,
1113 target_max_b: f64,
1114) -> bool {
1115 if rects.is_empty() {
1116 return false;
1117 }
1118
1119 let mut a_edges: EdgeList = smallvec![target_min_a, target_max_a];
1120 let mut b_edges: EdgeList = smallvec![target_min_b, target_max_b];
1121 for rect in &rects {
1122 a_edges.push(rect.min_a);
1123 a_edges.push(rect.max_a);
1124 b_edges.push(rect.min_b);
1125 b_edges.push(rect.max_b);
1126 }
1127 sort_and_dedup_edges(&mut a_edges);
1128 sort_and_dedup_edges(&mut b_edges);
1129
1130 for a_pair in a_edges.windows(2) {
1131 if a_pair[1] - a_pair[0] <= FACE_EPSILON {
1132 continue;
1133 }
1134 for b_pair in b_edges.windows(2) {
1135 if b_pair[1] - b_pair[0] <= FACE_EPSILON {
1136 continue;
1137 }
1138 let covered = rects.iter().any(|rect| {
1139 rect.min_a <= a_pair[0] + FACE_EPSILON
1140 && rect.max_a >= a_pair[1] - FACE_EPSILON
1141 && rect.min_b <= b_pair[0] + FACE_EPSILON
1142 && rect.max_b >= b_pair[1] - FACE_EPSILON
1143 });
1144 if !covered {
1145 return false;
1146 }
1147 }
1148 }
1149
1150 true
1151}
1152
1153fn face_rect_for_aabb(aabb: BlockLocalAabb, direction: Direction) -> Option<FaceRect> {
1154 let rect = match direction {
1155 Direction::Down if aabb.min_y() <= FACE_EPSILON && aabb.max_y() >= -FACE_EPSILON => {
1156 FaceRect {
1157 min_a: aabb.min_x(),
1158 max_a: aabb.max_x(),
1159 min_b: aabb.min_z(),
1160 max_b: aabb.max_z(),
1161 }
1162 }
1163 Direction::Up
1164 if aabb.max_y() >= 1.0 - FACE_EPSILON && aabb.min_y() <= 1.0 + FACE_EPSILON =>
1165 {
1166 FaceRect {
1167 min_a: aabb.min_x(),
1168 max_a: aabb.max_x(),
1169 min_b: aabb.min_z(),
1170 max_b: aabb.max_z(),
1171 }
1172 }
1173 Direction::North if aabb.min_z() <= FACE_EPSILON && aabb.max_z() >= -FACE_EPSILON => {
1174 FaceRect {
1175 min_a: aabb.min_x(),
1176 max_a: aabb.max_x(),
1177 min_b: aabb.min_y(),
1178 max_b: aabb.max_y(),
1179 }
1180 }
1181 Direction::South
1182 if aabb.max_z() >= 1.0 - FACE_EPSILON && aabb.min_z() <= 1.0 + FACE_EPSILON =>
1183 {
1184 FaceRect {
1185 min_a: aabb.min_x(),
1186 max_a: aabb.max_x(),
1187 min_b: aabb.min_y(),
1188 max_b: aabb.max_y(),
1189 }
1190 }
1191 Direction::West if aabb.min_x() <= FACE_EPSILON && aabb.max_x() >= -FACE_EPSILON => {
1192 FaceRect {
1193 min_a: aabb.min_y(),
1194 max_a: aabb.max_y(),
1195 min_b: aabb.min_z(),
1196 max_b: aabb.max_z(),
1197 }
1198 }
1199 Direction::East
1200 if aabb.max_x() >= 1.0 - FACE_EPSILON && aabb.min_x() <= 1.0 + FACE_EPSILON =>
1201 {
1202 FaceRect {
1203 min_a: aabb.min_y(),
1204 max_a: aabb.max_y(),
1205 min_b: aabb.min_z(),
1206 max_b: aabb.max_z(),
1207 }
1208 }
1209 _ => return None,
1210 };
1211
1212 if rect.min_a >= rect.max_a || rect.min_b >= rect.max_b {
1213 return None;
1214 }
1215 Some(rect)
1216}
1217
1218fn sort_and_dedup_edges(edges: &mut EdgeList) {
1219 edges.sort_by(f64::total_cmp);
1220 edges.dedup_by(|a, b| (*a - *b).abs() <= FACE_EPSILON);
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226
1227 const QUADRANT_TOP_FACE: &[BlockLocalAabb] = &[
1228 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.5, 1.0, 0.5),
1229 BlockLocalAabb::new(0.5, 0.5, 0.0, 1.0, 1.0, 0.5),
1230 BlockLocalAabb::new(0.0, 0.5, 0.5, 0.5, 1.0, 1.0),
1231 BlockLocalAabb::new(0.5, 0.5, 0.5, 1.0, 1.0, 1.0),
1232 ];
1233
1234 const GAPPED_TOP_FACE: &[BlockLocalAabb] = &[
1235 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.45, 1.0, 1.0),
1236 BlockLocalAabb::new(0.55, 0.5, 0.0, 1.0, 1.0, 1.0),
1237 ];
1238
1239 const VANILLA_AZALEA_SHAPE: &[BlockLocalAabb] = &[
1240 BlockLocalAabb::new(0.375, 0.0, 0.375, 0.625, 1.0, 0.625),
1241 BlockLocalAabb::new(0.0, 0.5, 0.0, 0.375, 1.0, 1.0),
1242 BlockLocalAabb::new(0.375, 0.5, 0.0, 1.0, 1.0, 0.375),
1243 BlockLocalAabb::new(0.375, 0.5, 0.625, 1.0, 1.0, 1.0),
1244 BlockLocalAabb::new(0.625, 0.5, 0.375, 1.0, 1.0, 0.625),
1245 ];
1246
1247 const SPLIT_FULL_BLOCK: &[BlockLocalAabb] = &[
1248 BlockLocalAabb::new(0.0, 0.0, 0.0, 0.5, 1.0, 1.0),
1249 BlockLocalAabb::new(0.5, 0.0, 0.0, 1.0, 1.0, 1.0),
1250 ];
1251
1252 const Z_GAPPED_BLOCK_WITH_OFFSET: &[BlockLocalAabb] = &[
1253 BlockLocalAabb::new(0.0, 0.0, -0.1, 1.0, 1.0, 0.15),
1254 BlockLocalAabb::new(0.0, 0.0, 0.65, 1.0, 1.0, 0.9),
1255 ];
1256
1257 const LOWER_HALF_BLOCK: &[BlockLocalAabb] =
1258 &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.5, 1.0)];
1259
1260 const UPPER_HALF_BLOCK: &[BlockLocalAabb] =
1261 &[BlockLocalAabb::new(0.0, 0.5, 0.0, 1.0, 1.0, 1.0)];
1262
1263 const OVERLAPPING_HALF_BLOCKS: &[BlockLocalAabb] = &[
1264 BlockLocalAabb::new(0.0, 0.0, 0.0, 0.75, 1.0, 1.0),
1265 BlockLocalAabb::new(0.25, 0.0, 0.0, 1.0, 1.0, 1.0),
1266 ];
1267
1268 const ZERO_VOLUME_BOX: &[BlockLocalAabb] = &[BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, 0.0, 1.0)];
1269
1270 const LARGE_COLLISION_SHAPE: &[BlockLocalAabb] =
1271 &[BlockLocalAabb::new(-0.25, 0.0, 0.0, 1.0, 1.0, 1.0)];
1272
1273 const RIGID_TOP_RING: &[BlockLocalAabb] = &[
1274 BlockLocalAabb::new(0.0, 0.0, 0.0, RIGID_BORDER, 1.0, 1.0),
1275 BlockLocalAabb::new(1.0 - RIGID_BORDER, 0.0, 0.0, 1.0, 1.0, 1.0),
1276 BlockLocalAabb::new(
1277 RIGID_BORDER,
1278 0.0,
1279 0.0,
1280 1.0 - RIGID_BORDER,
1281 1.0,
1282 RIGID_BORDER,
1283 ),
1284 BlockLocalAabb::new(
1285 RIGID_BORDER,
1286 0.0,
1287 1.0 - RIGID_BORDER,
1288 1.0 - RIGID_BORDER,
1289 1.0,
1290 1.0,
1291 ),
1292 ];
1293
1294 const RIGID_CENTER_PANEL: &[BlockLocalAabb] = &[BlockLocalAabb::new(
1295 RIGID_BORDER,
1296 0.0,
1297 RIGID_BORDER,
1298 1.0 - RIGID_BORDER,
1299 1.0,
1300 1.0 - RIGID_BORDER,
1301 )];
1302
1303 const RIGID_WEST_FACE_RING: &[BlockLocalAabb] = &[
1304 BlockLocalAabb::new(0.0, 0.0, 0.0, 1.0, RIGID_BORDER, 1.0),
1305 BlockLocalAabb::new(0.0, 1.0 - RIGID_BORDER, 0.0, 1.0, 1.0, 1.0),
1306 BlockLocalAabb::new(
1307 0.0,
1308 RIGID_BORDER,
1309 0.0,
1310 1.0,
1311 1.0 - RIGID_BORDER,
1312 RIGID_BORDER,
1313 ),
1314 BlockLocalAabb::new(
1315 0.0,
1316 RIGID_BORDER,
1317 1.0 - RIGID_BORDER,
1318 1.0,
1319 1.0 - RIGID_BORDER,
1320 1.0,
1321 ),
1322 ];
1323
1324 #[test]
1325 fn boolean_op_matches_vanilla_truth_table() {
1326 assert!(BooleanOp::OnlyFirst.apply(true, false));
1327 assert!(!BooleanOp::OnlyFirst.apply(false, true));
1328 assert!(BooleanOp::NotSame.apply(true, false));
1329 assert!(!BooleanOp::NotSame.apply(true, true));
1330 assert!(BooleanOp::Or.apply(false, true));
1331 assert!(!BooleanOp::And.apply(true, false));
1332 }
1333
1334 #[test]
1335 fn join_is_not_empty_detects_intersection() {
1336 assert!(join_is_not_empty(
1337 VoxelShape::from_boxes(OVERLAPPING_HALF_BLOCKS),
1338 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
1339 BooleanOp::And
1340 ));
1341 }
1342
1343 #[test]
1344 fn join_is_not_empty_rejects_disjoint_and() {
1345 assert!(!join_is_not_empty(
1346 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
1347 VoxelShape::from_boxes(UPPER_HALF_BLOCK),
1348 BooleanOp::And
1349 ));
1350 }
1351
1352 #[test]
1353 fn join_is_not_empty_detects_only_first_remainder() {
1354 assert!(join_is_not_empty(
1355 VoxelShape::FULL_BLOCK,
1356 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
1357 BooleanOp::OnlyFirst
1358 ));
1359 }
1360
1361 #[test]
1362 fn join_unoptimized_boxes_materializes_only_second_remainder() {
1363 let remainder = join_unoptimized_boxes(
1364 VoxelShape::from_boxes(LOWER_HALF_BLOCK),
1365 VoxelShape::FULL_BLOCK,
1366 BooleanOp::OnlySecond,
1367 );
1368
1369 assert_eq!(
1370 remainder,
1371 vec![BlockLocalAabb::new(0.0, 0.5, 0.0, 1.0, 1.0, 1.0)]
1372 );
1373 }
1374
1375 #[test]
1376 fn shape_full_block_accepts_tiled_boxes() {
1377 assert!(is_shape_full_block(VoxelShape::from_boxes(
1378 SPLIT_FULL_BLOCK
1379 )));
1380 }
1381
1382 #[test]
1383 fn shape_full_block_rejects_partial_boxes() {
1384 assert!(!is_shape_full_block(VoxelShape::from_boxes(
1385 LOWER_HALF_BLOCK
1386 )));
1387 }
1388
1389 #[test]
1390 fn offset_shape_full_block_rejects_shifted_full_block() {
1391 assert!(is_offset_shape_full_block(
1392 OffsetVoxelShape::without_offset(VoxelShape::FULL_BLOCK)
1393 ));
1394 assert!(!is_offset_shape_full_block(OffsetVoxelShape::new(
1395 VoxelShape::FULL_BLOCK,
1396 DVec3::new(0.25, 0.0, 0.0)
1397 )));
1398 }
1399
1400 #[test]
1401 fn offset_shape_full_block_rejects_z_gap_after_offset() {
1402 assert!(!is_offset_shape_full_block(OffsetVoxelShape::new(
1403 VoxelShape::from_boxes(Z_GAPPED_BLOCK_WITH_OFFSET),
1404 DVec3::new(0.0, 0.0, 0.1)
1405 )));
1406 }
1407
1408 #[test]
1409 fn zero_volume_boxes_are_empty() {
1410 assert!(VoxelShape::from_boxes(ZERO_VOLUME_BOX).is_empty());
1411 assert!(!join_is_not_empty(
1412 VoxelShape::from_boxes(ZERO_VOLUME_BOX),
1413 VoxelShape::FULL_BLOCK,
1414 BooleanOp::And
1415 ));
1416 }
1417
1418 #[test]
1419 fn large_collision_shape_matches_vanilla_bounds_rule() {
1420 assert!(!VoxelShape::EMPTY.has_large_collision_shape());
1421 assert!(!VoxelShape::FULL_BLOCK.has_large_collision_shape());
1422 assert!(VoxelShape::from_boxes(LARGE_COLLISION_SHAPE).has_large_collision_shape());
1423 }
1424
1425 #[test]
1426 fn face_full_accepts_union_covering_face() {
1427 assert!(is_face_full(
1428 VoxelShape::from_boxes(QUADRANT_TOP_FACE),
1429 Direction::Up
1430 ));
1431 }
1432
1433 #[test]
1434 fn dynamic_face_support_uses_union_at_the_block_boundary() {
1435 assert!(is_block_local_face_sturdy(
1436 QUADRANT_TOP_FACE,
1437 Direction::Up,
1438 SupportType::Full,
1439 ));
1440
1441 let beyond_east_face = [BlockLocalAabb::new(1.25, 0.0, 0.0, 2.25, 1.0, 1.0)];
1442 assert!(!is_block_local_face_sturdy(
1443 &beyond_east_face,
1444 Direction::East,
1445 SupportType::Full,
1446 ));
1447 }
1448
1449 #[test]
1450 fn face_full_rejects_union_with_gap() {
1451 assert!(!is_face_full(
1452 VoxelShape::from_boxes(GAPPED_TOP_FACE),
1453 Direction::Up
1454 ));
1455 }
1456
1457 #[test]
1458 fn offset_face_full_rejects_shifted_top_face() {
1459 assert!(!is_offset_face_full(
1460 OffsetVoxelShape::new(VoxelShape::FULL_BLOCK, DVec3::new(0.25, 0.0, 0.0)),
1461 Direction::Up
1462 ));
1463 }
1464
1465 #[test]
1466 fn face_full_accepts_vanilla_azalea_top_shape() {
1467 assert!(is_face_full(
1468 VoxelShape::from_boxes(VANILLA_AZALEA_SHAPE),
1469 Direction::Up
1470 ));
1471 }
1472
1473 #[test]
1474 fn rigid_support_accepts_border_ring_covered_by_multiple_boxes() {
1475 assert!(is_face_rigid_supported(
1476 VoxelShape::from_boxes(RIGID_TOP_RING),
1477 Direction::Up
1478 ));
1479 }
1480
1481 #[test]
1482 fn rigid_support_rejects_center_panel_without_border_ring() {
1483 assert!(!is_face_rigid_supported(
1484 VoxelShape::from_boxes(RIGID_CENTER_PANEL),
1485 Direction::Up
1486 ));
1487 }
1488
1489 #[test]
1490 fn rigid_support_rejects_side_border_ring_without_full_face() {
1491 assert!(!is_face_rigid_supported(
1492 VoxelShape::from_boxes(RIGID_WEST_FACE_RING),
1493 Direction::West
1494 ));
1495 }
1496}