azalea_physics/collision/
shape.rs

1use std::{cmp, num::NonZeroU32, sync::LazyLock};
2
3use azalea_core::{
4    block_hit_result::BlockHitResult,
5    direction::{Axis, AxisCycle, Direction},
6    math::{binary_search, EPSILON},
7    position::{BlockPos, Vec3},
8};
9
10use super::mergers::IndexMerger;
11use crate::collision::{BitSetDiscreteVoxelShape, DiscreteVoxelShape, AABB};
12
13pub struct Shapes;
14
15pub static BLOCK_SHAPE: LazyLock<VoxelShape> = LazyLock::new(|| {
16    let mut shape = BitSetDiscreteVoxelShape::new(1, 1, 1);
17    shape.fill(0, 0, 0);
18    VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(shape)))
19});
20
21pub fn box_shape(
22    min_x: f64,
23    min_y: f64,
24    min_z: f64,
25    max_x: f64,
26    max_y: f64,
27    max_z: f64,
28) -> VoxelShape {
29    // we don't check for the numbers being out of bounds because some blocks are
30    // weird and are outside 0-1
31
32    if max_x - min_x < EPSILON && max_y - min_y < EPSILON && max_z - min_z < EPSILON {
33        return EMPTY_SHAPE.clone();
34    }
35
36    let x_bits = find_bits(min_x, max_x);
37    let y_bits = find_bits(min_y, max_y);
38    let z_bits = find_bits(min_z, max_z);
39
40    if x_bits < 0 || y_bits < 0 || z_bits < 0 {
41        return VoxelShape::Array(ArrayVoxelShape::new(
42            BLOCK_SHAPE.shape().to_owned(),
43            vec![min_x, max_x],
44            vec![min_y, max_y],
45            vec![min_z, max_z],
46        ));
47    }
48    if x_bits == 0 && y_bits == 0 && z_bits == 0 {
49        return BLOCK_SHAPE.clone();
50    }
51
52    let x_bits = 1 << x_bits;
53    let y_bits = 1 << y_bits;
54    let z_bits = 1 << z_bits;
55    let shape = BitSetDiscreteVoxelShape::with_filled_bounds(
56        x_bits,
57        y_bits,
58        z_bits,
59        (min_x * x_bits as f64).round() as i32,
60        (min_y * y_bits as f64).round() as i32,
61        (min_z * z_bits as f64).round() as i32,
62        (max_x * x_bits as f64).round() as i32,
63        (max_y * y_bits as f64).round() as i32,
64        (max_z * z_bits as f64).round() as i32,
65    );
66    VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(shape)))
67}
68
69pub static EMPTY_SHAPE: LazyLock<VoxelShape> = LazyLock::new(|| {
70    VoxelShape::Array(ArrayVoxelShape::new(
71        DiscreteVoxelShape::BitSet(BitSetDiscreteVoxelShape::new(0, 0, 0)),
72        vec![0.],
73        vec![0.],
74        vec![0.],
75    ))
76});
77
78fn find_bits(min: f64, max: f64) -> i32 {
79    if min < -EPSILON || max > 1. + EPSILON {
80        return -1;
81    }
82    for bits in 0..=3 {
83        let shifted_bits = 1 << bits;
84        let min = min * shifted_bits as f64;
85        let max = max * shifted_bits as f64;
86        let min_ok = (min - min.round()).abs() < EPSILON * shifted_bits as f64;
87        let max_ok = (max - max.round()).abs() < EPSILON * shifted_bits as f64;
88        if min_ok && max_ok {
89            return bits;
90        }
91    }
92    -1
93}
94
95impl Shapes {
96    pub fn or(a: VoxelShape, b: VoxelShape) -> VoxelShape {
97        Self::join(a, b, |a, b| a || b)
98    }
99
100    pub fn collide(
101        axis: &Axis,
102        entity_box: &AABB,
103        collision_boxes: &Vec<VoxelShape>,
104        mut movement: f64,
105    ) -> f64 {
106        for shape in collision_boxes {
107            if movement.abs() < EPSILON {
108                return 0.;
109            }
110            movement = shape.collide(axis, entity_box, movement);
111        }
112        movement
113    }
114
115    pub fn join(a: VoxelShape, b: VoxelShape, op: fn(bool, bool) -> bool) -> VoxelShape {
116        Self::join_unoptimized(a, b, op).optimize()
117    }
118
119    pub fn join_unoptimized(
120        a: VoxelShape,
121        b: VoxelShape,
122        op: fn(bool, bool) -> bool,
123    ) -> VoxelShape {
124        if op(false, false) {
125            panic!("Illegal operation");
126        };
127        // if (a == b) {
128        //     return if op(true, true) { a } else { empty_shape() };
129        // }
130        let op_true_false = op(true, false);
131        let op_false_true = op(false, true);
132        if a.is_empty() {
133            return if op_false_true {
134                b
135            } else {
136                EMPTY_SHAPE.clone()
137            };
138        }
139        if b.is_empty() {
140            return if op_true_false {
141                a
142            } else {
143                EMPTY_SHAPE.clone()
144            };
145        }
146        // IndexMerger var5 = createIndexMerger(1, a.getCoords(Direction.Axis.X),
147        // b.getCoords(Direction.Axis.X), var3, var4); IndexMerger var6 =
148        // createIndexMerger(var5.size() - 1, a.getCoords(Direction.Axis.Y),
149        // b.getCoords(Direction.Axis.Y), var3, var4); IndexMerger var7 =
150        // createIndexMerger((var5.size() - 1) * (var6.size() - 1),
151        // a.getCoords(Direction.Axis.Z), b.getCoords(Direction.Axis.Z), var3, var4);
152        // BitSetDiscreteVoxelShape var8 = BitSetDiscreteVoxelShape.join(a.shape,
153        // b.shape, var5, var6, var7, op); return (VoxelShape)(var5 instanceof
154        // DiscreteCubeMerger && var6 instanceof DiscreteCubeMerger && var7 instanceof
155        // DiscreteCubeMerger ? new CubeVoxelShape(var8) : new ArrayVoxelShape(var8,
156        // var5.getList(), var6.getList(), var7.getList()));
157        let var5 = Self::create_index_merger(
158            1,
159            a.get_coords(Axis::X),
160            b.get_coords(Axis::X),
161            op_true_false,
162            op_false_true,
163        );
164        let var6 = Self::create_index_merger(
165            (var5.size() - 1).try_into().unwrap(),
166            a.get_coords(Axis::Y),
167            b.get_coords(Axis::Y),
168            op_true_false,
169            op_false_true,
170        );
171        let var7 = Self::create_index_merger(
172            ((var5.size() - 1) * (var6.size() - 1)).try_into().unwrap(),
173            a.get_coords(Axis::Z),
174            b.get_coords(Axis::Z),
175            op_true_false,
176            op_false_true,
177        );
178        let var8 = BitSetDiscreteVoxelShape::join(a.shape(), b.shape(), &var5, &var6, &var7, op);
179        // if var5.is_discrete_cube_merger()
180
181        if matches!(var5, IndexMerger::DiscreteCube { .. })
182            && matches!(var6, IndexMerger::DiscreteCube { .. })
183            && matches!(var7, IndexMerger::DiscreteCube { .. })
184        {
185            VoxelShape::Cube(CubeVoxelShape::new(DiscreteVoxelShape::BitSet(var8)))
186        } else {
187            VoxelShape::Array(ArrayVoxelShape::new(
188                DiscreteVoxelShape::BitSet(var8),
189                var5.get_list(),
190                var6.get_list(),
191                var7.get_list(),
192            ))
193        }
194    }
195
196    /// Check if the op is true anywhere when joining the two shapes
197    /// vanilla calls this joinIsNotEmpty
198    pub fn matches_anywhere(
199        a: &VoxelShape,
200        b: &VoxelShape,
201        op: impl Fn(bool, bool) -> bool,
202    ) -> bool {
203        debug_assert!(!op(false, false));
204        let a_is_empty = a.is_empty();
205        let b_is_empty = b.is_empty();
206        if a_is_empty || b_is_empty {
207            return op(!a_is_empty, !b_is_empty);
208        }
209        if a == b {
210            return op(true, true);
211        }
212
213        let op_true_false = op(true, false);
214        let op_false_true = op(false, true);
215
216        for axis in [Axis::X, Axis::Y, Axis::Z] {
217            if a.max(axis) < b.min(axis) - EPSILON {
218                return op_true_false || op_false_true;
219            }
220            if b.max(axis) < a.min(axis) - EPSILON {
221                return op_true_false || op_false_true;
222            }
223        }
224
225        let x_merger = Self::create_index_merger(
226            1,
227            a.get_coords(Axis::X),
228            b.get_coords(Axis::X),
229            op_true_false,
230            op_false_true,
231        );
232        let y_merger = Self::create_index_merger(
233            (x_merger.size() - 1) as i32,
234            a.get_coords(Axis::Y),
235            b.get_coords(Axis::Y),
236            op_true_false,
237            op_false_true,
238        );
239        let z_merger = Self::create_index_merger(
240            ((x_merger.size() - 1) * (y_merger.size() - 1)) as i32,
241            a.get_coords(Axis::Z),
242            b.get_coords(Axis::Z),
243            op_true_false,
244            op_false_true,
245        );
246
247        Self::matches_anywhere_with_mergers(
248            x_merger,
249            y_merger,
250            z_merger,
251            a.shape().to_owned(),
252            b.shape().to_owned(),
253            op,
254        )
255    }
256
257    pub fn matches_anywhere_with_mergers(
258        merged_x: IndexMerger,
259        merged_y: IndexMerger,
260        merged_z: IndexMerger,
261        shape1: DiscreteVoxelShape,
262        shape2: DiscreteVoxelShape,
263        op: impl Fn(bool, bool) -> bool,
264    ) -> bool {
265        !merged_x.for_merged_indexes(|var5x, var6, _var7| {
266            merged_y.for_merged_indexes(|var6x, var7x, _var8| {
267                merged_z.for_merged_indexes(|var7, var8x, _var9| {
268                    !op(
269                        shape1.is_full_wide(var5x, var6x, var7),
270                        shape2.is_full_wide(var6, var7x, var8x),
271                    )
272                })
273            })
274        })
275    }
276
277    pub fn create_index_merger(
278        _var0: i32,
279        coords1: &[f64],
280        coords2: &[f64],
281        var3: bool,
282        var4: bool,
283    ) -> IndexMerger {
284        let var5 = coords1.len() - 1;
285        let var6 = coords2.len() - 1;
286        // if (&var1 as &dyn Any).is::<CubePointRange>() && (&var2 as &dyn
287        // Any).is::<CubePointRange>() {
288        // return new DiscreteCubeMerger(var0, var5, var6, var3, var4);
289        // let var7: i64 = lcm(var5 as u32, var6 as u32).try_into().unwrap();
290        // //    if ((long)var0 * var7 <= 256L) {
291        // if var0 as i64 * var7 <= 256 {
292        //     return IndexMerger::new_discrete_cube(var5 as u32, var6 as u32);
293        // }
294        // }
295
296        if coords1[var5] < coords2[0] - EPSILON {
297            IndexMerger::NonOverlapping {
298                lower: coords1.to_vec(),
299                upper: coords2.to_vec(),
300                swap: false,
301            }
302        } else if coords2[var6] < coords1[0] - EPSILON {
303            IndexMerger::NonOverlapping {
304                lower: coords2.to_vec(),
305                upper: coords1.to_vec(),
306                swap: true,
307            }
308        } else if var5 == var6 && coords1 == coords2 {
309            IndexMerger::Identical {
310                coords: coords1.to_vec(),
311            }
312        } else {
313            IndexMerger::new_indirect(coords1, coords2, var3, var4)
314        }
315    }
316}
317
318#[derive(Clone, PartialEq, Debug)]
319pub enum VoxelShape {
320    Array(ArrayVoxelShape),
321    Cube(CubeVoxelShape),
322}
323
324impl VoxelShape {
325    // public double min(Direction.Axis var1) {
326    //     int var2 = this.shape.firstFull(var1);
327    //     return var2 >= this.shape.getSize(var1) ? 1.0D / 0.0 : this.get(var1,
328    // var2); }
329    // public double max(Direction.Axis var1) {
330    //     int var2 = this.shape.lastFull(var1);
331    //     return var2 <= 0 ? -1.0D / 0.0 : this.get(var1, var2);
332    // }
333    fn min(&self, axis: Axis) -> f64 {
334        let first_full = self.shape().first_full(axis);
335        if first_full >= self.shape().size(axis) as i32 {
336            f64::INFINITY
337        } else {
338            self.get(axis, first_full.try_into().unwrap())
339        }
340    }
341    fn max(&self, axis: Axis) -> f64 {
342        let last_full = self.shape().last_full(axis);
343        if last_full <= 0 {
344            f64::NEG_INFINITY
345        } else {
346            self.get(axis, last_full.try_into().unwrap())
347        }
348    }
349
350    pub fn shape(&self) -> &DiscreteVoxelShape {
351        match self {
352            VoxelShape::Array(s) => s.shape(),
353            VoxelShape::Cube(s) => s.shape(),
354        }
355    }
356
357    pub fn get_coords(&self, axis: Axis) -> &[f64] {
358        match self {
359            VoxelShape::Array(s) => s.get_coords(axis),
360            VoxelShape::Cube(s) => s.get_coords(axis),
361        }
362    }
363
364    pub fn is_empty(&self) -> bool {
365        self.shape().is_empty()
366    }
367
368    #[must_use]
369    pub fn move_relative(&self, delta: Vec3) -> VoxelShape {
370        if self.shape().is_empty() {
371            return EMPTY_SHAPE.clone();
372        }
373
374        VoxelShape::Array(ArrayVoxelShape::new(
375            self.shape().to_owned(),
376            self.get_coords(Axis::X)
377                .iter()
378                .map(|c| c + delta.x)
379                .collect(),
380            self.get_coords(Axis::Y)
381                .iter()
382                .map(|c| c + delta.y)
383                .collect(),
384            self.get_coords(Axis::Z)
385                .iter()
386                .map(|c| c + delta.z)
387                .collect(),
388        ))
389    }
390
391    #[inline]
392    pub fn get(&self, axis: Axis, index: usize) -> f64 {
393        // self.get_coords(axis)[index]
394        match self {
395            VoxelShape::Array(s) => s.get_coords(axis)[index],
396            VoxelShape::Cube(s) => s.get_coords(axis)[index],
397            // _ => self.get_coords(axis)[index],
398        }
399    }
400
401    pub fn find_index(&self, axis: Axis, coord: f64) -> i32 {
402        // let r = binary_search(0, (self.shape().size(axis) + 1) as i32, &|t| {
403        //     coord < self.get(axis, t as usize)
404        // }) - 1;
405        // r
406        match self {
407            VoxelShape::Cube(s) => s.find_index(axis, coord),
408            _ => {
409                let upper_limit = (self.shape().size(axis) + 1) as i32;
410                binary_search(0, upper_limit, |t| coord < self.get(axis, t as usize)) - 1
411            }
412        }
413    }
414
415    pub fn clip(&self, from: &Vec3, to: &Vec3, block_pos: &BlockPos) -> Option<BlockHitResult> {
416        if self.is_empty() {
417            return None;
418        }
419        let vector = to - from;
420        if vector.length_squared() < EPSILON {
421            return None;
422        }
423        let right_after_start = from + &(vector * 0.0001);
424
425        if self.shape().is_full_wide(
426            self.find_index(Axis::X, right_after_start.x - block_pos.x as f64),
427            self.find_index(Axis::Y, right_after_start.y - block_pos.y as f64),
428            self.find_index(Axis::Z, right_after_start.z - block_pos.z as f64),
429        ) {
430            Some(BlockHitResult {
431                block_pos: *block_pos,
432                direction: Direction::nearest(vector).opposite(),
433                location: right_after_start,
434                inside: true,
435                miss: false,
436                world_border: false,
437            })
438        } else {
439            AABB::clip_iterable(&self.to_aabbs(), from, to, block_pos)
440        }
441    }
442
443    pub fn collide(&self, axis: &Axis, entity_box: &AABB, movement: f64) -> f64 {
444        self.collide_x(AxisCycle::between(*axis, Axis::X), entity_box, movement)
445    }
446    pub fn collide_x(&self, axis_cycle: AxisCycle, entity_box: &AABB, mut movement: f64) -> f64 {
447        if self.shape().is_empty() {
448            return movement;
449        }
450        if movement.abs() < EPSILON {
451            return 0.;
452        }
453
454        let inverse_axis_cycle = axis_cycle.inverse();
455
456        let x_axis = inverse_axis_cycle.cycle(Axis::X);
457        let y_axis = inverse_axis_cycle.cycle(Axis::Y);
458        let z_axis = inverse_axis_cycle.cycle(Axis::Z);
459
460        let max_x = entity_box.max(&x_axis);
461        let min_x = entity_box.min(&x_axis);
462
463        let x_min_index = self.find_index(x_axis, min_x + EPSILON);
464        let x_max_index = self.find_index(x_axis, max_x - EPSILON);
465
466        let y_min_index = cmp::max(
467            0,
468            self.find_index(y_axis, entity_box.min(&y_axis) + EPSILON),
469        );
470        let y_max_index = cmp::min(
471            self.shape().size(y_axis) as i32,
472            self.find_index(y_axis, entity_box.max(&y_axis) - EPSILON) + 1,
473        );
474
475        let z_min_index = cmp::max(
476            0,
477            self.find_index(z_axis, entity_box.min(&z_axis) + EPSILON),
478        );
479        let z_max_index = cmp::min(
480            self.shape().size(z_axis) as i32,
481            self.find_index(z_axis, entity_box.max(&z_axis) - EPSILON) + 1,
482        );
483
484        if movement > 0. {
485            for x in x_max_index + 1..(self.shape().size(x_axis) as i32) {
486                for y in y_min_index..y_max_index {
487                    for z in z_min_index..z_max_index {
488                        if self
489                            .shape()
490                            .is_full_wide_axis_cycle(inverse_axis_cycle, x, y, z)
491                        {
492                            let var23 = self.get(x_axis, x as usize) - max_x;
493                            if var23 >= -EPSILON {
494                                movement = f64::min(movement, var23);
495                            }
496                            return movement;
497                        }
498                    }
499                }
500            }
501        } else if movement < 0. && x_min_index > 0 {
502            for x in (0..x_min_index).rev() {
503                for y in y_min_index..y_max_index {
504                    for z in z_min_index..z_max_index {
505                        if self
506                            .shape()
507                            .is_full_wide_axis_cycle(inverse_axis_cycle, x, y, z)
508                        {
509                            let var23 = self.get(x_axis, (x + 1) as usize) - min_x;
510                            if var23 <= EPSILON {
511                                movement = f64::max(movement, var23);
512                            }
513                            return movement;
514                        }
515                    }
516                }
517            }
518        }
519
520        movement
521    }
522
523    fn optimize(&self) -> VoxelShape {
524        let mut shape = EMPTY_SHAPE.clone();
525        self.for_all_boxes(|var1x, var3, var5, var7, var9, var11| {
526            shape = Shapes::join_unoptimized(
527                shape.clone(),
528                box_shape(var1x, var3, var5, var7, var9, var11),
529                |a, b| a || b,
530            );
531        });
532        shape
533    }
534
535    pub fn for_all_boxes(&self, mut consumer: impl FnMut(f64, f64, f64, f64, f64, f64))
536    where
537        Self: Sized,
538    {
539        let x_coords = self.get_coords(Axis::X);
540        let y_coords = self.get_coords(Axis::Y);
541        let z_coords = self.get_coords(Axis::Z);
542        self.shape().for_all_boxes(
543            |min_x, min_y, min_z, max_x, max_y, max_z| {
544                consumer(
545                    x_coords[min_x as usize],
546                    y_coords[min_y as usize],
547                    z_coords[min_z as usize],
548                    x_coords[max_x as usize],
549                    y_coords[max_y as usize],
550                    z_coords[max_z as usize],
551                );
552            },
553            true,
554        );
555    }
556
557    pub fn to_aabbs(&self) -> Vec<AABB> {
558        let mut aabbs = Vec::new();
559        self.for_all_boxes(|min_x, min_y, min_z, max_x, max_y, max_z| {
560            aabbs.push(AABB {
561                min: Vec3::new(min_x, min_y, min_z),
562                max: Vec3::new(max_x, max_y, max_z),
563            });
564        });
565        aabbs
566    }
567
568    pub fn bounds(&self) -> AABB {
569        assert!(!self.is_empty(), "Can't get bounds for empty shape");
570        AABB {
571            min: Vec3::new(self.min(Axis::X), self.min(Axis::Y), self.min(Axis::Z)),
572            max: Vec3::new(self.max(Axis::X), self.max(Axis::Y), self.max(Axis::Z)),
573        }
574    }
575}
576
577impl From<AABB> for VoxelShape {
578    fn from(aabb: AABB) -> Self {
579        box_shape(
580            aabb.min.x, aabb.min.y, aabb.min.z, aabb.max.x, aabb.max.y, aabb.max.z,
581        )
582    }
583}
584
585#[derive(Clone, PartialEq, Debug)]
586pub struct ArrayVoxelShape {
587    shape: DiscreteVoxelShape,
588    // TODO: check where faces is used in minecraft
589    #[allow(dead_code)]
590    faces: Option<Vec<VoxelShape>>,
591
592    pub xs: Vec<f64>,
593    pub ys: Vec<f64>,
594    pub zs: Vec<f64>,
595}
596
597#[derive(Clone, PartialEq, Debug)]
598pub struct CubeVoxelShape {
599    shape: DiscreteVoxelShape,
600    // TODO: check where faces is used in minecraft
601    #[allow(dead_code)]
602    faces: Option<Vec<VoxelShape>>,
603
604    x_coords: Vec<f64>,
605    y_coords: Vec<f64>,
606    z_coords: Vec<f64>,
607}
608
609impl ArrayVoxelShape {
610    pub fn new(shape: DiscreteVoxelShape, xs: Vec<f64>, ys: Vec<f64>, zs: Vec<f64>) -> Self {
611        let x_size = shape.size(Axis::X) + 1;
612        let y_size = shape.size(Axis::Y) + 1;
613        let z_size = shape.size(Axis::Z) + 1;
614
615        // Lengths of point arrays must be consistent with the size of the VoxelShape.
616        debug_assert_eq!(x_size, xs.len() as u32);
617        debug_assert_eq!(y_size, ys.len() as u32);
618        debug_assert_eq!(z_size, zs.len() as u32);
619
620        Self {
621            faces: None,
622            shape,
623            xs,
624            ys,
625            zs,
626        }
627    }
628}
629
630impl ArrayVoxelShape {
631    fn shape(&self) -> &DiscreteVoxelShape {
632        &self.shape
633    }
634
635    #[inline]
636    fn get_coords(&self, axis: Axis) -> &[f64] {
637        axis.choose(&self.xs, &self.ys, &self.zs)
638    }
639}
640
641impl CubeVoxelShape {
642    pub fn new(shape: DiscreteVoxelShape) -> Self {
643        // pre-calculate the coor
644        let x_coords = Self::calculate_coords(&shape, Axis::X);
645        let y_coords = Self::calculate_coords(&shape, Axis::Y);
646        let z_coords = Self::calculate_coords(&shape, Axis::Z);
647
648        Self {
649            shape,
650            faces: None,
651            x_coords,
652            y_coords,
653            z_coords,
654        }
655    }
656}
657
658impl CubeVoxelShape {
659    fn shape(&self) -> &DiscreteVoxelShape {
660        &self.shape
661    }
662
663    fn calculate_coords(shape: &DiscreteVoxelShape, axis: Axis) -> Vec<f64> {
664        let size = shape.size(axis);
665        let mut parts = Vec::with_capacity(size as usize);
666        for i in 0..=size {
667            parts.push(i as f64 / size as f64);
668        }
669        parts
670    }
671
672    #[inline]
673    fn get_coords(&self, axis: Axis) -> &[f64] {
674        axis.choose(&self.x_coords, &self.y_coords, &self.z_coords)
675    }
676
677    fn find_index(&self, axis: Axis, coord: f64) -> i32 {
678        let n = self.shape().size(axis);
679        (f64::clamp(coord * (n as f64), -1f64, n as f64)) as i32
680    }
681}
682
683#[derive(Debug)]
684pub struct CubePointRange {
685    /// Needs at least 1 part
686    pub parts: NonZeroU32,
687}
688impl CubePointRange {
689    pub fn get_double(&self, index: u32) -> f64 {
690        index as f64 / self.parts.get() as f64
691    }
692
693    pub fn size(&self) -> u32 {
694        self.parts.get() + 1
695    }
696
697    pub fn iter(&self) -> Vec<f64> {
698        (0..=self.parts.get()).map(|i| self.get_double(i)).collect()
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_block_shape() {
708        let shape = &*BLOCK_SHAPE;
709        assert_eq!(shape.shape().size(Axis::X), 1);
710        assert_eq!(shape.shape().size(Axis::Y), 1);
711        assert_eq!(shape.shape().size(Axis::Z), 1);
712
713        assert_eq!(shape.get_coords(Axis::X).len(), 2);
714        assert_eq!(shape.get_coords(Axis::Y).len(), 2);
715        assert_eq!(shape.get_coords(Axis::Z).len(), 2);
716    }
717
718    #[test]
719    fn test_box_shape() {
720        let shape = box_shape(0., 0., 0., 1., 1., 1.);
721        assert_eq!(shape.shape().size(Axis::X), 1);
722        assert_eq!(shape.shape().size(Axis::Y), 1);
723        assert_eq!(shape.shape().size(Axis::Z), 1);
724
725        assert_eq!(shape.get_coords(Axis::X).len(), 2);
726        assert_eq!(shape.get_coords(Axis::Y).len(), 2);
727        assert_eq!(shape.get_coords(Axis::Z).len(), 2);
728    }
729
730    #[test]
731    fn test_top_slab_shape() {
732        let shape = box_shape(0., 0.5, 0., 1., 1., 1.);
733        assert_eq!(shape.shape().size(Axis::X), 1);
734        assert_eq!(shape.shape().size(Axis::Y), 2);
735        assert_eq!(shape.shape().size(Axis::Z), 1);
736
737        assert_eq!(shape.get_coords(Axis::X).len(), 2);
738        assert_eq!(shape.get_coords(Axis::Y).len(), 3);
739        assert_eq!(shape.get_coords(Axis::Z).len(), 2);
740    }
741
742    #[test]
743    fn test_join_is_not_empty() {
744        let shape = box_shape(0., 0., 0., 1., 1., 1.);
745        let shape2 = box_shape(0., 0.5, 0., 1., 1., 1.);
746        // detect if the shapes intersect at all
747        let joined = Shapes::matches_anywhere(&shape, &shape2, |a, b| a && b);
748        assert!(joined, "Shapes should intersect");
749    }
750}