use glam::{Vec2, Vec3};
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
pub min: Vec2,
pub max: Vec2,
}
impl Rect {
pub const ZERO: Self = Self {
min: Vec2::ZERO,
max: Vec2::ZERO,
};
pub fn new(min: Vec2, max: Vec2) -> Self {
Self {
min: min.min(max),
max: min.max(max),
}
}
pub fn from_center_size(center: Vec2, size: Vec2) -> Self {
let half_size = size * 0.5;
Self::new(center - half_size, center + half_size)
}
pub fn from_position_size(position: Vec2, size: Vec2) -> Self {
Self::new(position, position + size)
}
pub fn width(&self) -> f32 {
self.max.x - self.min.x
}
pub fn height(&self) -> f32 {
self.max.y - self.min.y
}
pub fn size(&self) -> Vec2 {
self.max - self.min
}
pub fn center(&self) -> Vec2 {
(self.min + self.max) * 0.5
}
pub fn area(&self) -> f32 {
let size = self.size();
size.x * size.y
}
pub fn perimeter(&self) -> f32 {
let size = self.size();
2.0 * (size.x + size.y)
}
pub fn contains(&self, point: Vec2) -> bool {
point.x >= self.min.x && point.x <= self.max.x &&
point.y >= self.min.y && point.y <= self.max.y
}
pub fn intersects(&self, other: &Rect) -> bool {
self.min.x <= other.max.x && self.max.x >= other.min.x &&
self.min.y <= other.max.y && self.max.y >= other.min.y
}
pub fn intersects_circle(&self, circle: &Circle) -> bool {
let closest_point = Vec2::new(
circle.center.x.clamp(self.min.x, self.max.x),
circle.center.y.clamp(self.min.y, self.max.y),
);
let distance_squared = (circle.center - closest_point).length_squared();
distance_squared <= circle.radius * circle.radius
}
pub fn intersection(&self, other: &Rect) -> Option<Rect> {
if !self.intersects(other) {
return None;
}
Some(Rect::new(
self.min.max(other.min),
self.max.min(other.max),
))
}
pub fn union(&self, other: &Rect) -> Rect {
Rect::new(
self.min.min(other.min),
self.max.max(other.max),
)
}
pub fn expand_to_include(&mut self, point: Vec2) {
self.min = self.min.min(point);
self.max = self.max.max(point);
}
pub fn expand(&self, amount: f32) -> Rect {
let expansion = Vec2::splat(amount);
Rect::new(self.min - expansion, self.max + expansion)
}
pub fn is_valid(&self) -> bool {
self.min.x <= self.max.x && self.min.y <= self.max.y
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Circle {
pub center: Vec2,
pub radius: f32,
}
impl Circle {
pub fn new(center: Vec2, radius: f32) -> Self {
Self {
center,
radius: radius.max(0.0), }
}
pub fn area(&self) -> f32 {
std::f32::consts::PI * self.radius * self.radius
}
pub fn circumference(&self) -> f32 {
2.0 * std::f32::consts::PI * self.radius
}
pub fn contains(&self, point: Vec2) -> bool {
(point - self.center).length_squared() <= self.radius * self.radius
}
pub fn intersects(&self, other: &Circle) -> bool {
let distance_squared = (self.center - other.center).length_squared();
let radius_sum = self.radius + other.radius;
distance_squared <= radius_sum * radius_sum
}
pub fn intersects_rect(&self, rect: &Rect) -> bool {
rect.intersects_circle(self)
}
pub fn bounding_rect(&self) -> Rect {
let radius_vec = Vec2::splat(self.radius);
Rect::new(self.center - radius_vec, self.center + radius_vec)
}
}
pub type Bounds2D = Rect;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bounds3D {
pub min: Vec3,
pub max: Vec3,
}
impl Bounds3D {
pub const ZERO: Self = Self {
min: Vec3::ZERO,
max: Vec3::ZERO,
};
pub fn new(min: Vec3, max: Vec3) -> Self {
Self {
min: min.min(max),
max: min.max(max),
}
}
pub fn from_center_size(center: Vec3, size: Vec3) -> Self {
let half_size = size * 0.5;
Self::new(center - half_size, center + half_size)
}
pub fn size(&self) -> Vec3 {
self.max - self.min
}
pub fn center(&self) -> Vec3 {
(self.min + self.max) * 0.5
}
pub fn volume(&self) -> f32 {
let size = self.size();
size.x * size.y * size.z
}
pub fn contains(&self, point: Vec3) -> bool {
point.x >= self.min.x && point.x <= self.max.x &&
point.y >= self.min.y && point.y <= self.max.y &&
point.z >= self.min.z && point.z <= self.max.z
}
pub fn intersects(&self, other: &Bounds3D) -> bool {
self.min.x <= other.max.x && self.max.x >= other.min.x &&
self.min.y <= other.max.y && self.max.y >= other.min.y &&
self.min.z <= other.max.z && self.max.z >= other.min.z
}
pub fn expand_to_include(&mut self, point: Vec3) {
self.min = self.min.min(point);
self.max = self.max.max(point);
}
pub fn intersection(&self, other: &Bounds3D) -> Option<Bounds3D> {
if !self.intersects(other) {
return None;
}
Some(Bounds3D::new(
self.min.max(other.min),
self.max.min(other.max),
))
}
pub fn union(&self, other: &Bounds3D) -> Bounds3D {
Bounds3D::new(
self.min.min(other.min),
self.max.max(other.max),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_rect_creation() {
let rect = Rect::new(Vec2::ZERO, Vec2::new(10.0, 20.0));
assert_eq!(rect.width(), 10.0);
assert_eq!(rect.height(), 20.0);
assert_eq!(rect.area(), 200.0);
}
#[test]
fn test_rect_from_center_size() {
let rect = Rect::from_center_size(Vec2::new(5.0, 10.0), Vec2::new(10.0, 20.0));
assert_eq!(rect.center(), Vec2::new(5.0, 10.0));
assert_eq!(rect.size(), Vec2::new(10.0, 20.0));
}
#[test]
fn test_rect_contains() {
let rect = Rect::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
assert!(rect.contains(Vec2::new(5.0, 5.0)));
assert!(rect.contains(Vec2::ZERO)); assert!(rect.contains(Vec2::new(10.0, 10.0))); assert!(!rect.contains(Vec2::new(15.0, 5.0)));
}
#[test]
fn test_rect_intersection() {
let rect1 = Rect::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
let rect2 = Rect::new(Vec2::new(5.0, 5.0), Vec2::new(15.0, 15.0));
assert!(rect1.intersects(&rect2));
let intersection = rect1.intersection(&rect2).unwrap();
assert_eq!(intersection.min, Vec2::new(5.0, 5.0));
assert_eq!(intersection.max, Vec2::new(10.0, 10.0));
}
#[test]
fn test_rect_union() {
let rect1 = Rect::new(Vec2::ZERO, Vec2::new(5.0, 5.0));
let rect2 = Rect::new(Vec2::new(3.0, 3.0), Vec2::new(8.0, 8.0));
let union = rect1.union(&rect2);
assert_eq!(union.min, Vec2::ZERO);
assert_eq!(union.max, Vec2::new(8.0, 8.0));
}
#[test]
fn test_circle_creation() {
let circle = Circle::new(Vec2::new(5.0, 5.0), 3.0);
assert_eq!(circle.center, Vec2::new(5.0, 5.0));
assert_eq!(circle.radius, 3.0);
assert_relative_eq!(circle.area(), std::f32::consts::PI * 9.0, epsilon = 1e-6);
}
#[test]
fn test_circle_contains() {
let circle = Circle::new(Vec2::ZERO, 5.0);
assert!(circle.contains(Vec2::new(3.0, 4.0))); assert!(!circle.contains(Vec2::new(4.0, 4.0))); }
#[test]
fn test_circle_intersection() {
let circle1 = Circle::new(Vec2::ZERO, 5.0);
let circle2 = Circle::new(Vec2::new(8.0, 0.0), 5.0);
assert!(circle1.intersects(&circle2));
let circle3 = Circle::new(Vec2::new(12.0, 0.0), 5.0);
assert!(!circle1.intersects(&circle3)); }
#[test]
fn test_rect_circle_intersection() {
let rect = Rect::new(Vec2::ZERO, Vec2::new(10.0, 10.0));
let circle = Circle::new(Vec2::new(5.0, 5.0), 3.0);
assert!(rect.intersects_circle(&circle));
assert!(circle.intersects_rect(&rect));
}
#[test]
fn test_bounds3d() {
let bounds = Bounds3D::from_center_size(Vec3::ZERO, Vec3::ONE);
assert_eq!(bounds.center(), Vec3::ZERO);
assert_eq!(bounds.volume(), 1.0);
assert!(bounds.contains(Vec3::new(0.4, 0.4, 0.4)));
assert!(!bounds.contains(Vec3::new(0.6, 0.6, 0.6)));
}
#[test]
fn test_rect_expand() {
let rect = Rect::new(Vec2::new(2.0, 2.0), Vec2::new(8.0, 8.0));
let expanded = rect.expand(1.0);
assert_eq!(expanded.min, Vec2::new(1.0, 1.0));
assert_eq!(expanded.max, Vec2::new(9.0, 9.0));
}
#[test]
fn test_rect_auto_correct() {
let rect = Rect::new(Vec2::new(10.0, 10.0), Vec2::ZERO);
assert_eq!(rect.min, Vec2::ZERO);
assert_eq!(rect.max, Vec2::new(10.0, 10.0));
}
#[test]
fn test_rect_zero() {
let rect = Rect::ZERO;
assert_eq!(rect.width(), 0.0);
assert_eq!(rect.height(), 0.0);
assert_eq!(rect.area(), 0.0);
assert!(rect.contains(Vec2::ZERO));
}
#[test]
fn test_rect_from_position_size() {
let rect = Rect::from_position_size(Vec2::new(2.0, 3.0), Vec2::new(4.0, 5.0));
assert_eq!(rect.min, Vec2::new(2.0, 3.0));
assert_eq!(rect.max, Vec2::new(6.0, 8.0));
assert_eq!(rect.width(), 4.0);
assert_eq!(rect.height(), 5.0);
}
#[test]
fn test_rect_perimeter() {
let rect = Rect::new(Vec2::ZERO, Vec2::new(3.0, 4.0));
assert_eq!(rect.perimeter(), 14.0);
}
#[test]
fn test_rect_no_intersection() {
let rect1 = Rect::new(Vec2::ZERO, Vec2::new(1.0, 1.0));
let rect2 = Rect::new(Vec2::new(5.0, 5.0), Vec2::new(6.0, 6.0));
assert!(!rect1.intersects(&rect2));
assert!(rect1.intersection(&rect2).is_none());
}
#[test]
fn test_rect_expand_to_include() {
let mut rect = Rect::new(Vec2::new(1.0, 1.0), Vec2::new(3.0, 3.0));
rect.expand_to_include(Vec2::new(5.0, 0.0));
assert_eq!(rect.min, Vec2::new(1.0, 0.0));
assert_eq!(rect.max, Vec2::new(5.0, 3.0));
}
#[test]
fn test_rect_expand_negative() {
let rect = Rect::new(Vec2::new(0.0, 0.0), Vec2::new(10.0, 10.0));
let contracted = rect.expand(-2.0);
assert_eq!(contracted.min, Vec2::new(2.0, 2.0));
assert_eq!(contracted.max, Vec2::new(8.0, 8.0));
}
#[test]
fn test_rect_is_valid() {
let valid = Rect::new(Vec2::ZERO, Vec2::ONE);
assert!(valid.is_valid());
let also_valid = Rect::ZERO;
assert!(also_valid.is_valid());
}
#[test]
fn test_circle_negative_radius() {
let circle = Circle::new(Vec2::ZERO, -5.0);
assert_eq!(circle.radius, 0.0); }
#[test]
fn test_circle_zero_radius() {
let circle = Circle::new(Vec2::new(1.0, 1.0), 0.0);
assert_eq!(circle.area(), 0.0);
assert_eq!(circle.circumference(), 0.0);
assert!(circle.contains(Vec2::new(1.0, 1.0))); }
#[test]
fn test_circle_bounding_rect() {
let circle = Circle::new(Vec2::new(5.0, 5.0), 3.0);
let bounding = circle.bounding_rect();
assert_eq!(bounding.min, Vec2::new(2.0, 2.0));
assert_eq!(bounding.max, Vec2::new(8.0, 8.0));
}
#[test]
fn test_circle_touching() {
let c1 = Circle::new(Vec2::ZERO, 3.0);
let c2 = Circle::new(Vec2::new(6.0, 0.0), 3.0);
assert!(c1.intersects(&c2)); }
#[test]
fn test_bounds3d_intersection() {
let b1 = Bounds3D::new(Vec3::ZERO, Vec3::splat(5.0));
let b2 = Bounds3D::new(Vec3::splat(3.0), Vec3::splat(8.0));
let intersection = b1.intersection(&b2).unwrap();
assert_eq!(intersection.min, Vec3::splat(3.0));
assert_eq!(intersection.max, Vec3::splat(5.0));
}
#[test]
fn test_bounds3d_no_intersection() {
let b1 = Bounds3D::new(Vec3::ZERO, Vec3::ONE);
let b2 = Bounds3D::new(Vec3::splat(5.0), Vec3::splat(6.0));
assert!(!b1.intersects(&b2));
assert!(b1.intersection(&b2).is_none());
}
#[test]
fn test_bounds3d_union() {
let b1 = Bounds3D::new(Vec3::ZERO, Vec3::splat(2.0));
let b2 = Bounds3D::new(Vec3::splat(3.0), Vec3::splat(5.0));
let union = b1.union(&b2);
assert_eq!(union.min, Vec3::ZERO);
assert_eq!(union.max, Vec3::splat(5.0));
}
#[test]
fn test_bounds3d_expand_to_include() {
let mut bounds = Bounds3D::new(Vec3::ONE, Vec3::splat(3.0));
bounds.expand_to_include(Vec3::new(-1.0, 5.0, 2.0));
assert_eq!(bounds.min, Vec3::new(-1.0, 1.0, 1.0));
assert_eq!(bounds.max, Vec3::new(3.0, 5.0, 3.0));
}
#[test]
fn test_bounds3d_contains_boundary() {
let bounds = Bounds3D::new(Vec3::ZERO, Vec3::splat(10.0));
assert!(bounds.contains(Vec3::ZERO));
assert!(bounds.contains(Vec3::splat(10.0)));
assert!(!bounds.contains(Vec3::new(10.001, 5.0, 5.0)));
}
}