Skip to content

Introduce the (Typed)Box2D type. #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -133,3 +135,10 @@ pub type Matrix4D<T> = Transform3D<T>;
#[deprecated]
pub type TypedMatrix4D<T, Src, Dst> = TypedTransform3D<T, Src, Dst>;

fn min<T: PartialOrd>(x: T, y: T) -> T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems rather sad that we need those functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a more general rust std sadness topic. They were already there, I just moved them up so that Point::min/max can use that instead of depending on the Float trait.

if x <= y { x } else { y }
}

fn max<T: PartialOrd>(x: T, y: T) -> T {
if x >= y { x } else { y }
}
15 changes: 8 additions & 7 deletions src/point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -169,15 +170,15 @@ impl<T: Copy + Sub<T, Output=T>, U> Sub<TypedVector2D<T, U>> for TypedPoint2D<T,
}
}

impl<T: Float, U> TypedPoint2D<T, U> {
impl<T: Copy + PartialOrd, U> TypedPoint2D<T, U> {
#[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))
}
}

Expand Down Expand Up @@ -527,15 +528,15 @@ impl<T: Copy + Div<T, Output=T>, U> Div<T> for TypedPoint3D<T, U> {
}
}

impl<T: Float, U> TypedPoint3D<T, U> {
impl<T: Copy + PartialOrd, U> TypedPoint3D<T, U> {
#[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))
}
}

Expand Down
221 changes: 206 additions & 15 deletions src/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<T, U = UnknownUnit> {
pub origin: TypedPoint2D<T, U>,
Expand Down Expand Up @@ -165,6 +166,11 @@ where T: Copy + Clone + Zero + PartialOrd + PartialEq + Add<T, Output=T> + Sub<T
lower_right_y - upper_left.y)))
}

#[inline]
pub fn to_box(&self) -> TypedBox2D<T, U> {
TypedBox2D::new(self.origin, point2(self.max_x(), self.max_y()))
}

/// Returns the same rectangle, translated by a vector.
#[inline]
#[must_use]
Expand Down Expand Up @@ -317,15 +323,6 @@ impl<T: Copy + PartialEq + Zero, U> TypedRect<T, U> {
}
}


pub fn min<T: Clone + PartialOrd>(x: T, y: T) -> T {
if x <= y { x } else { y }
}

pub fn max<T: Clone + PartialOrd>(x: T, y: T) -> T {
if x >= y { x } else { y }
}

impl<T: Copy + Mul<T, Output=T>, U> Mul<T> for TypedRect<T, U> {
type Output = Self;
#[inline]
Expand Down Expand Up @@ -455,9 +452,153 @@ impl<T: NumCast + Copy, Unit> TypedRect<T, Unit> {
}
}

/// A 2d Rectangle represented using two points, optionally tagged with a unit.
#[repr(C)]
pub struct TypedBox2D<T, Unit = UnknownUnit> {
pub min: TypedPoint2D<T, Unit>,
pub max: TypedPoint2D<T, Unit>,
}

/// The default box type with no unit.
pub type Box2D<T> = TypedBox2D<T, UnknownUnit>;

impl<T, U> TypedBox2D<T, U> {
pub fn new(min: TypedPoint2D<T, U>, max: TypedPoint2D<T, U>) -> Self {
TypedBox2D { min, max }
}
}

impl<T, U> TypedBox2D<T, U>
where T: Copy + Clone + Zero + One + PartialOrd + PartialEq + Add<Output=T> + Sub<Output=T> + Mul<Output=T> {
/// Returns a rectangle if this box is positive.
#[inline]
pub fn to_rect(&self) -> Option<TypedRect<T, U>> {
if self.is_positive() {
return None
}

Some(TypedRect { origin: self.min, size: self.size() })
}

#[inline]
pub fn size(&self) -> TypedSize2D<T, U> {
(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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to special case this or can we just rely on the maxes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can rely on the maxes for union (although it does work for intersection). We could turn this into a debug_assert and force people to use ensure_positive beforehand if this is a performance concern.

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<T, U>) -> 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<T: Copy, U> Copy for TypedBox2D<T, U> {}

impl<T: Copy, U> Clone for TypedBox2D<T, U> {
fn clone(&self) -> Self { *self }
}

impl<T: PartialEq, U> PartialEq<TypedBox2D<T, U>> for TypedBox2D<T, U> {
fn eq(&self, other: &Self) -> bool {
self.min.eq(&other.min) && self.max.eq(&other.max)
}
}

impl<T: Eq, U> Eq for TypedBox2D<T, U> {}

impl<T: fmt::Debug, U> fmt::Debug for TypedBox2D<T, U> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Box2D({:?} to {:?})", self.min, self.max)
}
}

impl<T: fmt::Display, U> fmt::Display for TypedBox2D<T, U> {
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<T: Copy, U>(x: T, y: T, w: T, h: T) -> TypedRect<T, U> {
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<T: Copy, U>(min_x: T, min_y: T, max_x: T, max_y: T) -> TypedBox2D<T, U> {
TypedBox2D::new(point2(min_x, min_y), point2(max_x, max_y))
}

#[cfg(test)]
Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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());
}
}
Loading