use delaunator::EPSILON;
use super::Point;
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum BoundingBoxTopBottomEdge {
Top,
Bottom,
None
}
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum BoundingBoxLeftRightEdge {
Left,
Right,
None
}
#[derive(Debug, Clone)]
pub struct BoundingBox {
center: Point,
top_right: Point,
}
impl Default for BoundingBox {
fn default() -> Self {
Self::new_centered(1.0, 1.0)
}
}
impl BoundingBox {
pub fn new(origin: Point, width: f64, height: f64) -> Self {
Self {
top_right: Point { x: origin.x + width / 2.0, y: origin.y + height / 2.0 },
center: origin,
}
}
pub fn new_centered(width: f64, height: f64) -> Self {
Self::new(Point { x: 0.0, y: 0.0 }, width, height)
}
pub fn new_centered_square(width: f64) -> Self {
Self::new_centered(width, width)
}
#[inline]
pub fn center(&self) -> &Point {
&self.center
}
#[inline]
pub fn top_right(&self) -> &Point {
&self.top_right
}
#[inline]
pub fn width(&self) -> f64 {
2.0 * (self.top_right.x - self.center.x)
}
#[inline]
pub fn height(&self) -> f64 {
2.0 * (self.top_right.y - self.center.y)
}
#[inline]
pub fn is_inside(&self, point: &Point) -> bool {
point.x.abs() <= self.top_right.x && point.y.abs() <= self.top_right.y
}
#[inline]
pub (crate) fn which_edge(&self, point: &Point) -> (BoundingBoxTopBottomEdge, BoundingBoxLeftRightEdge) {
(
if point.y == self.top_right.y {
BoundingBoxTopBottomEdge::Top
} else if point.y == -self.top_right.y {
BoundingBoxTopBottomEdge::Bottom
} else {
BoundingBoxTopBottomEdge::None
},
if point.x == self.top_right.x {
BoundingBoxLeftRightEdge::Right
} else if point.x == - self.top_right.x {
BoundingBoxLeftRightEdge::Left
} else {
BoundingBoxLeftRightEdge::None
}
)
}
pub (crate) fn intersect_line(&self, a: &Point, b: &Point) -> (Option<Point>, Option<Point>) {
let c_x = b.x - a.x;
let c_y = b.y - a.y;
let c = c_y / c_x;
let d = a.y - (a.x * c);
let mut f = None;
let mut g = None;
let mut h = None;
let mut i = None;
if c_x.abs() > EPSILON {
let right_y = (self.top_right.x * c) + d;
let left_y = d - (self.top_right.x * c);
if right_y.abs() <= self.top_right.y {
f = Some(Point { x: self.top_right.x, y: right_y });
}
if left_y.abs() <= self.top_right.y {
g = Some(Point { x: -self.top_right.x, y: left_y })
}
if g.is_some() && f.is_some() {
return (f, g);
}
}
if c_y.abs() > EPSILON {
if c_x.abs() < EPSILON {
if a.x.abs() <= self.top_right.x {
return (
Some(Point { x: a.x, y: self.top_right.y }),
Some(Point { x: a.x, y: -self.top_right.y })
);
} else {
return (None, None);
}
}
let top_x = (self.top_right.y - d) / c;
let bottom_x = -(d + self.top_right.y) / c;
if top_x.abs() <= self.top_right.x {
h = Some(Point { x: top_x, y: self.top_right.y })
}
if bottom_x.abs() <= self.top_right.x {
i = Some(Point { x: bottom_x, y: -self.top_right.y })
}
if h.is_some() && i.is_some() {
return (h, i);
}
}
(f.or(g), h.or(i))
}
pub (crate) fn project_ray(&self, point: &Point, direction: &Point) -> (Option<Point>, Option<Point>) {
let b = Point { x: point.x + direction.x, y: point.y + direction.y };
let (a, b) = self.intersect_line(point, &b);
order_points_on_ray(point, direction, a, b)
}
pub (crate) fn project_ray_closest(&self, point: &Point, direction: &Point) -> Option<Point> {
self.project_ray(point, direction).0
}
}
pub (crate) fn order_points_on_ray(point: &Point, direction: &Point, a: Option<Point>, b: Option<Point>) -> (Option<Point>, Option<Point>) {
if let Some(va) = a {
if let Some(vb) = b {
let (d, da, db) = if direction.x.abs() > direction.y.abs() {
(direction.x, va.x - point.x, vb.x - point.x)
} else {
(direction.y, va.y - point.y, vb.y - point.y)
};
if d.signum() == da.signum() {
if d.signum() == db.signum() {
if da.abs() > db.abs() {
(Some(vb), Some(va))
} else {
(Some(va), Some(vb))
}
} else {
(Some(va), None)
}
} else if d.signum() == db.signum() {
(Some(vb), None)
} else {
(None, None)
}
} else if direction.x.signum() == va.x.signum() && direction.y.signum() == va.y.signum() {
(Some(va), None)
} else {
(None, None)
}
} else {
(None, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn line(x: f64, c: f64, d: f64) -> Point {
Point { x, y: (x * c) + d }
}
fn direction(a: &Point, b: &Point) -> Point {
Point { x: a.x - b.x, y: a.y - b.y }
}
fn assert_close_enough(a: Point, b: Option<Point>, message: &str) {
let b = b.expect(&format!("Expected value, but found None. {}", message));
let close_enough = 5.0 * EPSILON;
assert!((a.x - b.x).abs() < close_enough, "a.x [{:?}] and b.x [{:?}] expected to be close enough. Difference was: [{}]. {}", a.x, b.x, (a.x - b.x).abs(), message);
assert!((a.y - b.y).abs() < close_enough, "a.y [{:?}] and b.y [{:?}] expected to be close enough. Difference was: [{}]. {}", a.y, b.y, (a.y - b.y).abs(), message);
}
#[test]
fn intersect_line_tests() {
let bbox = BoundingBox::new_centered_square(2.0);
let (a, b) = bbox.intersect_line(&Point { x: 5.0, y: 0.0 }, &Point { x: 5.0, y: 1.0 });
assert_eq!(a, None, "No intersection expected for a parallel line to X outside of the box");
assert_eq!(b, None, "No intersection expected for a parallel line to X outside of the box");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 5.0 }, &Point { x: 1.0, y: 5.0 });
assert_eq!(a, None, "No intersection expected for a parallel line to Y outside of the box");
assert_eq!(b, None, "No intersection expected for a parallel line to Y outside of the box");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 0.0 }, &Point { x: 0.0, y: 1.0 });
assert_eq!(Some(Point { x: 0.0, y: 1.0 }), a, "Expected intersection with top edge");
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), b, "Expected intersection with bottom edge");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 0.0 }, &Point { x: 1.0, y: 0.0 });
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), a, "Expected intersection with right edge");
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), b, "Expected intersection with left edge");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 1.0 }, &Point { x: 1.0, y: 1.0 });
assert_eq!(Some(Point { x: 1.0, y: 1.0 }), a, "Expected intersection with top right corner");
assert_eq!(Some(Point { x: -1.0, y: 1.0 }), b, "Expected intersection with top left corner");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: -1.0 }, &Point { x: 1.0, y: -1.0 });
assert_eq!(Some(Point { x: 1.0, y: -1.0 }), a, "Expected intersection with bottom right corner");
assert_eq!(Some(Point { x: -1.0, y: -1.0 }), b, "Expected intersection with bottom left corner");
let (a, b) = bbox.intersect_line(&Point { x: 1.0, y: 0.0 }, &Point { x: 1.0, y: 1.0 });
assert_eq!(Some(Point { x: 1.0, y: 1.0 }), a, "Expected intersection with top right corner");
assert_eq!(Some(Point { x: 1.0, y: -1.0 }), b, "Expected intersection with bottom right corner");
let (a, b) = bbox.intersect_line(&Point { x: -1.0, y: 0.0 }, &Point { x: -1.0, y: 1.0 });
assert_eq!(Some(Point { x: -1.0, y: 1.0 }), a, "Expected intersection with top left corner");
assert_eq!(Some(Point { x: -1.0, y: -1.0 }), b, "Expected intersection with bottom left corner");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 0.0 }, &Point { x: 1.0, y: 1.0 });
assert_eq!(Some(Point { x: 1.0, y: 1.0 }), a, "Expected intersection with top right corner");
assert_eq!(Some(Point { x: -1.0, y: -1.0 }), b, "Expected intersection with left bottom corner");
let (a, b) = bbox.intersect_line(&Point { x: 0.0, y: 0.0 }, &Point { x: -1.0, y: -1.0 });
assert_eq!(Some(Point { x: 1.0, y: 1.0 }), a, "Expected intersection with top right corner");
assert_eq!(Some(Point { x: -1.0, y: -1.0 }), b, "Expected intersection with left bottom corner");
let (a, b) = bbox.intersect_line(&Point { x: 0.5, y: 0.5 }, &Point { x: 0.4, y: 0.6 });
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), a, "Expected intersection with middle of the right edge");
assert_eq!(Some(Point { x: 0.0, y: 1.0 }), b, "Expected intersection with middle of the top edge");
let (a, b) = bbox.intersect_line(&Point { x: -0.5, y: 0.5 }, &Point { x: -0.4, y: 0.6 });
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), a, "Expected intersection with middle of the left edge");
assert_eq!(Some(Point { x: 0.0, y: 1.0 }), b, "Expected intersection with middle of the top edge");
let (a, b) = bbox.intersect_line(&Point { x: -0.5, y: -0.5 }, &Point { x: -0.4, y: -0.6 });
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), a, "Expected intersection with middle of the left edge");
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), b, "Expected intersection with middle of the bottom edge");
let (a, b) = bbox.intersect_line(&Point { x: 0.5, y: -0.5 }, &Point { x: 0.4, y: -0.6 });
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), a, "Expected intersection with middle of the right edge");
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), b, "Expected intersection with middle of the bottom edge");
}
#[test]
fn project_ray_tests() {
let bbox = BoundingBox::new_centered_square(2.0);
let (a, b) = bbox.project_ray(&Point { x: 2.0, y: 0.0 }, &Point { x: -0.1, y: 0.0 });
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), a, "Expected to hit right side first");
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), b, "And then hit the left side");
let (a, b) = bbox.project_ray(&Point { x: 0.9, y: 0.0 }, &Point { x: -0.1, y: 0.0 });
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), a, "Expected to hit left side first");
assert_eq!(None, b, "and only that");
let (a, b) = bbox.project_ray(&Point { x: -0.9, y: 0.0 }, &Point { x: 0.1, y: 0.0 });
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), a, "Expected to hit right side first");
assert_eq!(None, b, "and only that");
let (a, b) = bbox.project_ray(&Point { x: -2.0, y: 0.0 }, &Point { x: 0.1, y: 0.0 });
assert_eq!(Some(Point { x: -1.0, y: 0.0 }), a, "Expected to hit left side first");
assert_eq!(Some(Point { x: 1.0, y: 0.0 }), b, "And then hit the right side");
let (a, b) = bbox.project_ray(&Point { x: 0.0, y: 3.0 }, &Point { x: 0.0, y: -10.0 });
assert_eq!(Some(Point { x: 0.0, y: 1.0 }), a, "Expected to hit top side first");
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), b, "And then hit the bottom side");
let (a, b) = bbox.project_ray(&Point { x: 0.0, y: 0.5 }, &Point { x: 0.0, y: -10.0 });
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), a, "Expected to hit bottom side first");
assert_eq!(None, b, "and only that");
let (a, b) = bbox.project_ray(&Point { x: 0.0, y: -0.5 }, &Point { x: 0.0, y: 0.2 });
assert_eq!(Some(Point { x: 0.0, y: 1.0 }), a, "Expected to hit top side first");
assert_eq!(None, b, "and only that");
let (a, b) = bbox.project_ray(&Point { x: 0.0, y: 0.5 }, &Point { x: 0.0, y: -10.0 });
assert_eq!(Some(Point { x: 0.0, y: -1.0 }), a, "Expected to hit bottom side first");
assert_eq!(None, b, "and only that");
let c = -0.8;
let d = 1.0;
let (a, b) = bbox.project_ray(&line(2.0, c, d), &direction(&line(-20.0, c, d), &line(2.0, c, d)));
assert_close_enough(line(1.0, c, d), a, "Expected to hit left side first");
assert_close_enough(line(0.0, c, d), b, "And then top side");
let (a, b) = bbox.project_ray(&Point { x: -0.5, y: 0.0 }, &Point { x: -1.0, y: 0.8 });
assert_eq!(Some(Point { x: -1.0, y: 0.4 }), a, "Expected to hit bottom side first");
assert_eq!(None, b, "No collision");
let (a, b) = bbox.project_ray(&Point { x: -10.0, y: 0.0 }, &Point { x: 1.0, y: 0.8 });
assert_eq!(None, a, "No collision");
assert_eq!(None, b, "No collision");
}
}