Skip to main content

steel_registry/blocks/
shapes.rs

1use glam::DVec3;
2use smallvec::{SmallVec, smallvec};
3use steel_utils::{BlockLocalAabb, axis::Axis};
4
5/// Vanilla shape boolean operation.
6///
7/// Mirrors `net.minecraft.world.phys.shapes.BooleanOp`. Operations where
8/// `apply(false, false)` is true are not valid for `join_is_not_empty`, matching
9/// vanilla's guard for unbounded outside-space results.
10#[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/// A block-local voxel shape.
55///
56/// This currently stores the optimized AABB list extracted from vanilla data.
57/// It is intentionally a domain type rather than a raw slice so the full
58/// vanilla shape implementation can grow behind the same API.
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct VoxelShape {
61    boxes: &'static [BlockLocalAabb],
62}
63
64impl VoxelShape {
65    /// Empty shape.
66    pub const EMPTY: Self = Self::from_boxes(&[]);
67
68    /// Full block shape.
69    pub const FULL_BLOCK: Self = Self::from_boxes(FULL_BLOCK_BOXES);
70
71    /// Creates a shape from static block-local boxes.
72    #[must_use]
73    pub const fn from_boxes(boxes: &'static [BlockLocalAabb]) -> Self {
74        Self { boxes }
75    }
76
77    /// Returns the block-local boxes backing this shape.
78    #[must_use]
79    pub const fn boxes(self) -> &'static [BlockLocalAabb] {
80        self.boxes
81    }
82
83    /// Returns an iterator over the block-local boxes.
84    pub fn iter(self) -> core::slice::Iter<'static, BlockLocalAabb> {
85        self.boxes.iter()
86    }
87
88    /// Returns the number of block-local boxes in this shape.
89    #[must_use]
90    pub const fn len(self) -> usize {
91        self.boxes.len()
92    }
93
94    /// Returns true if this shape has no non-empty boxes.
95    #[must_use]
96    pub fn is_empty(self) -> bool {
97        self.boxes.iter().all(steel_utils::geometry::Aabb::is_empty)
98    }
99
100    /// Returns the minimum coordinate on `axis`, or positive infinity for an empty shape.
101    #[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    /// Returns the maximum coordinate on `axis`, or negative infinity for an empty shape.
111    #[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    /// Returns the union bounds of this shape, or `None` for empty shapes.
121    #[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    /// Returns true when this shape extends outside its owning block.
149    ///
150    /// Mirrors vanilla `BlockState.hasLargeCollisionShape()` for collision
151    /// iterator filtering.
152    #[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/// A voxel shape plus the block-local offset from vanilla `BlockState.getOffset(pos)`.
170#[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/// An ID referencing a registered `VoxelShape` in the `ShapeRegistry`.
245///
246/// Use this to refer to shapes in a compact way. The actual shape data
247/// can be retrieved from the `ShapeRegistry` using this ID.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub struct ShapeId(pub u16);
250
251impl ShapeId {
252    /// The empty shape (no AABBs).
253    pub const EMPTY: ShapeId = ShapeId(0);
254
255    /// A full block shape.
256    pub const FULL_BLOCK: ShapeId = ShapeId(1);
257}
258
259/// Registry for `VoxelShapes`.
260///
261/// Shapes are registered once and referenced by `ShapeId`. This allows
262/// deduplication of shapes and compact storage of shape references.
263///
264/// Vanilla shapes are registered at startup. Plugins can register
265/// additional shapes for custom blocks.
266pub 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    /// Creates a new shape registry with the standard empty and full block shapes.
279    #[must_use]
280    pub fn new() -> Self {
281        let mut registry = Self {
282            shapes: Vec::new(),
283            allows_registering: true,
284        };
285
286        // Register the two standard shapes - IDs must match ShapeId::EMPTY and ShapeId::FULL_BLOCK
287        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    /// Registers a new shape and returns its ID.
297    ///
298    /// # Panics
299    /// Panics if the registry has been frozen.
300    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    /// Gets the shape for a given ID.
312    ///
313    /// Returns an empty shape if the ID is invalid.
314    #[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    /// Returns the number of registered shapes.
323    #[must_use]
324    pub const fn len(&self) -> usize {
325        self.shapes.len()
326    }
327
328    /// Returns true if no shapes are registered.
329    #[must_use]
330    pub const fn is_empty(&self) -> bool {
331        self.shapes.is_empty()
332    }
333
334    /// Freezes the registry, preventing further registrations.
335    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/// Shape data for a block state.
345#[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    /// Creates new block shapes.
357    #[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    /// Full block for every shape channel except interaction.
377    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    /// Empty shapes for all shape channels.
387    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/// Shape channel names used by vanilla block-state shape queries.
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum ShapeChannel {
400    Collision,
401    Support,
402    Outline,
403    Occlusion,
404    Interaction,
405    Visual,
406}
407
408/// Records which extracted shape channels already include `BlockState.getOffset(pos)`.
409#[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/// Returns the overall bounding box of a voxel shape (union of all AABBs).
457///
458/// The shape must be non-empty; panics otherwise.
459#[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/// Checks if a shape is a full block (covers the entire 0-1 cube).
468///
469/// This matches vanilla's `Block.isShapeFullBlock()` used by `isSolidRender()`.
470///
471#[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/// Returns true if applying `op` to two voxel shapes produces any filled space.
540///
541/// This is the box-backed equivalent of vanilla `Shapes.joinIsNotEmpty`. It
542/// decomposes both shapes into a shared coordinate grid and tests occupancy in
543/// each cell. The current representation does not materialize a joined shape;
544/// it answers the boolean query needed for full-block and occlusion checks.
545///
546/// # Panics
547/// Panics if `op.apply(false, false)` is true, matching vanilla's invalid
548/// operation guard for unbounded outside-space results.
549#[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/// Materializes the unoptimized cell boxes produced by a shape boolean operation.
609///
610/// Vanilla parity: `Shapes.joinUnoptimized(first, second, op)`, expressed as
611/// block-local boxes instead of a lazily merged voxel shape.
612///
613/// # Panics
614/// Panics if `op.apply(false, false)` is true, matching vanilla's invalid
615/// operation guard for unbounded outside-space results.
616#[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/// Support type for `is_face_sturdy` checks.
724///
725/// Determines what kind of support a block face provides for other blocks.
726/// Used by fences, walls, torches, etc. to decide if they can connect/attach.
727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
728pub enum SupportType {
729    /// Full face support - the entire face must be solid.
730    /// Used by most blocks that need a solid surface.
731    Full,
732    /// Center support - only the center of the face needs to be solid.
733    /// Used by things like hanging signs that only need a small attachment point.
734    Center,
735    /// Rigid support - most of the face must be solid, but allows small gaps.
736    /// Used by bells and similar blocks.
737    Rigid,
738}
739
740/// Vanilla `SupportType.CENTER`: `Block.column(2.0, 0.0, 10.0)`.
741const 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
745/// Vanilla `SupportType.RIGID`: `Shapes.block() ONLY_FIRST Block.column(12.0, 0.0, 16.0)`.
746const RIGID_BORDER: f64 = 0.125; // 2/16
747
748/// Checks if a shape fully covers a face (for `SupportType::Full`).
749///
750/// Returns true if the 2D projection of the shape on the given face
751/// completely covers the 1x1 face area.
752#[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/// Checks if a shape provides center support on a face.
763///
764/// The center area is a 12x12 pixel region (0.125 to 0.875 on each axis).
765#[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/// Checks if a shape provides rigid support on a face.
834///
835/// Rigid support requires coverage of vanilla's fixed 3D support mask.
836#[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/// Checks if a shape is sturdy on a face for the given support type.
903#[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/// Checks whether an owned union of block-local boxes is sturdy on a face.
926///
927/// Static registry shapes use [`VoxelShape`], while dynamic blocks resolve
928/// their boxes from live world data. Both representations use the same face
929/// coverage rules.
930#[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}