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>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
//! 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)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user