#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Point {
pub const ZERO: Self = Self { x: 0, y: 0 };
#[inline]
pub const fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Size {
pub width: u32,
pub height: u32,
}
impl Size {
pub const ZERO: Self = Self {
width: 0,
height: 0,
};
#[inline]
pub const fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
#[inline]
pub const fn area(&self) -> u64 {
self.width as u64 * self.height as u64
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl Rect {
pub const ZERO: Self = Self {
x: 0,
y: 0,
width: 0,
height: 0,
};
#[inline]
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
Self {
x,
y,
width: if width > 0 { width } else { 0 },
height: if height > 0 { height } else { 0 },
}
}
#[inline]
pub const fn from_edges(left: i32, top: i32, right: i32, bottom: i32) -> Self {
Self::new(
left,
top,
right.saturating_sub(left),
bottom.saturating_sub(top),
)
}
#[inline]
pub const fn from_size(size: Size) -> Self {
Self::new(0, 0, size.width as i32, size.height as i32)
}
#[inline]
pub const fn right(&self) -> i32 {
self.x.saturating_add(self.width)
}
#[inline]
pub const fn bottom(&self) -> i32 {
self.y.saturating_add(self.height)
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.width <= 0 || self.height <= 0
}
#[inline]
pub const fn area(&self) -> u64 {
if self.is_empty() {
0
} else {
self.width as u64 * self.height as u64
}
}
#[inline]
pub const fn contains(&self, p: Point) -> bool {
p.x >= self.x && p.y >= self.y && p.x < self.right() && p.y < self.bottom()
}
#[inline]
pub const fn contains_rect(&self, other: &Rect) -> bool {
other.is_empty()
|| (other.x >= self.x
&& other.y >= self.y
&& other.right() <= self.right()
&& other.bottom() <= self.bottom())
}
#[inline]
pub const fn intersects(&self, other: &Rect) -> bool {
!self.is_empty()
&& !other.is_empty()
&& self.x < other.right()
&& other.x < self.right()
&& self.y < other.bottom()
&& other.y < self.bottom()
}
#[inline]
pub const fn touches(&self, other: &Rect) -> bool {
!self.is_empty()
&& !other.is_empty()
&& self.x <= other.right()
&& other.x <= self.right()
&& self.y <= other.bottom()
&& other.y <= self.bottom()
}
#[inline]
pub fn intersect(&self, other: &Rect) -> Option<Rect> {
let r = Rect::from_edges(
self.x.max(other.x),
self.y.max(other.y),
self.right().min(other.right()),
self.bottom().min(other.bottom()),
);
(!r.is_empty()).then_some(r)
}
#[inline]
pub fn union(&self, other: &Rect) -> Rect {
if self.is_empty() {
return *other;
}
if other.is_empty() {
return *self;
}
Rect::from_edges(
self.x.min(other.x),
self.y.min(other.y),
self.right().max(other.right()),
self.bottom().max(other.bottom()),
)
}
#[inline]
pub const fn translate(&self, dx: i32, dy: i32) -> Rect {
Self {
x: self.x.saturating_add(dx),
y: self.y.saturating_add(dy),
width: self.width,
height: self.height,
}
}
#[inline]
pub fn inflate(&self, d: i32) -> Rect {
Rect::from_edges(
self.x.saturating_sub(d),
self.y.saturating_sub(d),
self.right().saturating_add(d),
self.bottom().saturating_add(d),
)
}
#[inline]
pub fn clip_to_size(&self, size: Size) -> Option<Rect> {
self.intersect(&Rect::from_size(size))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn negative_extent_is_clamped() {
let r = Rect::new(10, 10, -5, -5);
assert!(r.is_empty());
assert_eq!(r.area(), 0);
}
#[test]
fn from_edges_handles_inverted_input() {
assert!(Rect::from_edges(20, 20, 10, 10).is_empty());
assert_eq!(Rect::from_edges(1, 2, 5, 9), Rect::new(1, 2, 4, 7));
}
#[test]
fn intersect_and_union() {
let a = Rect::new(0, 0, 10, 10);
let b = Rect::new(5, 5, 10, 10);
assert_eq!(a.intersect(&b), Some(Rect::new(5, 5, 5, 5)));
assert_eq!(a.union(&b), Rect::new(0, 0, 15, 15));
assert_eq!(a.intersect(&Rect::new(50, 50, 1, 1)), None);
}
#[test]
fn union_ignores_empty_operands() {
let a = Rect::new(3, 4, 5, 6);
assert_eq!(a.union(&Rect::ZERO), a);
assert_eq!(Rect::ZERO.union(&a), a);
}
#[test]
fn touching_but_not_intersecting() {
let a = Rect::new(0, 0, 10, 10);
let b = Rect::new(10, 0, 10, 10);
assert!(!a.intersects(&b));
assert!(a.touches(&b));
}
#[test]
fn containment() {
let outer = Rect::new(0, 0, 100, 100);
assert!(outer.contains_rect(&Rect::new(10, 10, 10, 10)));
assert!(!outer.contains_rect(&Rect::new(95, 95, 10, 10)));
assert!(outer.contains(Point::new(99, 99)));
assert!(!outer.contains(Point::new(100, 0)));
}
}