Skip to main content

valo_geometry/
rect.rs

1use crate::{Point, Size};
2
3/// `Rect` is an axis-aligned rectangle in Valo's y-down coordinate system.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Rect {
7    /// `x` is the left edge.
8    pub x: f32,
9    /// `y` is the top edge.
10    pub y: f32,
11    /// `width` is the horizontal extent.
12    pub width: f32,
13    /// `height` is the vertical extent.
14    pub height: f32,
15}
16
17impl Rect {
18    /// `new` creates a rectangle from its top-left origin and size.
19    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
20        Self {
21            x,
22            y,
23            width,
24            height,
25        }
26    }
27
28    /// `from_ltrb` creates a rectangle from its left, top, right, and bottom edges.
29    pub fn from_ltrb(l: f32, t: f32, r: f32, b: f32) -> Self {
30        Self {
31            x: l,
32            y: t,
33            width: r - l,
34            height: b - t,
35        }
36    }
37
38    /// `from_origin_size` creates a rectangle from an origin and size.
39    pub fn from_origin_size(origin: Point, size: Size) -> Self {
40        Self {
41            x: origin.x,
42            y: origin.y,
43            width: size.width,
44            height: size.height,
45        }
46    }
47
48    /// `right` returns the x-coordinate of the right edge.
49    pub fn right(&self) -> f32 {
50        self.x + self.width
51    }
52
53    /// `bottom` returns the y-coordinate of the bottom edge.
54    pub fn bottom(&self) -> f32 {
55        self.y + self.height
56    }
57
58    /// `origin` returns the top-left point.
59    pub fn origin(&self) -> Point {
60        Point::new(self.x, self.y)
61    }
62
63    /// `size` returns the rectangle's width and height.
64    pub fn size(&self) -> Size {
65        Size::new(self.width, self.height)
66    }
67
68    /// `is_empty` reports whether either extent is nonpositive.
69    pub fn is_empty(&self) -> bool {
70        self.width <= 0.0 || self.height <= 0.0
71    }
72
73    /// `union` returns the smallest rectangle containing both rectangles.
74    ///
75    /// Empty rectangles act as the identity.
76    pub fn union(&self, other: &Rect) -> Rect {
77        if self.is_empty() {
78            return *other;
79        }
80        if other.is_empty() {
81            return *self;
82        }
83        Rect::from_ltrb(
84            self.x.min(other.x),
85            self.y.min(other.y),
86            self.right().max(other.right()),
87            self.bottom().max(other.bottom()),
88        )
89    }
90
91    /// `intersect` returns the overlapping area or `None` when there is none.
92    pub fn intersect(&self, other: &Rect) -> Option<Rect> {
93        let r = Rect::from_ltrb(
94            self.x.max(other.x),
95            self.y.max(other.y),
96            self.right().min(other.right()),
97            self.bottom().min(other.bottom()),
98        );
99        (!r.is_empty()).then_some(r)
100    }
101
102    /// `intersects` reports whether the rectangles overlap with positive area.
103    pub fn intersects(&self, other: &Rect) -> bool {
104        !self.is_empty()
105            && !other.is_empty()
106            && self.x < other.right()
107            && other.x < self.right()
108            && self.y < other.bottom()
109            && other.y < self.bottom()
110    }
111
112    /// `contains` reports whether a point lies within the half-open rectangle.
113    ///
114    /// Left and top edges are included; right and bottom edges are excluded.
115    pub fn contains(&self, p: Point) -> bool {
116        p.x >= self.x && p.x < self.right() && p.y >= self.y && p.y < self.bottom()
117    }
118
119    /// `contains_inclusive` reports whether a point lies within or on every edge.
120    pub fn contains_inclusive(&self, p: Point) -> bool {
121        p.x >= self.x && p.x <= self.right() && p.y >= self.y && p.y <= self.bottom()
122    }
123
124    /// `expand` moves every edge outward by `d`.
125    ///
126    /// Negative values contract the rectangle.
127    pub fn expand(&self, d: f32) -> Rect {
128        Rect::new(
129            self.x - d,
130            self.y - d,
131            self.width + 2.0 * d,
132            self.height + 2.0 * d,
133        )
134    }
135
136    /// `EVERYTHING` is the conservative bound used when content cannot be bounded.
137    pub const EVERYTHING: Rect = Rect {
138        x: -1.0e9,
139        y: -1.0e9,
140        width: 2.0e9,
141        height: 2.0e9,
142    };
143
144    /// `corners` returns corners clockwise from the top-left.
145    pub fn corners(&self) -> [Point; 4] {
146        [
147            Point::new(self.x, self.y),
148            Point::new(self.right(), self.y),
149            Point::new(self.right(), self.bottom()),
150            Point::new(self.x, self.bottom()),
151        ]
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn union_ignores_empty() {
161        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
162        assert_eq!(Rect::default().union(&a), a);
163        assert_eq!(a.union(&Rect::default()), a);
164    }
165
166    #[test]
167    fn intersect_disjoint_is_none() {
168        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
169        let b = Rect::new(20.0, 0.0, 10.0, 10.0);
170        assert_eq!(a.intersect(&b), None);
171        assert!(!a.intersects(&b));
172    }
173
174    #[test]
175    fn intersect_overlap() {
176        let a = Rect::new(0.0, 0.0, 10.0, 10.0);
177        let b = Rect::new(5.0, 5.0, 10.0, 10.0);
178        assert_eq!(a.intersect(&b), Some(Rect::new(5.0, 5.0, 5.0, 5.0)));
179    }
180}