///|
/// An axis-aligned rectangle in data or display coordinates.
pub struct BBox {
x0 : Double
y0 : Double
x1 : Double
y1 : Double
}
///|
/// Construct a bounding box from its two corners.
pub fn bbox(x0 : Double, y0 : Double, x1 : Double, y1 : Double) -> BBox {
{ x0, y0, x1, y1, }
}
///|
/// Construct a box from an origin and its width and height.
pub fn bbox_from_bounds(
x : Double,
y : Double,
width : Double,
height : Double,
) -> BBox {
{ x0: x, y0: y, x1: x + width, y1: y + height, }
}
///|
/// Construct the unit box from `(0, 0)` to `(1, 1)`.
pub fn unit_bbox() -> BBox {
bbox(0.0, 0.0, 1.0, 1.0)
}
///|
/// Construct an empty box whose extents can be expanded by union operations.
pub fn null_bbox() -> BBox {
bbox(
@double.infinity, @double.infinity, @double.neg_infinity, @double.neg_infinity,
)
}
///|
pub fn BBox::x0(self : BBox) -> Double {
self.x0
}
///|
pub fn BBox::y0(self : BBox) -> Double {
self.y0
}
///|
pub fn BBox::x1(self : BBox) -> Double {
self.x1
}
///|
pub fn BBox::y1(self : BBox) -> Double {
self.y1
}
///|
pub fn BBox::xmin(self : BBox) -> Double {
Double::min(self.x0, self.x1)
}
///|
pub fn BBox::xmax(self : BBox) -> Double {
Double::max(self.x0, self.x1)
}
///|
pub fn BBox::ymin(self : BBox) -> Double {
Double::min(self.y0, self.y1)
}
///|
pub fn BBox::ymax(self : BBox) -> Double {
Double::max(self.y0, self.y1)
}
///|
pub fn BBox::width(self : BBox) -> Double {
self.x1 - self.x0
}
///|
pub fn BBox::height(self : BBox) -> Double {
self.y1 - self.y0
}
///|
/// Return `(x0, y0, width, height)` using the box's original orientation.
pub fn BBox::bounds(self : BBox) -> (Double, Double, Double, Double) {
(self.x0, self.y0, self.width(), self.height())
}
///|
/// Return the signed `(width, height)` pair.
pub fn BBox::size(self : BBox) -> (Double, Double) {
(self.width(), self.height())
}
///|
/// Return the lower-left coordinate after normalizing each axis.
pub fn BBox::min_point(self : BBox) -> @core.Point {
@core.point(self.xmin(), self.ymin())
}
///|
/// Return the upper-right coordinate after normalizing each axis.
pub fn BBox::max_point(self : BBox) -> @core.Point {
@core.point(self.xmax(), self.ymax())
}
///|
/// Return the original x interval, preserving its orientation.
pub fn BBox::interval_x(self : BBox) -> (Double, Double) {
(self.x0, self.x1)
}
///|
/// Return the original y interval, preserving its orientation.
pub fn BBox::interval_y(self : BBox) -> (Double, Double) {
(self.y0, self.y1)
}
///|
pub fn BBox::is_finite(self : BBox) -> Bool {
!self.x0.is_nan() &&
!self.x0.is_inf() &&
!self.y0.is_nan() &&
!self.y0.is_inf() &&
!self.x1.is_nan() &&
!self.x1.is_inf() &&
!self.y1.is_nan() &&
!self.y1.is_inf()
}
///|
pub fn BBox::corners(self : BBox) -> Array[@core.Point] {
[
@core.point(self.x0, self.y0),
@core.point(self.x0, self.y1),
@core.point(self.x1, self.y1),
@core.point(self.x1, self.y0),
]
}
///|
pub fn BBox::contains(self : BBox, x : Double, y : Double) -> Bool {
self.contains_x(x) && self.contains_y(y)
}
///|
/// Return whether x lies in the closed interval, including reversed bounds.
pub fn BBox::contains_x(self : BBox, x : Double) -> Bool {
(self.x0 <= x && x <= self.x1) || (self.x1 <= x && x <= self.x0)
}
///|
/// Return whether y lies in the closed interval, including reversed bounds.
pub fn BBox::contains_y(self : BBox, y : Double) -> Bool {
(self.y0 <= y && y <= self.y1) || (self.y1 <= y && y <= self.y0)
}
///|
/// Return whether x lies strictly inside the interval.
pub fn BBox::fully_contains_x(self : BBox, x : Double) -> Bool {
(self.x0 < x && x < self.x1) || (self.x1 < x && x < self.x0)
}
///|
/// Return whether y lies strictly inside the interval.
pub fn BBox::fully_contains_y(self : BBox, y : Double) -> Bool {
(self.y0 < y && y < self.y1) || (self.y1 < y && y < self.y0)
}
///|
/// Return whether a point lies strictly inside the box.
pub fn BBox::fully_contains(self : BBox, x : Double, y : Double) -> Bool {
self.fully_contains_x(x) && self.fully_contains_y(y)
}
///|
pub fn BBox::contains_point(self : BBox, point : @core.Point) -> Bool {
self.contains(point.x(), point.y())
}
///|
pub fn BBox::overlaps(self : BBox, other : BBox) -> Bool {
self.xmin() <= other.xmax() &&
self.xmax() >= other.xmin() &&
self.ymin() <= other.ymax() &&
self.ymax() >= other.ymin()
}
///|
/// Return whether the interiors of two boxes overlap.
pub fn BBox::fully_overlaps(self : BBox, other : BBox) -> Bool {
self.xmin() < other.xmax() &&
self.xmax() > other.xmin() &&
self.ymin() < other.ymax() &&
self.ymax() > other.ymin()
}
///|
pub fn BBox::intersection(self : BBox, other : BBox) -> BBox? {
let x0 = Double::max(self.xmin(), other.xmin())
let y0 = Double::max(self.ymin(), other.ymin())
let x1 = Double::min(self.xmax(), other.xmax())
let y1 = Double::min(self.ymax(), other.ymax())
if x0 <= x1 && y0 <= y1 {
Some(bbox(x0, y0, x1, y1))
} else {
None
}
}
///|
pub fn BBox::union(self : BBox, other : BBox) -> BBox {
bbox(
Double::min(self.xmin(), other.xmin()),
Double::min(self.ymin(), other.ymin()),
Double::max(self.xmax(), other.xmax()),
Double::max(self.ymax(), other.ymax()),
)
}
///|
pub fn BBox::translated(self : BBox, x : Double, y : Double) -> BBox {
bbox(self.x0 + x, self.y0 + y, self.x1 + x, self.y1 + y)
}
///|
pub fn BBox::expanded(self : BBox, x_scale : Double, y_scale : Double) -> BBox {
let center_x = (self.x0 + self.x1) / 2.0
let center_y = (self.y0 + self.y1) / 2.0
let half_width = self.width() * x_scale / 2.0
let half_height = self.height() * y_scale / 2.0
bbox(
center_x - half_width,
center_y - half_height,
center_x + half_width,
center_y + half_height,
)
}
///|
/// Shrink or expand from the lower-left corner by axis factors.
pub fn BBox::shrunk(self : BBox, x_scale : Double, y_scale : Double) -> BBox {
bbox(
self.x0,
self.y0,
self.x0 + self.width() * x_scale,
self.y0 + self.height() * y_scale,
)
}
///|
/// Shrink the box to the largest rectangle with the requested physical
/// aspect ratio. The result keeps the original lower-left corner.
pub fn BBox::shrunk_to_aspect(
self : BBox,
box_aspect : Double,
container? : BBox,
fig_aspect~ : Double = 1.0,
) -> BBox {
if box_aspect <= 0.0 || fig_aspect <= 0.0 {
return self
}
let container_box = match container {
Some(value) => value
None => self
}
let container_width = container_box.width()
let container_height = container_box.height()
let desired_height = container_width * box_aspect / fig_aspect
let (width, height) = if desired_height <= container_height {
(container_width, desired_height)
} else {
(container_height * fig_aspect / box_aspect, container_height)
}
bbox(self.x0, self.y0, self.x0 + width, self.y0 + height)
}
///|
pub fn BBox::padded(self : BBox, x : Double, y : Double) -> BBox {
bbox(self.x0 - x, self.y0 - y, self.x1 + x, self.y1 + y)
}
///|
/// Split the box at fractional x positions in `[0, 1]`.
pub fn BBox::split_x(self : BBox, fractions : Array[Double]) -> Array[BBox] {
let result : Array[BBox] = []
let mut previous = 0.0
for fraction in fractions {
result.push(
bbox(
self.x0 + previous * self.width(),
self.y0,
self.x0 + fraction * self.width(),
self.y1,
),
)
previous = fraction
}
result.push(
bbox(self.x0 + previous * self.width(), self.y0, self.x1, self.y1),
)
result
}
///|
/// Split the box at fractional y positions in `[0, 1]`.
pub fn BBox::split_y(self : BBox, fractions : Array[Double]) -> Array[BBox] {
let result : Array[BBox] = []
let mut previous = 0.0
for fraction in fractions {
result.push(
bbox(
self.x0,
self.y0 + previous * self.height(),
self.x1,
self.y0 + fraction * self.height(),
),
)
previous = fraction
}
result.push(
bbox(self.x0, self.y0 + previous * self.height(), self.x1, self.y1),
)
result
}
///|
/// Return an axis-aligned box containing this box after rotation around the
/// origin.
pub fn BBox::rotated(self : BBox, radians : Double) -> BBox {
let transform = rotation(radians)
let points = transform.apply_all(self.corners())
let first = points[0]
let mut x0 = first.x()
let mut x1 = first.x()
let mut y0 = first.y()
let mut y1 = first.y()
for point in points[1:] {
x0 = Double::min(x0, point.x())
x1 = Double::max(x1, point.x())
y0 = Double::min(y0, point.y())
y1 = Double::max(y1, point.y())
}
bbox(x0, y0, x1, y1)
}
///|
/// Transform all four corners and return the enclosing axis-aligned box.
pub fn BBox::transformed(self : BBox, transform : Affine2D) -> BBox {
let points = transform.apply_all(self.corners())
let first = points[0]
let mut x0 = first.x()
let mut x1 = first.x()
let mut y0 = first.y()
let mut y1 = first.y()
for point in points[1:] {
x0 = Double::min(x0, point.x())
x1 = Double::max(x1, point.x())
y0 = Double::min(y0, point.y())
y1 = Double::max(y1, point.y())
}
bbox(x0, y0, x1, y1)
}