Skip to main content

stipple_geometry/
rect.rs

1use crate::{Insets, Point, Size, Vec2};
2
3/// An axis-aligned rectangle in logical-pixel space, defined by its top-left
4/// `origin` and its `size`.
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
6pub struct Rect {
7    pub origin: Point,
8    pub size: Size,
9}
10
11impl Rect {
12    pub const ZERO: Self = Self {
13        origin: Point::ORIGIN,
14        size: Size::ZERO,
15    };
16
17    #[inline]
18    pub const fn new(origin: Point, size: Size) -> Self {
19        Self { origin, size }
20    }
21
22    /// Construct from individual coordinates.
23    #[inline]
24    pub const fn from_xywh(x: f64, y: f64, width: f64, height: f64) -> Self {
25        Self {
26            origin: Point::new(x, y),
27            size: Size::new(width, height),
28        }
29    }
30
31    /// Construct from two corner points (in any order).
32    #[inline]
33    pub fn from_points(a: Point, b: Point) -> Self {
34        let x = a.x.min(b.x);
35        let y = a.y.min(b.y);
36        Self::from_xywh(x, y, (a.x - b.x).abs(), (a.y - b.y).abs())
37    }
38
39    #[inline]
40    pub fn min_x(self) -> f64 {
41        self.origin.x
42    }
43    #[inline]
44    pub fn min_y(self) -> f64 {
45        self.origin.y
46    }
47    #[inline]
48    pub fn max_x(self) -> f64 {
49        self.origin.x + self.size.width
50    }
51    #[inline]
52    pub fn max_y(self) -> f64 {
53        self.origin.y + self.size.height
54    }
55
56    #[inline]
57    pub fn width(self) -> f64 {
58        self.size.width
59    }
60    #[inline]
61    pub fn height(self) -> f64 {
62        self.size.height
63    }
64
65    #[inline]
66    pub fn center(self) -> Point {
67        Point::new(
68            (self.min_x() + self.max_x()) * 0.5,
69            (self.min_y() + self.max_y()) * 0.5,
70        )
71    }
72
73    #[inline]
74    pub fn is_empty(self) -> bool {
75        self.size.is_empty()
76    }
77
78    /// True if `p` lies within the rectangle (left/top inclusive,
79    /// right/bottom exclusive — the standard half-open convention for
80    /// pixel coverage and hit-testing).
81    #[inline]
82    pub fn contains(self, p: Point) -> bool {
83        p.x >= self.min_x() && p.x < self.max_x() && p.y >= self.min_y() && p.y < self.max_y()
84    }
85
86    /// Translate by a displacement.
87    #[inline]
88    pub fn translate(self, by: Vec2) -> Rect {
89        Rect::new(self.origin + by, self.size)
90    }
91
92    /// Shrink inward by `insets` on each edge, clamping to a non-negative size.
93    #[inline]
94    pub fn inset(self, insets: Insets) -> Rect {
95        Rect::new(
96            Point::new(self.min_x() + insets.left, self.min_y() + insets.top),
97            self.size.deflate(insets),
98        )
99    }
100
101    /// The overlapping region of two rectangles, or `None` if disjoint.
102    pub fn intersection(self, other: Rect) -> Option<Rect> {
103        let x0 = self.min_x().max(other.min_x());
104        let y0 = self.min_y().max(other.min_y());
105        let x1 = self.max_x().min(other.max_x());
106        let y1 = self.max_y().min(other.max_y());
107        if x1 > x0 && y1 > y0 {
108            Some(Rect::from_xywh(x0, y0, x1 - x0, y1 - y0))
109        } else {
110            None
111        }
112    }
113
114    /// The smallest rectangle containing both inputs.
115    pub fn union(self, other: Rect) -> Rect {
116        if self.is_empty() {
117            return other;
118        }
119        if other.is_empty() {
120            return self;
121        }
122        let x0 = self.min_x().min(other.min_x());
123        let y0 = self.min_y().min(other.min_y());
124        let x1 = self.max_x().max(other.max_x());
125        let y1 = self.max_y().max(other.max_y());
126        Rect::from_xywh(x0, y0, x1 - x0, y1 - y0)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn contains_is_half_open() {
136        let r = Rect::from_xywh(0.0, 0.0, 10.0, 10.0);
137        assert!(r.contains(Point::new(0.0, 0.0)));
138        assert!(r.contains(Point::new(9.999, 9.999)));
139        assert!(!r.contains(Point::new(10.0, 5.0)));
140    }
141
142    #[test]
143    fn intersection_and_union() {
144        let a = Rect::from_xywh(0.0, 0.0, 10.0, 10.0);
145        let b = Rect::from_xywh(5.0, 5.0, 10.0, 10.0);
146        assert_eq!(a.intersection(b), Some(Rect::from_xywh(5.0, 5.0, 5.0, 5.0)));
147        assert_eq!(a.union(b), Rect::from_xywh(0.0, 0.0, 15.0, 15.0));
148
149        let c = Rect::from_xywh(100.0, 100.0, 1.0, 1.0);
150        assert_eq!(a.intersection(c), None);
151    }
152
153    #[test]
154    fn inset_shrinks() {
155        let r = Rect::from_xywh(0.0, 0.0, 20.0, 20.0).inset(Insets::uniform(5.0));
156        assert_eq!(r, Rect::from_xywh(5.0, 5.0, 10.0, 10.0));
157    }
158}