use crate::error::GeometryResult;
use crate::resource::point::CartesianPoint;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub const CORNER: usize = 0;
pub const X_DIM: usize = 1;
pub const Y_DIM: usize = 2;
pub const Z_DIM: usize = 3;
}
#[derive(Debug, Clone, Copy)]
pub struct BoundingBox<'m> {
slots: Slots<'m>,
}
impl<'m> BoundingBox<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn corner(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::CORNER, "Corner")
}
pub fn corner_point<'v>(&self, model: &'v Model) -> GeometryResult<CartesianPoint<'v>> {
resolve::cartesian_point(model, self.id(), self.corner()?)
}
pub fn x_dim(&self) -> GeometryResult<f64> {
self.slots.req_f64(slot::X_DIM, "XDim")
}
pub fn y_dim(&self) -> GeometryResult<f64> {
self.slots.req_f64(slot::Y_DIM, "YDim")
}
pub fn z_dim(&self) -> GeometryResult<f64> {
self.slots.req_f64(slot::Z_DIM, "ZDim")
}
pub fn dimensions(&self) -> GeometryResult<[f64; 3]> {
Ok([self.x_dim()?, self.y_dim()?, self.z_dim()?])
}
pub fn checked_dimensions(&self) -> GeometryResult<[f64; 3]> {
let dims = self.dimensions()?;
for (axis, value) in ["XDim", "YDim", "ZDim"].iter().zip(dims) {
if !matches!(value.partial_cmp(&0.0), Some(std::cmp::Ordering::Greater)) {
return Err(self
.slots
.degenerate(format!("{axis} must be positive, found {value}")));
}
}
Ok(dims)
}
pub fn max_corner_local(&self, corner: [f64; 3]) -> GeometryResult<[f64; 3]> {
let [x, y, z] = self.dimensions()?;
Ok([corner[0] + x, corner[1] + y, corner[2] + z])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, n, r};
fn bbox(x: f64, y: f64, z: f64) -> Entity {
entity("IFCBOUNDINGBOX", vec![r(10), n(x), n(y), n(z)])
}
#[test]
fn corner_precedes_the_three_extents_in_slot_order() {
let e = bbox(1.0, 2.0, 3.0);
let view = BoundingBox::new(EntityId(1), &e);
assert_eq!(view.corner().unwrap(), EntityId(10));
assert_eq!(view.dimensions().unwrap(), [1.0, 2.0, 3.0]);
}
#[test]
fn the_corner_resolves_to_its_point_and_feeds_the_max_corner() {
use crate::solid::testkit::{list, model};
let point = entity(
"IFCCARTESIANPOINT",
vec![list(vec![n(1.0), n(2.0), n(3.0)])],
);
let m = model(vec![(10, point)]);
let e = bbox(1.0, 1.0, 1.0);
let view = BoundingBox::new(EntityId(1), &e);
let corner = view.corner_point(&m).unwrap().coordinates_3d().unwrap();
assert_eq!(view.max_corner_local(corner).unwrap(), [2.0, 3.0, 4.0]);
}
#[test]
fn a_corner_that_is_not_a_point_or_is_dangling_is_a_typed_error() {
use crate::solid::testkit::model;
let m = model(vec![(10, entity("IFCDIRECTION", vec![]))]);
let e = bbox(1.0, 1.0, 1.0);
let view = BoundingBox::new(EntityId(1), &e);
assert!(matches!(
view.corner_point(&m).unwrap_err(),
crate::GeometryError::WrongEntityType {
entity: EntityId(10),
..
}
));
let err = view.corner_point(&Model::new()).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(1)));
}
#[test]
fn the_corner_is_the_minimum_and_extents_grow_positively() {
let e = bbox(2.0, 4.0, 6.0);
let view = BoundingBox::new(EntityId(1), &e);
assert_eq!(
view.max_corner_local([10.0, 20.0, 30.0]).unwrap(),
[12.0, 24.0, 36.0]
);
assert_eq!(
view.max_corner_local([-1.0, -1.0, -1.0]).unwrap(),
[1.0, 3.0, 5.0]
);
}
#[test]
fn a_non_positive_extent_is_rejected_as_degenerate() {
for e in [
bbox(0.0, 1.0, 1.0),
bbox(1.0, -2.0, 1.0),
bbox(1.0, 1.0, 0.0),
] {
let view = BoundingBox::new(EntityId(6), &e);
let err = view.checked_dimensions().unwrap_err();
assert_eq!(err.entity(), Some(EntityId(6)));
assert!(
view.dimensions().is_ok(),
"raw dimensions stay readable for inspection"
);
}
assert_eq!(
BoundingBox::new(EntityId(6), &bbox(1.0, 2.0, 3.0))
.checked_dimensions()
.unwrap(),
[1.0, 2.0, 3.0]
);
}
#[test]
fn a_box_missing_an_extent_names_the_attribute() {
let e = entity("IFCBOUNDINGBOX", vec![r(10), n(1.0), n(2.0)]);
let err = BoundingBox::new(EntityId(3), &e).z_dim().unwrap_err();
assert!(err.to_string().contains("ZDim"));
}
}