diff --git a/src/lib.rs b/src/lib.rs index bc967fba..3590ec91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,8 +79,10 @@ pub use vector::{ Vector2D, TypedVector2D, vec2, Vector3D, TypedVector3D, vec3, }; - -pub use rect::{Rect, TypedRect, rect}; +pub use rect::{ + Rect, TypedRect, rect, + Box2D, TypedBox2D, box2, +}; pub use side_offsets::{SideOffsets2D, TypedSideOffsets2D}; #[cfg(feature = "unstable")] pub use side_offsets::SideOffsets2DSimdI32; pub use size::{Size2D, TypedSize2D, size2}; @@ -133,3 +135,10 @@ pub type Matrix4D = Transform3D; #[deprecated] pub type TypedMatrix4D = TypedTransform3D; +fn min(x: T, y: T) -> T { + if x <= y { x } else { y } +} + +fn max(x: T, y: T) -> T { + if x >= y { x } else { y } +} diff --git a/src/point.rs b/src/point.rs index f1efca69..0bc29cd4 100644 --- a/src/point.rs +++ b/src/point.rs @@ -13,11 +13,12 @@ use length::Length; use scale_factor::ScaleFactor; use size::TypedSize2D; use num::*; -use num_traits::{Float, NumCast}; +use num_traits::NumCast; use vector::{TypedVector2D, TypedVector3D, vec2, vec3}; use std::fmt; use std::ops::{Add, Mul, Sub, Div, AddAssign, SubAssign, MulAssign, DivAssign}; use std::marker::PhantomData; +use {min, max}; define_matrix! { /// A 2d Point tagged with a unit. @@ -169,15 +170,15 @@ impl, U> Sub> for TypedPoint2D TypedPoint2D { +impl TypedPoint2D { #[inline] pub fn min(self, other: Self) -> Self { - point2(self.x.min(other.x), self.y.min(other.y)) + point2(min(self.x, other.x), min(self.y, other.y)) } #[inline] pub fn max(self, other: Self) -> Self { - point2(self.x.max(other.x), self.y.max(other.y)) + point2(max(self.x, other.x), max(self.y, other.y)) } } @@ -527,15 +528,15 @@ impl, U> Div for TypedPoint3D { } } -impl TypedPoint3D { +impl TypedPoint3D { #[inline] pub fn min(self, other: Self) -> Self { - point3(self.x.min(other.x), self.y.min(other.y), self.z.min(other.z)) + point3(min(self.x, other.x), min(self.y, other.y), min(self.z, other.z)) } #[inline] pub fn max(self, other: Self) -> Self { - point3(self.x.max(other.x), self.y.max(other.y), self.z.max(other.z)) + point3(max(self.x, other.x), max(self.y, other.y), max(self.z, other.z)) } } diff --git a/src/rect.rs b/src/rect.rs index ce472fe4..a0f182d8 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -11,9 +11,9 @@ use super::UnknownUnit; use length::Length; use scale_factor::ScaleFactor; use num::*; -use point::TypedPoint2D; +use point::{TypedPoint2D, point2}; use vector::TypedVector2D; -use size::TypedSize2D; +use size::{TypedSize2D, size2}; use heapsize::HeapSizeOf; use num_traits::NumCast; @@ -22,8 +22,9 @@ use std::cmp::PartialOrd; use std::fmt; use std::hash::{Hash, Hasher}; use std::ops::{Add, Sub, Mul, Div}; +use {min, max}; -/// A 2d Rectangle optionally tagged with a unit. +/// A 2d Rectangle represented using a point and a size, optionally tagged with a unit. #[repr(C)] pub struct TypedRect { pub origin: TypedPoint2D, @@ -165,6 +166,11 @@ where T: Copy + Clone + Zero + PartialOrd + PartialEq + Add + Sub TypedBox2D { + TypedBox2D::new(self.origin, point2(self.max_x(), self.max_y())) + } + /// Returns the same rectangle, translated by a vector. #[inline] #[must_use] @@ -317,15 +323,6 @@ impl TypedRect { } } - -pub fn min(x: T, y: T) -> T { - if x <= y { x } else { y } -} - -pub fn max(x: T, y: T) -> T { - if x >= y { x } else { y } -} - impl, U> Mul for TypedRect { type Output = Self; #[inline] @@ -455,9 +452,153 @@ impl TypedRect { } } +/// A 2d Rectangle represented using two points, optionally tagged with a unit. +#[repr(C)] +pub struct TypedBox2D { + pub min: TypedPoint2D, + pub max: TypedPoint2D, +} + +/// The default box type with no unit. +pub type Box2D = TypedBox2D; + +impl TypedBox2D { + pub fn new(min: TypedPoint2D, max: TypedPoint2D) -> Self { + TypedBox2D { min, max } + } +} + +impl TypedBox2D +where T: Copy + Clone + Zero + One + PartialOrd + PartialEq + Add + Sub + Mul { + /// Returns a rectangle if this box is positive. + #[inline] + pub fn to_rect(&self) -> Option> { + if self.is_positive() { + return None + } + + Some(TypedRect { origin: self.min, size: self.size() }) + } + + #[inline] + pub fn size(&self) -> TypedSize2D { + (self.max - self.min).to_size() + } + + /// Returns true if both width and height are superior or equal to zero. + #[inline] + pub fn is_positive(&self) -> bool { + self.max.x >= self.min.x && self.max.y >= self.min.y + } + + /// Returns true if either width or height are inferior to zero. + #[inline] + pub fn is_negative(&self) -> bool { + self.max.x < self.min.x || self.max.y < self.min.y + } + + /// Returns true if either width or height are inferior or equal to zero. + #[inline] + pub fn is_empty_or_negative(&self) -> bool { + self.max.x <= self.min.x || self.max.y <= self.min.y + } + + #[inline] + pub fn intersects(&self, other: &Self) -> bool { + self.intersection(other).is_positive() + } + + #[inline] + pub fn intersection(&self, other: &Self) -> Self { + TypedBox2D { + min: point2(max(self.min.x, other.min.x), max(self.min.y, other.min.y)), + max: point2(min(self.max.x, other.max.x), min(self.max.y, other.max.y)), + } + } + + #[inline] + pub fn union(&self, other: &Self) -> Self { + if other.is_empty_or_negative() { + return *self; + } + + if self.is_empty_or_negative() { + return *other; + } + + TypedBox2D { + min: point2(min(self.min.x, other.min.x), min(self.min.y, other.min.y)), + max: point2(max(self.max.x, other.max.x), max(self.max.y, other.max.y)), + } + } + + /// Returns true if this box contains the point. Points are considered + /// in the box if they are on the left or top edge, but outside if they + /// are on the right or bottom edge. + #[inline] + pub fn contains(&self, point: &TypedPoint2D) -> bool { + self.min.x <= point.x && point.x < self.max.x && + self.min.y <= point.y && point.y < self.max.y + } + + /// Linearly interpolate between this box and another box. + /// + /// The box should be positive. + /// `t` is expected to be between zero and one. + #[inline] + pub fn lerp(&self, other: Self, t: T) -> Self { + debug_assert!(self.is_positive()); + debug_assert!(other.is_positive()); + Self::new( + self.min.lerp(other.min, t), + self.max.lerp(other.max, t), + ) + } + + /// Return the same box if positive, or an empty box if negative. + /// + /// This is useful after computing intersections since the latter can produce negative boxes. + #[inline] + #[must_use] + pub fn ensure_positive(&self) -> Self { + TypedBox2D { min: self.min, max: self.max.max(self.min) } + } +} + +impl Copy for TypedBox2D {} + +impl Clone for TypedBox2D { + fn clone(&self) -> Self { *self } +} + +impl PartialEq> for TypedBox2D { + fn eq(&self, other: &Self) -> bool { + self.min.eq(&other.min) && self.max.eq(&other.max) + } +} + +impl Eq for TypedBox2D {} + +impl fmt::Debug for TypedBox2D { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Box2D({:?} to {:?})", self.min, self.max) + } +} + +impl fmt::Display for TypedBox2D { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "Box2D({} to {})", self.min, self.max) + } +} + /// Shorthand for `TypedRect::new(TypedPoint2D::new(x, y), TypedSize2D::new(w, h))`. pub fn rect(x: T, y: T, w: T, h: T) -> TypedRect { - TypedRect::new(TypedPoint2D::new(x, y), TypedSize2D::new(w, h)) + TypedRect::new(point2(x, y), size2(w, h)) +} + +/// Shorthand for `TypedBox2D::new(TypedPoint2D::new(min_x, min_y), TypedBox2D::new(max_x, max_y))`. +pub fn box2(min_x: T, min_y: T, max_x: T, max_y: T) -> TypedBox2D { + TypedBox2D::new(point2(min_x, min_y), point2(max_x, max_y)) } #[cfg(test)] @@ -517,7 +658,7 @@ mod tests { } #[test] - fn test_union() { + fn test_rect_union() { let p = Rect::new(Point2D::new(0, 0), Size2D::new(50, 40)); let q = Rect::new(Point2D::new(20,20), Size2D::new(5, 5)); let r = Rect::new(Point2D::new(-15, -30), Size2D::new(200, 15)); @@ -538,7 +679,27 @@ mod tests { } #[test] - fn test_intersection() { + fn test_box_union() { + let p = Rect::new(point2(0, 0), size2(50, 40)).to_box(); + let q = Rect::new(point2(20, 20), size2(5, 5)).to_box(); + let r = Rect::new(point2(-15, -30), size2(200, 15)).to_box(); + let s = Rect::new(point2(20, -15), size2(250, 200)).to_box(); + + let pq = p.union(&q); + assert_eq!(pq.min, point2(0, 0)); + assert_eq!(pq.size(), size2(50, 40)); + + let pr = p.union(&r); + assert_eq!(pr.min, point2(-15, -30)); + assert_eq!(pr.size(), size2(200, 70)); + + let ps = p.union(&s); + assert_eq!(ps.min, point2(0, -15)); + assert_eq!(ps.size(), size2(270, 200)); + } + + #[test] + fn test_rect_intersection() { let p = Rect::new(Point2D::new(0, 0), Size2D::new(10, 20)); let q = Rect::new(Point2D::new(5, 15), Size2D::new(10, 10)); let r = Rect::new(Point2D::new(-5, -5), Size2D::new(8, 8)); @@ -559,6 +720,26 @@ mod tests { assert!(qr.is_none()); } + #[test] + fn test_box_intersection() { + let p = Rect::new(point2(0, 0), size2(10, 20)).to_box(); + let q = Rect::new(point2(5, 15), size2(10, 10)).to_box(); + let r = Rect::new(point2(-5, -5), size2(8, 8)).to_box(); + + let pq = p.intersection(&q); + assert!(pq.is_positive()); + assert_eq!(pq.min, point2(5, 15)); + assert_eq!(pq.size(), size2(5, 5)); + + let pr = p.intersection(&r); + assert!(pr.is_positive()); + assert_eq!(pr.min, point2(0, 0)); + assert_eq!(pr.size(), size2(3, 3)); + + let qr = q.intersection(&r); + assert!(qr.is_empty_or_negative()); + } + #[test] fn test_contains() { let r = Rect::new(Point2D::new(-20, 15), Size2D::new(100, 200)); @@ -697,4 +878,14 @@ mod tests { x += 0.1 } } + + #[test] + fn test_box_negative() { + assert!(Box2D::new(point2(0.0, 0.0), point2(0.0, 0.0)).is_positive()); + assert!(Box2D::new(point2(0.0, 0.0), point2(0.0, 0.0)).is_empty_or_negative()); + assert!(Box2D::new(point2(-1.0, -2.0), point2(0.0, 0.0)).is_positive()); + assert!(Box2D::new(point2(1.0, 2.0), point2(0.0, 1.0)).is_negative()); + assert!(Box2D::new(point2(1.0, 2.0), point2(0.0, 2.0)).is_negative()); + assert!(Box2D::new(point2(1.0, 2.0), point2(1.0, 0.0)).is_negative()); + } } diff --git a/src/vector.rs b/src/vector.rs index 141306b8..7e53e9f0 100644 --- a/src/vector.rs +++ b/src/vector.rs @@ -18,6 +18,7 @@ use num_traits::{Float, NumCast}; use std::fmt; use std::ops::{Add, Neg, Mul, Sub, Div, AddAssign, SubAssign, MulAssign, DivAssign}; use std::marker::PhantomData; +use {min, max}; define_matrix! { /// A 2d Vector tagged with a unit. @@ -202,15 +203,15 @@ impl , U> Neg for TypedVector2D { } } -impl TypedVector2D { +impl TypedVector2D { #[inline] pub fn min(self, other: Self) -> Self { - vec2(self.x.min(other.x), self.y.min(other.y)) + vec2(min(self.x, other.x), min(self.y, other.y)) } #[inline] pub fn max(self, other: Self) -> Self { - vec2(self.x.max(other.x), self.y.max(other.y)) + vec2(max(self.x, other.x), max(self.y, other.y)) } } @@ -596,15 +597,15 @@ impl, U> DivAssign for TypedVector3D { } } -impl TypedVector3D { +impl TypedVector3D { #[inline] pub fn min(self, other: Self) -> Self { - vec3(self.x.min(other.x), self.y.min(other.y), self.z.min(other.z)) + vec3(min(self.x, other.x), min(self.y, other.y), min(self.z, other.z)) } #[inline] pub fn max(self, other: Self) -> Self { - vec3(self.x.max(other.x), self.y.max(other.y), self.z.max(other.z)) + vec3(max(self.x, other.x), max(self.y, other.y), max(self.z, other.z)) } }