1use crate::error::GeometryResult;
28use crate::resource::point::CartesianPoint;
29use crate::resource::resolve;
30use crate::slots::Slots;
31use ifc_model::{Entity, EntityId, Model};
32
33pub(crate) mod slot {
38 pub const CORNER: usize = 0;
40 pub const X_DIM: usize = 1;
42 pub const Y_DIM: usize = 2;
44 pub const Z_DIM: usize = 3;
46}
47
48#[derive(Debug, Clone, Copy)]
50pub struct BoundingBox<'m> {
51 slots: Slots<'m>,
52}
53
54impl<'m> BoundingBox<'m> {
55 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
57 Self {
58 slots: Slots::new(id, entity),
59 }
60 }
61
62 pub fn id(&self) -> EntityId {
64 self.slots.id()
65 }
66
67 pub fn corner(&self) -> GeometryResult<EntityId> {
72 self.slots.req_ref(slot::CORNER, "Corner")
73 }
74
75 pub fn corner_point<'v>(&self, model: &'v Model) -> GeometryResult<CartesianPoint<'v>> {
80 resolve::cartesian_point(model, self.id(), self.corner()?)
81 }
82
83 pub fn x_dim(&self) -> GeometryResult<f64> {
85 self.slots.req_f64(slot::X_DIM, "XDim")
86 }
87
88 pub fn y_dim(&self) -> GeometryResult<f64> {
90 self.slots.req_f64(slot::Y_DIM, "YDim")
91 }
92
93 pub fn z_dim(&self) -> GeometryResult<f64> {
95 self.slots.req_f64(slot::Z_DIM, "ZDim")
96 }
97
98 pub fn dimensions(&self) -> GeometryResult<[f64; 3]> {
100 Ok([self.x_dim()?, self.y_dim()?, self.z_dim()?])
101 }
102
103 pub fn checked_dimensions(&self) -> GeometryResult<[f64; 3]> {
108 let dims = self.dimensions()?;
109 for (axis, value) in ["XDim", "YDim", "ZDim"].iter().zip(dims) {
110 if !matches!(value.partial_cmp(&0.0), Some(std::cmp::Ordering::Greater)) {
114 return Err(self
115 .slots
116 .degenerate(format!("{axis} must be positive, found {value}")));
117 }
118 }
119 Ok(dims)
120 }
121
122 pub fn max_corner_local(&self, corner: [f64; 3]) -> GeometryResult<[f64; 3]> {
129 let [x, y, z] = self.dimensions()?;
130 Ok([corner[0] + x, corner[1] + y, corner[2] + z])
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::solid::testkit::{entity, n, r};
138
139 fn bbox(x: f64, y: f64, z: f64) -> Entity {
140 entity("IFCBOUNDINGBOX", vec![r(10), n(x), n(y), n(z)])
141 }
142
143 #[test]
144 fn corner_precedes_the_three_extents_in_slot_order() {
145 let e = bbox(1.0, 2.0, 3.0);
146 let view = BoundingBox::new(EntityId(1), &e);
147 assert_eq!(view.corner().unwrap(), EntityId(10));
148 assert_eq!(view.dimensions().unwrap(), [1.0, 2.0, 3.0]);
149 }
150
151 #[test]
152 fn the_corner_resolves_to_its_point_and_feeds_the_max_corner() {
153 use crate::solid::testkit::{list, model};
154 let point = entity(
155 "IFCCARTESIANPOINT",
156 vec![list(vec![n(1.0), n(2.0), n(3.0)])],
157 );
158 let m = model(vec![(10, point)]);
159 let e = bbox(1.0, 1.0, 1.0);
160 let view = BoundingBox::new(EntityId(1), &e);
161 let corner = view.corner_point(&m).unwrap().coordinates_3d().unwrap();
162 assert_eq!(view.max_corner_local(corner).unwrap(), [2.0, 3.0, 4.0]);
163 }
164
165 #[test]
166 fn a_corner_that_is_not_a_point_or_is_dangling_is_a_typed_error() {
167 use crate::solid::testkit::model;
168 let m = model(vec![(10, entity("IFCDIRECTION", vec![]))]);
169 let e = bbox(1.0, 1.0, 1.0);
170 let view = BoundingBox::new(EntityId(1), &e);
171 assert!(matches!(
172 view.corner_point(&m).unwrap_err(),
173 crate::GeometryError::WrongEntityType {
174 entity: EntityId(10),
175 ..
176 }
177 ));
178 let err = view.corner_point(&Model::new()).unwrap_err();
179 assert_eq!(err.entity(), Some(EntityId(1)));
180 }
181
182 #[test]
185 fn the_corner_is_the_minimum_and_extents_grow_positively() {
186 let e = bbox(2.0, 4.0, 6.0);
187 let view = BoundingBox::new(EntityId(1), &e);
188 assert_eq!(
189 view.max_corner_local([10.0, 20.0, 30.0]).unwrap(),
190 [12.0, 24.0, 36.0]
191 );
192 assert_eq!(
194 view.max_corner_local([-1.0, -1.0, -1.0]).unwrap(),
195 [1.0, 3.0, 5.0]
196 );
197 }
198
199 #[test]
200 fn a_non_positive_extent_is_rejected_as_degenerate() {
201 for e in [
202 bbox(0.0, 1.0, 1.0),
203 bbox(1.0, -2.0, 1.0),
204 bbox(1.0, 1.0, 0.0),
205 ] {
206 let view = BoundingBox::new(EntityId(6), &e);
207 let err = view.checked_dimensions().unwrap_err();
208 assert_eq!(err.entity(), Some(EntityId(6)));
209 assert!(
210 view.dimensions().is_ok(),
211 "raw dimensions stay readable for inspection"
212 );
213 }
214 assert_eq!(
215 BoundingBox::new(EntityId(6), &bbox(1.0, 2.0, 3.0))
216 .checked_dimensions()
217 .unwrap(),
218 [1.0, 2.0, 3.0]
219 );
220 }
221
222 #[test]
223 fn a_box_missing_an_extent_names_the_attribute() {
224 let e = entity("IFCBOUNDINGBOX", vec![r(10), n(1.0), n(2.0)]);
225 let err = BoundingBox::new(EntityId(3), &e).z_dim().unwrap_err();
226 assert!(err.to_string().contains("ZDim"));
227 }
228}