Skip to main content

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