zng_unit/
orientation.rs

1use crate::{Px, PxBox, PxPoint, PxRect, PxSize, PxVector};
2
3use serde::{Deserialize, Serialize};
4
5/// Orientation of two 2D items.
6#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
7pub enum Orientation2D {
8    /// Point is above the origin.
9    Above,
10    /// Point is to the right of the origin.
11    Right,
12    /// Point is below the origin.
13    Below,
14    /// Point is to the left of the origin.
15    Left,
16}
17impl Orientation2D {
18    /// Check if `point` is orientation from `origin`.
19    ///
20    /// Returns `true` if the point is hit by a 45º frustum cast from origin in the direction defined by the orientation.
21    pub fn point_is(self, origin: PxPoint, point: PxPoint) -> bool {
22        let (a, b, c, d) = match self {
23            Orientation2D::Above => (point.y, origin.y, point.x, origin.x),
24            Orientation2D::Right => (origin.x, point.x, point.y, origin.y),
25            Orientation2D::Below => (origin.y, point.y, point.x, origin.x),
26            Orientation2D::Left => (point.x, origin.x, point.y, origin.y),
27        };
28
29        let mut is = false;
30
31        // for 'Above' this is:
32        // is above line?
33        if a < b {
34            // is to the right?
35            if c > d {
36                // is in the 45º 'frustum'
37                // │?╱
38                // │╱__
39                is = c <= d + (b - a);
40            } else {
41                //  ╲?│
42                // __╲│
43                is = c >= d - (b - a);
44            }
45        }
46
47        is
48    }
49
50    /// Check if `b` is orientation from `origin`.
51    ///
52    /// Returns `true` if the box `b` collides with the box `origin` in the direction defined by orientation. Also
53    /// returns `true` if the boxes already overlap.
54    pub fn box_is(self, origin: PxBox, b: PxBox) -> bool {
55        fn d_intersects(a_min: Px, a_max: Px, b_min: Px, b_max: Px) -> bool {
56            a_min < b_max && a_max > b_min
57        }
58        match self {
59            Orientation2D::Above => b.min.y <= origin.min.y && d_intersects(b.min.x, b.max.x, origin.min.x, origin.max.x),
60            Orientation2D::Left => b.min.x <= origin.min.x && d_intersects(b.min.y, b.max.y, origin.min.y, origin.max.y),
61            Orientation2D::Below => b.max.y >= origin.max.y && d_intersects(b.min.x, b.max.x, origin.min.x, origin.max.x),
62            Orientation2D::Right => b.max.x >= origin.max.x && d_intersects(b.min.y, b.max.y, origin.min.y, origin.max.y),
63        }
64    }
65
66    /// Iterator that yields quadrants for efficient search in a quad-tree, if a point is inside a quadrant and
67    /// passes the [`Orientation2D::point_is`] check it is in the orientation, them if it is within the `max_distance` it is valid.
68    pub fn search_bounds(self, origin: PxPoint, max_distance: Px, spatial_bounds: PxBox) -> impl Iterator<Item = PxBox> + 'static {
69        let mut bounds = PxRect::new(origin, PxSize::splat(max_distance));
70        match self {
71            Orientation2D::Above => {
72                bounds.origin.x -= max_distance / Px(2);
73                bounds.origin.y -= max_distance;
74            }
75            Orientation2D::Right => bounds.origin.y -= max_distance / Px(2),
76            Orientation2D::Below => bounds.origin.x -= max_distance / Px(2),
77            Orientation2D::Left => {
78                bounds.origin.y -= max_distance / Px(2);
79                bounds.origin.x -= max_distance;
80            }
81        }
82
83        // oriented search is a 45º square in the direction specified, so we grow and cut the search quadrant like
84        // in the "nearest with bounds" algorithm, but then cut again to only the part that fully overlaps the 45º
85        // square, points found are then matched with the `Orientation2D::is` method.
86
87        let max_quad = spatial_bounds.intersection_unchecked(&bounds.to_box2d());
88        let mut is_none = max_quad.is_empty();
89
90        let mut source_quad = PxRect::new(origin - PxVector::splat(Px(64)), PxSize::splat(Px(128))).to_box2d();
91        let mut search_quad = source_quad.intersection_unchecked(&max_quad);
92        is_none |= search_quad.is_empty();
93
94        let max_diameter = max_distance * Px(2);
95
96        let mut is_first = true;
97
98        std::iter::from_fn(move || {
99            let source_width = source_quad.width();
100            if is_none {
101                None
102            } else if is_first {
103                is_first = false;
104                Some(search_quad)
105            } else if source_width >= max_diameter {
106                is_none = true;
107                None
108            } else {
109                source_quad = source_quad.inflate(source_width, source_width);
110                let mut new_search = source_quad.intersection_unchecked(&max_quad);
111                if new_search == source_quad || new_search.is_empty() {
112                    is_none = true; // filled bounds
113                    return None;
114                }
115
116                match self {
117                    Orientation2D::Above => {
118                        new_search.max.y = search_quad.min.y;
119                    }
120                    Orientation2D::Right => {
121                        new_search.min.x = search_quad.max.x;
122                    }
123                    Orientation2D::Below => {
124                        new_search.min.y = search_quad.max.y;
125                    }
126                    Orientation2D::Left => {
127                        new_search.max.x = search_quad.min.x;
128                    }
129                }
130
131                search_quad = new_search;
132
133                Some(search_quad)
134            }
135        })
136    }
137}