use valo_geometry::{Matrix, Rect};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Bounds {
Empty,
Bounded(Rect),
Unbounded,
}
impl Bounds {
pub fn of(rect: Rect) -> Self {
if rect.is_empty() {
Self::Empty
} else {
Self::Bounded(rect)
}
}
pub fn rect(&self) -> Option<Rect> {
match self {
Self::Bounded(rect) => Some(*rect),
Self::Empty | Self::Unbounded => None,
}
}
pub fn is_empty(&self) -> bool {
*self == Self::Empty
}
pub fn map(&self, transform: &Matrix) -> Self {
match self {
Self::Bounded(rect) => Self::of(transform.map_rect(rect)),
Self::Empty | Self::Unbounded => *self,
}
}
pub fn union(&self, other: &Self) -> Self {
match (self, other) {
(Self::Unbounded, _) | (_, Self::Unbounded) => Self::Unbounded,
(Self::Empty, bounds) | (bounds, Self::Empty) => *bounds,
(Self::Bounded(a), Self::Bounded(b)) => Self::Bounded(a.union(b)),
}
}
pub fn intersect(&self, other: &Self) -> Self {
match (self, other) {
(Self::Empty, _) | (_, Self::Empty) => Self::Empty,
(Self::Unbounded, bounds) | (bounds, Self::Unbounded) => *bounds,
(Self::Bounded(a), Self::Bounded(b)) => {
a.intersect(b).map_or(Self::Empty, Self::Bounded)
}
}
}
pub(crate) fn intersects(&self, other: &Self) -> bool {
!self.intersect(other).is_empty()
}
pub(crate) fn expand(&self, distance: f32) -> Self {
match self {
Self::Bounded(rect) => Self::of(rect.expand(distance)),
Self::Empty | Self::Unbounded => *self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bounded(x: f32, y: f32, width: f32, height: f32) -> Bounds {
Bounds::Bounded(Rect::new(x, y, width, height))
}
#[test]
fn a_rectangle_without_area_bounds_nothing() {
assert_eq!(Bounds::of(Rect::new(4.0, 4.0, 0.0, 10.0)), Bounds::Empty);
assert_eq!(
Bounds::of(Rect::new(4.0, 4.0, 1.0, 10.0)),
bounded(4.0, 4.0, 1.0, 10.0)
);
}
#[test]
fn a_union_takes_the_larger_case() {
let a = bounded(0.0, 0.0, 10.0, 10.0);
let b = bounded(20.0, 0.0, 10.0, 10.0);
assert_eq!(a.union(&b), bounded(0.0, 0.0, 30.0, 10.0));
assert_eq!(a.union(&Bounds::Empty), a);
assert_eq!(Bounds::Empty.union(&Bounds::Empty), Bounds::Empty);
assert_eq!(a.union(&Bounds::Unbounded), Bounds::Unbounded);
}
#[test]
fn an_intersection_takes_the_smaller_case() {
let clip = bounded(0.0, 0.0, 10.0, 10.0);
assert_eq!(Bounds::Unbounded.intersect(&clip), clip);
assert_eq!(
bounded(5.0, 5.0, 10.0, 10.0).intersect(&clip),
bounded(5.0, 5.0, 5.0, 5.0)
);
assert_eq!(bounded(20.0, 0.0, 5.0, 5.0).intersect(&clip), Bounds::Empty);
assert_eq!(Bounds::Unbounded.intersect(&Bounds::Empty), Bounds::Empty);
assert_eq!(
Bounds::Unbounded.intersect(&Bounds::Unbounded),
Bounds::Unbounded
);
}
#[test]
fn a_transform_moves_only_bounded_content() {
let moved = Matrix::translation(1.0e9, 0.0);
assert_eq!(Bounds::Unbounded.map(&moved), Bounds::Unbounded);
assert_eq!(
bounded(0.0, 0.0, 10.0, 10.0).map(&Matrix::translation(5.0, 0.0)),
bounded(5.0, 0.0, 10.0, 10.0)
);
assert_eq!(
bounded(0.0, 0.0, 10.0, 10.0).map(&Matrix::scale(0.0, 1.0)),
Bounds::Empty
);
}
}