Skip to main content

wisp/math/
rect.rs

1//! Axis-aligned rectangle.
2
3use glam::Vec2;
4
5/// Axis-aligned rectangle defined by minimum corner and size.
6///
7/// Coordinate system: top-left origin, `+Y` down (Pixi / screen convention).
8/// [`Rect::contains`] is half-open: `[min, max)`.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct Rect {
11    /// Top-left corner.
12    pub min: Vec2,
13    /// `(width, height)`. Components should be non-negative.
14    pub size: Vec2,
15}
16
17impl Rect {
18    /// Construct from `(x, y, width, height)`.
19    #[must_use]
20    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
21        Self {
22            min: Vec2::new(x, y),
23            size: Vec2::new(width, height),
24        }
25    }
26
27    /// Construct from `min` and `max` corners. Size = `max - min`.
28    #[must_use]
29    pub fn from_min_max(min: Vec2, max: Vec2) -> Self {
30        Self {
31            min,
32            size: max - min,
33        }
34    }
35
36    /// Maximum corner: `min + size`.
37    #[must_use]
38    pub fn max(&self) -> Vec2 {
39        self.min + self.size
40    }
41
42    /// Geometric center.
43    #[must_use]
44    pub fn center(&self) -> Vec2 {
45        self.min + self.size * 0.5
46    }
47
48    /// Width.
49    #[must_use]
50    pub fn width(&self) -> f32 {
51        self.size.x
52    }
53
54    /// Height.
55    #[must_use]
56    pub fn height(&self) -> f32 {
57        self.size.y
58    }
59
60    /// Whether the rect contains a point (half-open: `[min, max)`).
61    #[must_use]
62    pub fn contains(&self, p: Vec2) -> bool {
63        let max = self.max();
64        p.x >= self.min.x && p.y >= self.min.y && p.x < max.x && p.y < max.y
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn new_constructs_min_and_size() {
74        let r = Rect::new(10.0, 20.0, 30.0, 40.0);
75        assert_eq!(r.min, Vec2::new(10.0, 20.0));
76        assert_eq!(r.size, Vec2::new(30.0, 40.0));
77    }
78
79    #[test]
80    fn max_is_min_plus_size() {
81        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
82        assert_eq!(r.max(), Vec2::new(100.0, 50.0));
83    }
84
85    #[test]
86    fn from_min_max_round_trips() {
87        let min = Vec2::new(5.0, 10.0);
88        let max = Vec2::new(25.0, 40.0);
89        let r = Rect::from_min_max(min, max);
90        assert_eq!(r.min, min);
91        assert_eq!(r.max(), max);
92    }
93
94    #[test]
95    fn center_is_midpoint() {
96        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
97        assert_eq!(r.center(), Vec2::new(50.0, 50.0));
98    }
99
100    #[test]
101    fn contains_inside_point() {
102        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
103        assert!(r.contains(Vec2::new(50.0, 50.0)));
104    }
105
106    #[test]
107    fn contains_includes_min_edge() {
108        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
109        assert!(r.contains(Vec2::new(0.0, 0.0)));
110    }
111
112    #[test]
113    fn contains_excludes_max_edge() {
114        let r = Rect::new(0.0, 0.0, 100.0, 100.0);
115        assert!(!r.contains(Vec2::new(100.0, 50.0)));
116        assert!(!r.contains(Vec2::new(50.0, 100.0)));
117    }
118
119    #[test]
120    fn width_and_height_match_size() {
121        let r = Rect::new(0.0, 0.0, 100.0, 50.0);
122        assert!((r.width() - 100.0).abs() < f32::EPSILON);
123        assert!((r.height() - 50.0).abs() < f32::EPSILON);
124    }
125}