Files
Oxide/engine/src/math/rect.rs
T
Homer Simpson f56a1eea3b Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

206 lines
5.8 KiB
Rust

//! 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)));
}
}