//! A 2D axis-aligned [`Rect`]angle, used for UI, viewports, and texture regions. use glam::Vec2; use serde::{Deserialize, Serialize}; /// An axis-aligned rectangle defined by its `min` (top-left in a y-down UI /// space, or bottom-left in y-up) and `max` corners. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Rect { /// Minimum corner (smallest x and y). pub min: Vec2, /// Maximum corner (largest x and y). pub max: Vec2, } impl Rect { /// A zero-area rectangle at the origin. pub const ZERO: Self = Self { min: Vec2::ZERO, max: Vec2::ZERO, }; /// Creates a rectangle from two corners, sorting so `min <= max`. #[inline] pub fn new(a: Vec2, b: Vec2) -> Self { Self { min: a.min(b), max: a.max(b), } } /// Creates a rectangle from a `min` corner and a size. #[inline] pub fn from_min_size(min: Vec2, size: Vec2) -> Self { Self { min, max: min + size, } } /// Creates a rectangle from a center point and full size. #[inline] pub fn from_center_size(center: Vec2, size: Vec2) -> Self { let half = size * 0.5; Self { min: center - half, max: center + half, } } /// The width and height as a vector. #[inline] pub fn size(&self) -> Vec2 { (self.max - self.min).max(Vec2::ZERO) } /// The width (x extent). #[inline] pub fn width(&self) -> f32 { self.size().x } /// The height (y extent). #[inline] pub fn height(&self) -> f32 { self.size().y } /// The center point. #[inline] pub fn center(&self) -> Vec2 { (self.min + self.max) * 0.5 } /// The area (`width * height`). #[inline] pub fn area(&self) -> f32 { let s = self.size(); s.x * s.y } /// Returns `true` if the rectangle has zero (or inverted) area. #[inline] pub fn is_empty(&self) -> bool { self.min.x >= self.max.x || self.min.y >= self.max.y } /// Returns `true` if `point` is inside or on the boundary. #[inline] pub fn contains_point(&self, point: Vec2) -> bool { point.cmpge(self.min).all() && point.cmple(self.max).all() } /// Returns `true` if the two rectangles overlap (touching counts). #[inline] pub fn intersects(&self, other: &Rect) -> bool { self.min.cmple(other.max).all() && self.max.cmpge(other.min).all() } /// Returns the overlapping region, or [`Rect::ZERO`] if disjoint. #[inline] pub fn intersection(&self, other: &Rect) -> Rect { let min = self.min.max(other.min); let max = self.max.min(other.max); if min.x > max.x || min.y > max.y { Rect::ZERO } else { Rect { min, max } } } /// Returns the smallest rectangle containing both. #[inline] pub fn union(&self, other: &Rect) -> Rect { Rect { min: self.min.min(other.min), max: self.max.max(other.max), } } /// Returns the point inside the rectangle closest to `point`. #[inline] pub fn closest_point(&self, point: Vec2) -> Vec2 { point.clamp(self.min, self.max) } /// Returns a copy expanded outward by `amount` on every side (negative /// shrinks). #[inline] pub fn expanded(&self, amount: f32) -> Rect { Rect { min: self.min - Vec2::splat(amount), max: self.max + Vec2::splat(amount), } } } #[cfg(test)] mod tests { use super::*; #[test] fn new_sorts_corners() { let r = Rect::new(Vec2::new(4.0, 1.0), Vec2::new(0.0, 5.0)); assert_eq!(r.min, Vec2::new(0.0, 1.0)); assert_eq!(r.max, Vec2::new(4.0, 5.0)); } #[test] fn min_size_and_center_size() { let r = Rect::from_min_size(Vec2::new(1.0, 2.0), Vec2::new(4.0, 6.0)); assert_eq!(r.size(), Vec2::new(4.0, 6.0)); assert_eq!(r.center(), Vec2::new(3.0, 5.0)); let c = Rect::from_center_size(Vec2::ZERO, Vec2::new(2.0, 2.0)); assert_eq!(c.min, Vec2::new(-1.0, -1.0)); assert_eq!(c.max, Vec2::new(1.0, 1.0)); } #[test] fn dimensions_and_area() { let r = Rect::from_min_size(Vec2::ZERO, Vec2::new(3.0, 4.0)); assert_eq!(r.width(), 3.0); assert_eq!(r.height(), 4.0); assert_eq!(r.area(), 12.0); } #[test] fn empty_detection() { assert!(Rect::ZERO.is_empty()); assert!(Rect::new(Vec2::ZERO, Vec2::new(0.0, 5.0)).is_empty()); assert!(!Rect::from_min_size(Vec2::ZERO, Vec2::ONE).is_empty()); } #[test] fn contains_and_closest() { let r = Rect::from_min_size(Vec2::ZERO, Vec2::splat(2.0)); assert!(r.contains_point(Vec2::ONE)); assert!(!r.contains_point(Vec2::new(3.0, 1.0))); assert_eq!(r.closest_point(Vec2::new(5.0, -1.0)), Vec2::new(2.0, 0.0)); } #[test] fn intersection_and_union() { let a = Rect::from_min_size(Vec2::ZERO, Vec2::splat(2.0)); let b = Rect::from_min_size(Vec2::ONE, Vec2::splat(2.0)); assert!(a.intersects(&b)); assert_eq!(a.intersection(&b), Rect::new(Vec2::ONE, Vec2::splat(2.0))); assert_eq!(a.union(&b), Rect::new(Vec2::ZERO, Vec2::splat(3.0))); let c = Rect::from_min_size(Vec2::splat(10.0), Vec2::ONE); assert!(!a.intersects(&c)); assert_eq!(a.intersection(&c), Rect::ZERO); } #[test] fn expanded_grows_and_shrinks() { let r = Rect::from_min_size(Vec2::ZERO, Vec2::splat(4.0)); assert_eq!( r.expanded(1.0), Rect::new(Vec2::splat(-1.0), Vec2::splat(5.0)) ); assert_eq!(r.expanded(-1.0), Rect::new(Vec2::ONE, Vec2::splat(3.0))); } }