Skip to main content

game_gem/math/
rect.rs

1//! Axis-aligned rectangle with game-oriented operations.
2
3use super::Vec2;
4
5/// An axis-aligned rectangle defined by its top-left corner, width, and height.
6///
7/// Uses the screen-space convention: Y increases downward.
8#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
9#[repr(C)]
10pub struct Rect {
11    /// X coordinate of the top-left corner.
12    pub x: f32,
13    /// Y coordinate of the top-left corner.
14    pub y: f32,
15    /// Width of the rectangle.
16    pub w: f32,
17    /// Height of the rectangle.
18    pub h: f32,
19}
20
21impl Rect {
22    /// Create a new rectangle from position and size.
23    #[inline]
24    pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
25        Self { x, y, w, h }
26    }
27
28    /// Create a rectangle from min and max corner points.
29    #[inline]
30    pub const fn from_min_max(min: Vec2, max: Vec2) -> Self {
31        Self {
32            x: min.x,
33            y: min.y,
34            w: max.x - min.x,
35            h: max.y - min.y,
36        }
37    }
38
39    /// Create a rectangle centered at `center` with the given size.
40    #[inline]
41    pub const fn centered(center: Vec2, size: Vec2) -> Self {
42        Self {
43            x: center.x - size.x * 0.5,
44            y: center.y - size.y * 0.5,
45            w: size.x,
46            h: size.y,
47        }
48    }
49
50    /// Top-left corner position.
51    #[inline]
52    pub fn pos(self) -> Vec2 {
53        Vec2::new(self.x, self.y)
54    }
55
56    /// Size of the rectangle.
57    #[inline]
58    pub fn size(self) -> Vec2 {
59        Vec2::new(self.w, self.h)
60    }
61
62    /// Center point of the rectangle.
63    #[inline]
64    pub fn center(self) -> Vec2 {
65        Vec2::new(self.x + self.w * 0.5, self.y + self.h * 0.5)
66    }
67
68    /// Bottom-right corner (exclusive).
69    #[inline]
70    pub fn max(self) -> Vec2 {
71        Vec2::new(self.x + self.w, self.y + self.h)
72    }
73
74    /// Top-right corner.
75    #[inline]
76    pub fn top_right(self) -> Vec2 {
77        Vec2::new(self.x + self.w, self.y)
78    }
79
80    /// Bottom-left corner.
81    #[inline]
82    pub fn bottom_left(self) -> Vec2 {
83        Vec2::new(self.x, self.y + self.h)
84    }
85
86    /// Check if the rectangle contains a point.
87    #[inline]
88    pub fn contains(self, point: Vec2) -> bool {
89        point.x >= self.x
90            && point.x <= self.x + self.w
91            && point.y >= self.y
92            && point.y <= self.y + self.h
93    }
94
95    /// Check if this rectangle fully contains another.
96    #[inline]
97    pub fn contains_rect(self, other: Rect) -> bool {
98        let self_max = self.max();
99        let other_max = other.max();
100        other.x >= self.x
101            && other.y >= self.y
102            && other_max.x <= self_max.x
103            && other_max.y <= self_max.y
104    }
105
106    /// Check if two rectangles overlap (AABB intersection test).
107    #[inline]
108    pub fn overlaps(self, other: Rect) -> bool {
109        self.x < other.x + other.w
110            && self.x + self.w > other.x
111            && self.y < other.y + other.h
112            && self.y + self.h > other.y
113    }
114
115    /// Compute the intersection rectangle, or `None` if they don't overlap.
116    #[inline]
117    pub fn intersection(self, other: Rect) -> Option<Rect> {
118        if !self.overlaps(other) {
119            return None;
120        }
121        let x1 = self.x.max(other.x);
122        let y1 = self.y.max(other.y);
123        let x2 = (self.x + self.w).min(other.x + other.w);
124        let y2 = (self.y + self.h).min(other.y + other.h);
125        Some(Rect::new(x1, y1, x2 - x1, y2 - y1))
126    }
127
128    /// Compute the union rectangle that covers both.
129    #[inline]
130    pub fn union(self, other: Rect) -> Rect {
131        let x1 = self.x.min(other.x);
132        let y1 = self.y.min(other.y);
133        let x2 = (self.x + self.w).max(other.x + other.w);
134        let y2 = (self.y + self.h).max(other.y + other.h);
135        Rect::new(x1, y1, x2 - x1, y2 - y1)
136    }
137
138    /// Inflate the rectangle by `amount` on all sides.
139    #[inline]
140    pub fn inflated(self, amount: f32) -> Rect {
141        Rect::new(
142            self.x - amount,
143            self.y - amount,
144            self.w + amount * 2.0,
145            self.h + amount * 2.0,
146        )
147    }
148
149    /// Shrink the rectangle by `amount` on all sides.
150    #[inline]
151    pub fn deflated(self, amount: f32) -> Rect {
152        self.inflated(-amount)
153    }
154
155    /// Scale the rectangle around its center.
156    #[inline]
157    pub fn scaled(self, scale: Vec2) -> Rect {
158        let new_size = self.size() * scale;
159        let offset = (new_size - self.size()) * 0.5;
160        Rect::new(self.x - offset.x, self.y - offset.y, new_size.x, new_size.y)
161    }
162
163    /// Move the rectangle so its center is at `center`.
164    #[inline]
165    pub fn with_center(self, center: Vec2) -> Rect {
166        Rect::centered(center, self.size())
167    }
168
169    /// Translate the rectangle by an offset.
170    #[inline]
171    pub fn translated(self, offset: Vec2) -> Rect {
172        Rect::new(self.x + offset.x, self.y + offset.y, self.w, self.h)
173    }
174
175    /// Closest point inside the rectangle to the given `point`.
176    #[inline]
177    pub fn closest_point(self, point: Vec2) -> Vec2 {
178        Vec2::new(
179            point.x.clamp(self.x, self.x + self.w),
180            point.y.clamp(self.y, self.y + self.h),
181        )
182    }
183
184    /// Area of the rectangle.
185    #[inline]
186    pub fn area(self) -> f32 {
187        self.w * self.h
188    }
189
190    /// Perimeter of the rectangle.
191    #[inline]
192    pub fn perimeter(self) -> f32 {
193        2.0 * (self.w + self.h)
194    }
195
196    /// Aspect ratio (width / height).
197    #[inline]
198    pub fn aspect_ratio(self) -> f32 {
199        self.w / self.h
200    }
201}
202
203impl std::fmt::Display for Rect {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        write!(f, "Rect({}, {}, {}, {})", self.x, self.y, self.w, self.h)
206    }
207}
208
209#[cfg(feature = "serde")]
210impl serde::Serialize for Rect {
211    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
212    where
213        S: serde::Serializer,
214    {
215        use serde::ser::SerializeStruct;
216        let mut s = serializer.serialize_struct("Rect", 4)?;
217        s.serialize_field("x", &self.x)?;
218        s.serialize_field("y", &self.y)?;
219        s.serialize_field("w", &self.w)?;
220        s.serialize_field("h", &self.h)?;
221        s.end()
222    }
223}
224
225#[cfg(feature = "serde")]
226impl<'de> serde::Deserialize<'de> for Rect {
227    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
228    where
229        D: serde::Deserializer<'de>,
230    {
231        #[derive(serde::Deserialize)]
232        struct RectHelper { x: f32, y: f32, w: f32, h: f32 }
233        let h = RectHelper::deserialize(deserializer)?;
234        Ok(Rect::new(h.x, h.y, h.w, h.h))
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_contains_point() {
244        let r = Rect::new(10.0, 10.0, 100.0, 50.0);
245        assert!(r.contains(Vec2::new(50.0, 30.0)));
246        assert!(r.contains(Vec2::new(10.0, 10.0)));
247        assert!(r.contains(Vec2::new(110.0, 60.0)));
248        assert!(!r.contains(Vec2::new(5.0, 30.0)));
249        assert!(!r.contains(Vec2::new(50.0, 65.0)));
250    }
251
252    #[test]
253    fn test_overlaps() {
254        let a = Rect::new(0.0, 0.0, 100.0, 100.0);
255        let b = Rect::new(50.0, 50.0, 100.0, 100.0);
256        let c = Rect::new(200.0, 200.0, 50.0, 50.0);
257        assert!(a.overlaps(b));
258        assert!(!a.overlaps(c));
259    }
260
261    #[test]
262    fn test_intersection() {
263        let a = Rect::new(0.0, 0.0, 100.0, 100.0);
264        let b = Rect::new(50.0, 50.0, 100.0, 100.0);
265        let i = a.intersection(b).unwrap();
266        assert_eq!(i, Rect::new(50.0, 50.0, 50.0, 50.0));
267    }
268}