use crate::core::affine::Affine2D;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bbox {
pub x0: f64,
pub y0: f64,
pub x1: f64,
pub y1: f64,
}
impl Bbox {
#[must_use]
pub const fn from_extents(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
Self { x0, y0, x1, y1 }
}
#[must_use]
pub const fn from_bounds(x: f64, y: f64, width: f64, height: f64) -> Self {
Self::from_extents(x, y, x + width, y + height)
}
#[must_use]
pub const fn unit() -> Self {
Self::from_extents(0.0, 0.0, 1.0, 1.0)
}
#[must_use]
pub const fn null() -> Self {
Self::from_extents(
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
)
}
#[must_use]
pub fn xmin(&self) -> f64 {
self.x0.min(self.x1)
}
#[must_use]
pub fn xmax(&self) -> f64 {
self.x0.max(self.x1)
}
#[must_use]
pub fn ymin(&self) -> f64 {
self.y0.min(self.y1)
}
#[must_use]
pub fn ymax(&self) -> f64 {
self.y0.max(self.y1)
}
#[must_use]
pub fn width(&self) -> f64 {
self.x1 - self.x0
}
#[must_use]
pub fn height(&self) -> f64 {
self.y1 - self.y0
}
#[must_use]
pub fn size(&self) -> (f64, f64) {
(self.width(), self.height())
}
#[must_use]
pub const fn p0(&self) -> (f64, f64) {
(self.x0, self.y0)
}
#[must_use]
pub const fn p1(&self) -> (f64, f64) {
(self.x1, self.y1)
}
#[must_use]
pub fn contains_point(&self, x: f64, y: f64) -> bool {
x >= self.xmin() && x <= self.xmax() && y >= self.ymin() && y <= self.ymax()
}
#[must_use]
pub fn union(&self, other: &Bbox) -> Bbox {
Bbox::from_extents(
self.xmin().min(other.xmin()),
self.ymin().min(other.ymin()),
self.xmax().max(other.xmax()),
self.ymax().max(other.ymax()),
)
}
#[must_use]
pub fn intersection(&self, other: &Bbox) -> Option<Bbox> {
let x0 = self.xmin().max(other.xmin());
let x1 = self.xmax().min(other.xmax());
let y0 = self.ymin().max(other.ymin());
let y1 = self.ymax().min(other.ymax());
if x0 <= x1 && y0 <= y1 {
Some(Bbox::from_extents(x0, y0, x1, y1))
} else {
None
}
}
#[must_use]
pub fn expanded(&self, sw: f64, sh: f64) -> Bbox {
let w = self.width();
let h = self.height();
let dw = (sw * w - w) / 2.0;
let dh = (sh * h - h) / 2.0;
Bbox::from_extents(self.x0 - dw, self.y0 - dh, self.x1 + dw, self.y1 + dh)
}
#[must_use]
pub fn padded(&self, p: f64) -> Bbox {
Bbox::from_extents(self.x0 - p, self.y0 - p, self.x1 + p, self.y1 + p)
}
#[must_use]
pub fn transformed(&self, transform: &Affine2D) -> Bbox {
let corners = [
transform.transform_point((self.x0, self.y0)),
transform.transform_point((self.x0, self.y1)),
transform.transform_point((self.x1, self.y0)),
transform.transform_point((self.x1, self.y1)),
];
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymax = f64::NEG_INFINITY;
for (x, y) in corners {
xmin = xmin.min(x);
ymin = ymin.min(y);
xmax = xmax.max(x);
ymax = ymax.max(y);
}
Bbox::from_extents(xmin, ymin, xmax, ymax)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-12, "expected {b}, got {a}");
}
#[test]
fn from_bounds_and_extents() {
let b = Bbox::from_bounds(1.0, 2.0, 3.0, 4.0);
approx(b.x0, 1.0);
approx(b.y0, 2.0);
approx(b.x1, 4.0);
approx(b.y1, 6.0);
approx(b.width(), 3.0);
approx(b.height(), 4.0);
}
#[test]
fn unit_box() {
let u = Bbox::unit();
assert_eq!(u, Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
assert_eq!(u.size(), (1.0, 1.0));
assert_eq!(u.p0(), (0.0, 0.0));
assert_eq!(u.p1(), (1.0, 1.0));
}
#[test]
fn null_sentinel_corners() {
let n = Bbox::null();
assert_eq!(n.x0, f64::INFINITY);
assert_eq!(n.y0, f64::INFINITY);
assert_eq!(n.x1, f64::NEG_INFINITY);
assert_eq!(n.y1, f64::NEG_INFINITY);
}
#[test]
fn normalization_with_swapped_corners() {
let b = Bbox::from_extents(5.0, 8.0, 1.0, 2.0);
approx(b.xmin(), 1.0);
approx(b.xmax(), 5.0);
approx(b.ymin(), 2.0);
approx(b.ymax(), 8.0);
approx(b.width(), -4.0);
approx(b.height(), -6.0);
assert!(b.contains_point(3.0, 5.0));
assert!(!b.contains_point(0.0, 5.0));
}
#[test]
fn contains_point_edges() {
let b = Bbox::unit();
assert!(b.contains_point(0.0, 0.0));
assert!(b.contains_point(1.0, 1.0));
assert!(b.contains_point(0.5, 0.5));
assert!(!b.contains_point(1.5, 0.5));
}
#[test]
fn union_of_two() {
let a = Bbox::from_extents(0.0, 0.0, 2.0, 2.0);
let b = Bbox::from_extents(1.0, 1.0, 3.0, 4.0);
let u = a.union(&b);
assert_eq!(u, Bbox::from_extents(0.0, 0.0, 3.0, 4.0));
}
#[test]
fn intersection_overlap_and_disjoint() {
let a = Bbox::from_extents(0.0, 0.0, 2.0, 2.0);
let b = Bbox::from_extents(1.0, 1.0, 3.0, 3.0);
assert_eq!(
a.intersection(&b),
Some(Bbox::from_extents(1.0, 1.0, 2.0, 2.0))
);
let c = Bbox::from_extents(5.0, 5.0, 6.0, 6.0);
assert_eq!(a.intersection(&c), None);
}
#[test]
fn expanded_about_center() {
let b = Bbox::from_extents(0.0, 0.0, 2.0, 2.0);
let e = b.expanded(2.0, 2.0);
assert_eq!(e, Bbox::from_extents(-1.0, -1.0, 3.0, 3.0));
}
#[test]
fn padded_all_sides() {
let b = Bbox::from_extents(1.0, 1.0, 2.0, 2.0);
assert_eq!(b.padded(0.5), Bbox::from_extents(0.5, 0.5, 2.5, 2.5));
}
#[test]
fn transformed_by_translation() {
let b = Bbox::unit();
let t = Affine2D::from_translation(2.0, 3.0);
let out = b.transformed(&t);
assert_eq!(out, Bbox::from_extents(2.0, 3.0, 3.0, 4.0));
}
#[test]
fn transformed_by_rotation() {
let b = Bbox::unit();
let r = Affine2D::from_rotation_deg(90.0);
let out = b.transformed(&r);
approx(out.xmin(), -1.0);
approx(out.xmax(), 0.0);
approx(out.ymin(), 0.0);
approx(out.ymax(), 1.0);
}
}