jagua_rs/geometry/primitives/
rect.rs

1use crate::geometry::geo_enums::{GeoPosition, GeoRelation};
2use crate::geometry::geo_traits::{
3    AlmostCollidesWith, CollidesWith, DistanceTo, SeparationDistance,
4};
5use crate::geometry::primitives::Edge;
6use crate::geometry::primitives::Point;
7use crate::util::FPA;
8use anyhow::Result;
9use anyhow::ensure;
10use ordered_float::OrderedFloat;
11
12///Axis-aligned rectangle
13#[derive(Clone, Debug, PartialEq, Copy)]
14pub struct Rect {
15    pub x_min: f32,
16    pub y_min: f32,
17    pub x_max: f32,
18    pub y_max: f32,
19}
20
21impl Rect {
22    pub fn try_new(x_min: f32, y_min: f32, x_max: f32, y_max: f32) -> Result<Self> {
23        ensure!(
24            x_min < x_max && y_min < y_max,
25            "invalid rectangle, x_min: {x_min}, x_max: {x_max}, y_min: {y_min}, y_max: {y_max}"
26        );
27        Ok(Rect {
28            x_min,
29            y_min,
30            x_max,
31            y_max,
32        })
33    }
34
35    pub fn from_diagonal_corners(c1: Point, c2: Point) -> Result<Self> {
36        let x_min = f32::min(c1.x(), c2.x());
37        let y_min = f32::min(c1.y(), c2.y());
38        let x_max = f32::max(c1.x(), c2.x());
39        let y_max = f32::max(c1.y(), c2.y());
40        Rect::try_new(x_min, y_min, x_max, y_max)
41    }
42
43    /// Returns the geometric relation between `self` and another [`Rect`].
44    /// Optimized for `GeoRelation::Disjoint`
45    #[inline(always)]
46    pub fn relation_to(&self, other: Rect) -> GeoRelation {
47        if !self.collides_with(&other) {
48            return GeoRelation::Disjoint;
49        }
50        if self.x_min <= other.x_min
51            && self.y_min <= other.y_min
52            && self.x_max >= other.x_max
53            && self.y_max >= other.y_max
54        {
55            return GeoRelation::Surrounding;
56        }
57        if self.x_min >= other.x_min
58            && self.y_min >= other.y_min
59            && self.x_max <= other.x_max
60            && self.y_max <= other.y_max
61        {
62            return GeoRelation::Enclosed;
63        }
64        GeoRelation::Intersecting
65    }
66
67    /// Returns the [`GeoRelation`] between `self` and another [`Rect`], with a tolerance for floating point precision.
68    /// In edge cases, this method will lean towards `Surrounding` and `Enclosed` instead of `Intersecting`.
69    #[inline(always)]
70    pub fn almost_relation_to(&self, other: Rect) -> GeoRelation {
71        if !self.almost_collides_with(&other) {
72            return GeoRelation::Disjoint;
73        }
74        if FPA::from(self.x_min) <= FPA::from(other.x_min)
75            && FPA::from(self.y_min) <= FPA::from(other.y_min)
76            && FPA::from(self.x_max) >= FPA::from(other.x_max)
77            && FPA::from(self.y_max) >= FPA::from(other.y_max)
78        {
79            return GeoRelation::Surrounding;
80        }
81        if FPA::from(self.x_min) >= FPA::from(other.x_min)
82            && FPA::from(self.y_min) >= FPA::from(other.y_min)
83            && FPA::from(self.x_max) <= FPA::from(other.x_max)
84            && FPA::from(self.y_max) <= FPA::from(other.y_max)
85        {
86            return GeoRelation::Enclosed;
87        }
88        GeoRelation::Intersecting
89    }
90
91    /// Returns a new rectangle with the same centroid but inflated
92    /// to be the minimum square that contains `self`.
93    pub fn inflate_to_square(&self) -> Rect {
94        let width = self.x_max - self.x_min;
95        let height = self.y_max - self.y_min;
96        let mut dx = 0.0;
97        let mut dy = 0.0;
98        if height < width {
99            dy = (width - height) / 2.0;
100        } else if width < height {
101            dx = (height - width) / 2.0;
102        }
103        Rect {
104            x_min: self.x_min - dx,
105            y_min: self.y_min - dy,
106            x_max: self.x_max + dx,
107            y_max: self.y_max + dy,
108        }
109    }
110
111    /// Returns a new rectangle with the same centroid but scaled by `factor`.
112    pub fn scale(self, factor: f32) -> Self {
113        let dx = (self.x_max - self.x_min) * (factor - 1.0) / 2.0;
114        let dy = (self.y_max - self.y_min) * (factor - 1.0) / 2.0;
115        self.resize_by(dx, dy)
116            .expect("scaling should not lead to invalid rectangle")
117    }
118
119    /// Returns a new rectangle with the same centroid as `self` but expanded by `dx` in both x-directions and by `dy` in both y-directions.
120    /// If the new rectangle is invalid (x_min >= x_max or y_min >= y_max), returns None.
121    pub fn resize_by(mut self, dx: f32, dy: f32) -> Option<Self> {
122        self.x_min -= dx;
123        self.y_min -= dy;
124        self.x_max += dx;
125        self.y_max += dy;
126
127        if self.x_min < self.x_max && self.y_min < self.y_max {
128            Some(self)
129        } else {
130            //resizing would lead to invalid rectangle
131            None
132        }
133    }
134
135    /// For all quadrants, contains indices of the two neighbors of the quadrant at that index.
136    pub const QUADRANT_NEIGHBOR_LAYOUT: [[usize; 2]; 4] = [[1, 3], [0, 2], [1, 3], [0, 2]];
137
138    /// Returns the 4 quadrants of `self`.
139    /// Ordered in the same way as quadrants in a cartesian plane:
140    /// <https://en.wikipedia.org/wiki/Quadrant_(plane_geometry)>
141    pub fn quadrants(&self) -> [Self; 4] {
142        let mid = self.centroid();
143        let corners = self.corners();
144
145        let q1 = Rect::from_diagonal_corners(corners[0], mid).unwrap();
146        let q2 = Rect::from_diagonal_corners(corners[1], mid).unwrap();
147        let q3 = Rect::from_diagonal_corners(corners[2], mid).unwrap();
148        let q4 = Rect::from_diagonal_corners(corners[3], mid).unwrap();
149
150        [q1, q2, q3, q4]
151    }
152
153    /// Returns the four corners of `self`, in the same order as [Rect::quadrants].
154    pub fn corners(&self) -> [Point; 4] {
155        [
156            Point(self.x_max, self.y_max),
157            Point(self.x_min, self.y_max),
158            Point(self.x_min, self.y_min),
159            Point(self.x_max, self.y_min),
160        ]
161    }
162
163    /// Returns the four edges that make up `self`, in the same order as [Rect::quadrants].
164    pub fn edges(&self) -> [Edge; 4] {
165        let c = self.corners();
166        [
167            Edge {
168                start: c[0],
169                end: c[1],
170            },
171            Edge {
172                start: c[1],
173                end: c[2],
174            },
175            Edge {
176                start: c[2],
177                end: c[3],
178            },
179            Edge {
180                start: c[3],
181                end: c[0],
182            },
183        ]
184    }
185    pub fn width(&self) -> f32 {
186        self.x_max - self.x_min
187    }
188
189    pub fn height(&self) -> f32 {
190        self.y_max - self.y_min
191    }
192
193    /// Returns the largest rectangle that is contained in both `a` and `b`.
194    pub fn intersection(a: Rect, b: Rect) -> Option<Rect> {
195        let x_min = f32::max(a.x_min, b.x_min);
196        let y_min = f32::max(a.y_min, b.y_min);
197        let x_max = f32::min(a.x_max, b.x_max);
198        let y_max = f32::min(a.y_max, b.y_max);
199        if x_min < x_max && y_min < y_max {
200            Some(Rect {
201                x_min,
202                y_min,
203                x_max,
204                y_max,
205            })
206        } else {
207            None
208        }
209    }
210
211    /// Returns the smallest rectangle that contains both `a` and `b`.
212    pub fn bounding_rect(a: Rect, b: Rect) -> Rect {
213        let x_min = f32::min(a.x_min, b.x_min);
214        let y_min = f32::min(a.y_min, b.y_min);
215        let x_max = f32::max(a.x_max, b.x_max);
216        let y_max = f32::max(a.y_max, b.y_max);
217        Rect {
218            x_min,
219            y_min,
220            x_max,
221            y_max,
222        }
223    }
224
225    pub fn centroid(&self) -> Point {
226        Point(
227            (self.x_min + self.x_max) / 2.0,
228            (self.y_min + self.y_max) / 2.0,
229        )
230    }
231
232    pub fn area(&self) -> f32 {
233        (self.x_max - self.x_min) * (self.y_max - self.y_min)
234    }
235
236    pub fn diameter(&self) -> f32 {
237        let dx = self.x_max - self.x_min;
238        let dy = self.y_max - self.y_min;
239        (dx.powi(2) + dy.powi(2)).sqrt()
240    }
241}
242
243impl CollidesWith<Rect> for Rect {
244    #[inline(always)]
245    fn collides_with(&self, other: &Rect) -> bool {
246        f32::max(self.x_min, other.x_min) <= f32::min(self.x_max, other.x_max)
247            && f32::max(self.y_min, other.y_min) <= f32::min(self.y_max, other.y_max)
248    }
249}
250
251impl AlmostCollidesWith<Rect> for Rect {
252    #[inline(always)]
253    fn almost_collides_with(&self, other: &Rect) -> bool {
254        FPA(f32::max(self.x_min, other.x_min)) <= FPA(f32::min(self.x_max, other.x_max))
255            && FPA(f32::max(self.y_min, other.y_min)) <= FPA(f32::min(self.y_max, other.y_max))
256    }
257}
258
259impl CollidesWith<Point> for Rect {
260    #[inline(always)]
261    fn collides_with(&self, point: &Point) -> bool {
262        let Point(x, y) = *point;
263        x >= self.x_min && x <= self.x_max && y >= self.y_min && y <= self.y_max
264    }
265}
266
267impl AlmostCollidesWith<Point> for Rect {
268    #[inline(always)]
269    fn almost_collides_with(&self, point: &Point) -> bool {
270        let (x, y) = (*point).into();
271        FPA(x) >= FPA(self.x_min)
272            && FPA(x) <= FPA(self.x_max)
273            && FPA(y) >= FPA(self.y_min)
274            && FPA(y) <= FPA(self.y_max)
275    }
276}
277
278impl CollidesWith<Edge> for Rect {
279    #[inline(always)]
280    fn collides_with(&self, edge: &Edge) -> bool {
281        //inspired by: https://stackoverflow.com/questions/99353/how-to-test-if-a-line-segment-intersects-an-axis-aligned-rectange-in-2d
282
283        let e_x_min = edge.x_min();
284        let e_x_max = edge.x_max();
285        let e_y_min = edge.y_min();
286        let e_y_max = edge.y_max();
287
288        let x_no_overlap = e_x_min.max(self.x_min) > e_x_max.min(self.x_max);
289        let y_no_overlap = e_y_min.max(self.y_min) > e_y_max.min(self.y_max);
290
291        if x_no_overlap || y_no_overlap {
292            return false; // Bounding boxes do not overlap
293        }
294
295        //If either end point of the line is inside the rectangle
296        if self.collides_with(&edge.start) || self.collides_with(&edge.end) {
297            return true;
298        }
299
300        //Determine which side of the edge each corner of the rectangle is on
301        let corner_sides = self.corners().map(|corner| {
302            let Point(p_x, p_y) = corner;
303            let Point(s_x, s_y) = edge.start;
304            let Point(e_x, e_y) = edge.end;
305            // if 0.0, the corner is on the edge
306            // if > 0.0, the corner is "above" of the edge
307            // if < 0.0, the corner is "below" the edge
308            (p_x - s_x) * (e_y - s_y) - (p_y - s_y) * (e_x - s_x)
309        });
310
311        //No collision if all corners are on the same side of the edge
312        if corner_sides.iter().all(|v| *v > 0.0) || corner_sides.iter().all(|v| *v < 0.0) {
313            return false;
314        }
315
316        //The only possible that remains is that the edge collides with one of the edges of the rectangle
317        self.edges()
318            .iter()
319            .any(|rect_edge| edge.collides_with(rect_edge))
320    }
321}
322
323impl DistanceTo<Point> for Rect {
324    #[inline(always)]
325    fn distance_to(&self, point: &Point) -> f32 {
326        self.sq_distance_to(point).sqrt()
327    }
328
329    #[inline(always)]
330    fn sq_distance_to(&self, point: &Point) -> f32 {
331        let Point(x, y) = *point;
332        let mut distance: f32 = 0.0;
333        if x < self.x_min {
334            distance += (x - self.x_min).powi(2);
335        } else if x > self.x_max {
336            distance += (x - self.x_max).powi(2);
337        }
338        if y < self.y_min {
339            distance += (y - self.y_min).powi(2);
340        } else if y > self.y_max {
341            distance += (y - self.y_max).powi(2);
342        }
343        distance.abs()
344    }
345}
346
347impl SeparationDistance<Point> for Rect {
348    #[inline(always)]
349    fn separation_distance(&self, point: &Point) -> (GeoPosition, f32) {
350        let (position, sq_distance) = self.sq_separation_distance(point);
351        (position, sq_distance.sqrt())
352    }
353
354    #[inline(always)]
355    fn sq_separation_distance(&self, point: &Point) -> (GeoPosition, f32) {
356        match self.collides_with(point) {
357            false => (GeoPosition::Exterior, self.sq_distance_to(point)),
358            true => {
359                let Point(x, y) = *point;
360                let min_distance = [
361                    (x - self.x_min).abs(),
362                    (x - self.x_max).abs(),
363                    (y - self.y_min).abs(),
364                    (y - self.y_max).abs(),
365                ]
366                .into_iter()
367                .min_by_key(|&d| OrderedFloat(d))
368                .unwrap();
369                (GeoPosition::Interior, min_distance.powi(2))
370            }
371        }
372    }
373}