retrofire_core/util/
rect.rs

1//! Rectangular regions.
2
3use core::fmt::Debug;
4use core::ops::{Bound::*, Range, RangeBounds, RangeFull, Sub};
5
6use crate::math::Point2u;
7
8/// An axis-aligned rectangular region.
9///
10/// Each of the four sides of a `Rect` can be either bounded (`Some(_)`) or
11/// unbounded (`None`). If bounded, the start bounds (left and top) are always
12/// inclusive, and the end bounds (right and bottom) are always exclusive.
13///
14/// If `left` and `right` are unbounded and `right` ≤ `left`, the `Rect` is
15/// considered empty. The same holds for `top` and `bottom`. This matches the
16/// semantics of the standard `Range` type.
17#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
18pub struct Rect<T = u32> {
19    /// The left bound of `self`, if any.
20    pub left: Option<T>,
21    /// The top bound of `self`, if any.
22    pub top: Option<T>,
23    /// The right bound of `self`, if any.
24    pub right: Option<T>,
25    /// The bottom bound of `self`, if any.
26    pub bottom: Option<T>,
27}
28
29impl<T: Copy> Rect<T> {
30    /// Returns the width of `self`, or `None` if horizontally unbounded.
31    ///
32    /// Returns 0 if `right` <= `left`.
33    pub fn width(&self) -> Option<T::Output>
34    where
35        T: Ord + Sub,
36    {
37        let (r, l) = (self.right?, self.left?);
38        Some(r - r.min(l)) // Clamp width to 0
39    }
40    /// Returns the height of `self`, or `None` if vertically unbounded.
41    ///
42    /// Returns 0 if `bottom` <= `top`.
43    pub fn height(&self) -> Option<T::Output>
44    where
45        T: Ord + Sub,
46    {
47        let (b, t) = (self.bottom?, self.top?);
48        Some(b - b.min(t)) // Clamp height to 0
49    }
50
51    /// Returns whether `self` contains no points.
52    pub fn is_empty(&self) -> bool
53    where
54        T: PartialOrd,
55    {
56        let Rect { left, top, right, bottom } = self;
57        // Empty if either extent is a bounded, empty range
58        let h_empty = left.is_some() && right.is_some() && left >= right;
59        let v_empty = top.is_some() && bottom.is_some() && top >= bottom;
60        h_empty || v_empty
61    }
62
63    /// Returns whether the point (x, y)  is contained within `self`.
64    pub fn contains(&self, x: T, y: T) -> bool
65    where
66        T: PartialOrd,
67    {
68        let [horiz, vert] = self.bounds();
69        horiz.contains(&x) && vert.contains(&y)
70    }
71
72    /// Returns the horizontal and vertical extents of `self`.
73    pub fn bounds(&self) -> [impl RangeBounds<T>; 2] {
74        let left = self.left.map(Included).unwrap_or(Unbounded);
75        let top = self.top.map(Included).unwrap_or(Unbounded);
76        let right = self.right.map(Excluded).unwrap_or(Unbounded);
77        let bottom = self.bottom.map(Excluded).unwrap_or(Unbounded);
78
79        [(left, right), (top, bottom)]
80    }
81
82    /// Returns the intersection of `self` and `other`.
83    ///
84    /// The intersection is a rect that contains exactly the points contained
85    /// by both `self` and `other`. If the input rects are disjoint, i.e.
86    /// contain no common points, the result is an empty rect with unspecified
87    /// top-left coordinates.
88    #[must_use]
89    pub fn intersect(&self, other: &Self) -> Self
90    where
91        T: Ord,
92    {
93        fn extremum<T>(
94            a: Option<T>,
95            b: Option<T>,
96            f: fn(T, T) -> T,
97        ) -> Option<T> {
98            match (a, b) {
99                (None, None) => None,
100                (some, None) | (None, some) => some,
101                (Some(s), Some(o)) => Some(f(s, o)),
102            }
103        }
104        Self {
105            left: extremum(self.left, other.left, T::max),
106            top: extremum(self.top, other.top, T::max),
107            right: extremum(self.right, other.right, T::min),
108            bottom: extremum(self.bottom, other.bottom, T::min),
109        }
110    }
111}
112
113impl<H: RangeBounds<u32>, V: RangeBounds<u32>> From<(H, V)> for Rect<u32> {
114    /// Creates a `Rect` from two ranges specifying the horizontal and
115    /// vertical extents of the `Rect` respectively.
116    fn from((horiz, vert): (H, V)) -> Self {
117        let resolve = |b, i, e| match b {
118            Included(&x) => Some(x + i),
119            Excluded(&x) => Some(x + e),
120            Unbounded => None,
121        };
122        let left = resolve(horiz.start_bound(), 0, 1);
123        let top = resolve(vert.start_bound(), 0, 1);
124        let right = resolve(horiz.end_bound(), 1, 0);
125        let bottom = resolve(vert.end_bound(), 1, 0);
126
127        Self { left, top, right, bottom }
128    }
129}
130
131impl From<Range<Point2u>> for Rect<u32> {
132    /// Creates a `Rect` from two points designating the left-top
133    /// and right-bottom corners of the `Rect`.
134    fn from(r: Range<Point2u>) -> Self {
135        Self {
136            left: Some(r.start.x()),
137            top: Some(r.start.y()),
138            right: Some(r.end.x()),
139            bottom: Some(r.end.y()),
140        }
141    }
142}
143
144impl<T> From<RangeFull> for Rect<T> {
145    /// Creates a `Rect` with all sides unbounded.
146    fn from(_: RangeFull) -> Self {
147        Self {
148            left: None,
149            top: None,
150            right: None,
151            bottom: None,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::math::pt2;
159
160    use super::*;
161
162    fn rect<T>(
163        l: impl Into<Option<T>>,
164        t: impl Into<Option<T>>,
165        r: impl Into<Option<T>>,
166        b: impl Into<Option<T>>,
167    ) -> Rect<T> {
168        Rect {
169            left: l.into(),
170            top: t.into(),
171            right: r.into(),
172            bottom: b.into(),
173        }
174    }
175
176    #[test]
177    fn extents_bounded() {
178        let r = rect(10, 20, 100, 120);
179        assert_eq!(r.width(), Some(90));
180        assert_eq!(r.height(), Some(100));
181    }
182    #[test]
183    fn extents_unbounded() {
184        let r = rect(None, 20, 100, None);
185        assert_eq!(r.width(), None);
186        assert_eq!(r.height(), None);
187    }
188    #[test]
189    fn extents_empty() {
190        let r = rect(10, 20, -100, 20);
191        assert_eq!(r.width(), Some(0));
192        assert_eq!(r.height(), Some(0));
193    }
194    #[test]
195    fn contains() {
196        let r = rect(None, -10i32, 100, None);
197        assert!(r.contains(0, 0));
198        assert!(!r.contains(0, -20));
199        assert!(r.contains(-9999, 9999));
200        assert!(r.contains(99, 0));
201        assert!(!r.contains(100, 0));
202    }
203
204    #[test]
205    fn is_empty_bounded() {
206        assert!(rect(10, 10, 10, 20).is_empty());
207        assert!(rect(10, 20, 10, 10).is_empty());
208    }
209    #[test]
210    fn is_empty_negative_extent() {
211        assert!(rect(20, 10, 10, 20).is_empty());
212        assert!(rect(10, 20, 20, 10).is_empty());
213    }
214
215    #[test]
216    fn is_empty_unbounded() {
217        assert!(rect(10, 10, 10, None).is_empty());
218        assert!(rect(10, 10, None, 10).is_empty());
219        assert!(rect(10, None, 10, 10).is_empty());
220        assert!(rect(None, 10, 10, 10).is_empty());
221    }
222    #[test]
223    fn is_empty_bounded_not_empty() {
224        assert!(!rect(10, 10, 20, 20).is_empty());
225        assert!(!rect(10, -10, 20, 10).is_empty());
226    }
227
228    #[test]
229    fn is_empty_unbounded_not_empty() {
230        assert!(!rect(10, 10, 20, None).is_empty());
231        assert!(!rect(10, 10, None, 20).is_empty());
232        assert!(!rect(10, None, 20, 10).is_empty());
233        assert!(!rect(None, 10, 10, 20).is_empty());
234
235        assert!(!rect(10, None, 20, None).is_empty());
236        assert!(!rect(None, 10, 10, None).is_empty());
237        assert!(!rect::<i32>(None, None, None, None).is_empty());
238    }
239
240    #[test]
241    fn bounds() {
242        let r = rect(None, -10i32, 100, None);
243        let [h, v] = r.bounds();
244
245        assert_eq!(h.start_bound(), Unbounded);
246        assert_eq!(v.start_bound(), Included(&-10));
247        assert_eq!(h.end_bound(), Excluded(&100));
248        assert_eq!(v.end_bound(), Unbounded);
249    }
250
251    #[test]
252    fn intersect_two_bounded() {
253        let r = rect(10, 20, 100, 40);
254        let s = rect(30, 0, 60, 50);
255
256        assert_eq!(r.intersect(&s), rect(30, 20, 60, 40));
257    }
258    #[test]
259    fn intersect_bounded_and_unbounded() {
260        let r = rect(10, 20, 50, 40);
261        let s = rect(30, 0, None, 50);
262
263        assert_eq!(r.intersect(&s), rect(30, 20, 50, 40));
264    }
265    #[test]
266    fn intersect_two_unbounded_to_unbounded() {
267        let r = rect(0, 0, 10, None);
268        let s = rect(0, 10, 10, None);
269
270        assert_eq!(r.intersect(&s), rect(0, 10, 10, None));
271    }
272    #[test]
273    fn intersect_two_unbounded_to_bounded() {
274        let r = rect(None, 0, 30, 10);
275        let s = rect(10, 0, None, 10);
276
277        assert_eq!(r.intersect(&s), rect(10, 0, 30, 10));
278    }
279    #[test]
280    fn intersect_empty() {
281        let r = rect(0, 0, 20, 20);
282        let s = rect(10, 10, 10, 10);
283        assert!(r.intersect(&s).is_empty());
284    }
285    #[test]
286    fn intersect_full() {
287        let r = rect(0, 0, 10, 20);
288        let s = (..).into();
289        assert_eq!(r.intersect(&s), r);
290    }
291    #[test]
292    fn intersect_disjoint() {
293        let r = rect(None, -10i32, 100, None);
294        let s = rect(100, None, None, None);
295
296        let t = r.intersect(&s);
297        assert!(t.is_empty());
298        assert_eq!(t.width(), Some(0));
299        assert_eq!(t.height(), None);
300    }
301
302    #[test]
303    fn from_pair_of_ranges() {
304        assert_eq!(Rect::from((2..5, 4..8)), rect(2, 4, 5, 8));
305        assert_eq!(Rect::from((2.., 4..8)), rect(2, 4, None, 8));
306        assert_eq!(Rect::from((2..5, ..8)), rect(2, None, 5, 8));
307        assert_eq!(Rect::from((2..=5, 4..=8)), rect(2, 4, 6, 9));
308        assert_eq!(Rect::from((.., ..)), rect(None, None, None, None));
309    }
310
311    #[test]
312    fn from_range_full() {
313        assert_eq!(Rect::<()>::from(..), rect(None, None, None, None));
314    }
315
316    #[test]
317    fn from_range_of_vecs() {
318        assert_eq!(Rect::from(pt2(10, 20)..pt2(40, 80)), rect(10, 20, 40, 80));
319    }
320}