1use crate::error::{GeometryError, GeometryResult};
29use crate::slots::Slots;
30use ifc_model::{Entity, EntityId, Model};
31
32pub(crate) mod slot {
34 pub mod direction {
37 pub const DIRECTION_RATIOS: usize = 0;
39 }
40
41 pub mod vector {
43 pub const ORIENTATION: usize = 0;
45 pub const MAGNITUDE: usize = 1;
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct Direction<'m> {
53 slots: Slots<'m>,
54}
55
56impl<'m> Direction<'m> {
57 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
59 Self {
60 slots: Slots::new(id, entity),
61 }
62 }
63
64 pub fn id(&self) -> EntityId {
66 self.slots.id()
67 }
68
69 pub fn ratios(&self) -> GeometryResult<Vec<f64>> {
71 self.slots
72 .req_f64_list(slot::direction::DIRECTION_RATIOS, "DirectionRatios")
73 }
74
75 pub fn dimension(&self) -> GeometryResult<usize> {
77 let n = self.ratios()?.len();
78 match n {
79 2 | 3 => Ok(n),
80 other => Err(self.slots.degenerate(format!(
81 "DirectionRatios has {other} entries, expected 2 or 3"
82 ))),
83 }
84 }
85
86 pub fn ratios_3d(&self) -> GeometryResult<[f64; 3]> {
93 let r = self.ratios()?;
94 match r.len() {
95 2 => Ok([r[0], r[1], 0.0]),
96 3 => Ok([r[0], r[1], r[2]]),
97 other => Err(self.slots.degenerate(format!(
98 "DirectionRatios has {other} entries, expected 2 or 3"
99 ))),
100 }
101 }
102
103 pub fn unit(&self) -> GeometryResult<[f64; 3]> {
109 let v = self.ratios_3d()?;
110 let scale = v
111 .iter()
112 .map(|component| component.abs())
113 .fold(0.0, f64::max);
114 if scale == 0.0 {
115 return Err(self
116 .slots
117 .degenerate("DirectionRatios are all zero, so there is no direction"));
118 }
119 if !scale.is_finite() {
120 return Err(self
121 .slots
122 .degenerate("DirectionRatios must be finite before normalization"));
123 }
124
125 let scaled = [v[0] / scale, v[1] / scale, v[2] / scale];
128 let length = (scaled[0] * scaled[0] + scaled[1] * scaled[1] + scaled[2] * scaled[2]).sqrt();
129 let unit = [scaled[0] / length, scaled[1] / length, scaled[2] / length];
130 if !unit.iter().all(|component| component.is_finite()) {
131 return Err(self
132 .slots
133 .degenerate("DirectionRatios did not derive a finite unit direction"));
134 }
135 Ok(unit)
136 }
137}
138
139#[derive(Debug, Clone, Copy)]
146pub struct Vector<'m> {
147 slots: Slots<'m>,
148}
149
150impl<'m> Vector<'m> {
151 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
153 Self {
154 slots: Slots::new(id, entity),
155 }
156 }
157
158 pub fn id(&self) -> EntityId {
160 self.slots.id()
161 }
162
163 pub fn orientation_ref(&self) -> GeometryResult<EntityId> {
165 self.slots.req_ref(slot::vector::ORIENTATION, "Orientation")
166 }
167
168 pub fn magnitude(&self) -> GeometryResult<f64> {
170 self.slots.req_f64(slot::vector::MAGNITUDE, "Magnitude")
171 }
172
173 pub fn components(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
175 let magnitude = self.magnitude()?;
176 let unit = resolve_unit(model, self.id(), self.orientation_ref()?)?;
177 Ok([
178 unit[0] * magnitude,
179 unit[1] * magnitude,
180 unit[2] * magnitude,
181 ])
182 }
183}
184
185pub fn resolve_unit(model: &Model, referrer: EntityId, id: EntityId) -> GeometryResult<[f64; 3]> {
190 direction_view(model, referrer, id)?.unit()
191}
192
193pub fn resolve_ratios_3d(
198 model: &Model,
199 referrer: EntityId,
200 id: EntityId,
201) -> GeometryResult<[f64; 3]> {
202 direction_view(model, referrer, id)?.ratios_3d()
203}
204
205fn direction_view<'m>(
207 model: &'m Model,
208 referrer: EntityId,
209 id: EntityId,
210) -> GeometryResult<Direction<'m>> {
211 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
212 referrer,
213 missing: id,
214 })?;
215 if !entity.is_type("IFCDIRECTION") {
216 return Err(GeometryError::WrongEntityType {
217 entity: id,
218 actual: entity.type_name.to_string(),
219 expected: "IfcDirection",
220 });
221 }
222 Ok(Direction::new(id, entity))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use ifc_model::Value;
229
230 fn direction_entity(values: &[f64]) -> Entity {
231 Entity::new(
232 "IFCDIRECTION",
233 vec![Value::List(
234 values.iter().copied().map(Value::Real).collect(),
235 )],
236 )
237 }
238
239 fn close(a: [f64; 3], b: [f64; 3]) -> bool {
240 a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-12)
241 }
242
243 #[test]
245 fn direction_ratios_are_returned_unnormalized() {
246 let e = direction_entity(&[3.0, 4.0, 0.0]);
247 let d = Direction::new(EntityId(1), &e);
248 assert_eq!(d.ratios().unwrap(), vec![3.0, 4.0, 0.0]);
249 assert!(close(d.unit().unwrap(), [0.6, 0.8, 0.0]));
250 }
251
252 #[test]
253 fn two_dimensional_directions_report_their_dimension() {
254 let e = direction_entity(&[1.0, 0.0]);
255 let d = Direction::new(EntityId(1), &e);
256 assert_eq!(d.dimension().unwrap(), 2);
257 assert_eq!(d.ratios_3d().unwrap(), [1.0, 0.0, 0.0]);
258 }
259
260 #[test]
263 fn zero_length_direction_is_degenerate_rather_than_nan() {
264 let e = direction_entity(&[0.0, 0.0, 0.0]);
265 let d = Direction::new(EntityId(3), &e);
266 let err = d.unit().unwrap_err();
267 assert!(matches!(err, GeometryError::Degenerate { .. }));
268 assert!(err.to_string().contains("#3"), "got: {err}");
269 }
270
271 #[test]
273 fn finite_extreme_ratios_normalize_without_underflow_or_overflow() {
274 let tiny = direction_entity(&[1e-300, 0.0, 0.0]);
275 assert_eq!(
276 Direction::new(EntityId(1), &tiny).unit().unwrap(),
277 [1.0, 0.0, 0.0]
278 );
279
280 let huge = direction_entity(&[f64::MAX, f64::MAX, 0.0]);
281 let unit = Direction::new(EntityId(2), &huge).unit().unwrap();
282 let expected = 1.0 / 2.0_f64.sqrt();
283 assert!(close(unit, [expected, expected, 0.0]));
284 }
285
286 #[test]
287 fn a_single_ratio_is_degenerate() {
288 let e = direction_entity(&[1.0]);
289 let d = Direction::new(EntityId(1), &e);
290 assert!(d.dimension().is_err());
291 assert!(d.ratios_3d().is_err());
292 }
293
294 #[test]
295 fn missing_direction_ratios_names_the_entity_and_attribute() {
296 let e = Entity::new("IFCDIRECTION", vec![]);
297 let err = Direction::new(EntityId(8), &e).ratios().unwrap_err();
298 assert!(err.to_string().contains("#8"), "got: {err}");
299 assert!(err.to_string().contains("DirectionRatios"), "got: {err}");
300 }
301
302 #[test]
303 fn vector_components_are_orientation_times_magnitude() {
304 let mut model = Model::new();
305 model.insert(EntityId(1), direction_entity(&[3.0, 4.0, 0.0]));
306 let v = Entity::new(
307 "IFCVECTOR",
308 vec![
309 Value::Ref(EntityId(1)),
310 Value::Typed {
311 type_name: "IFCLENGTHMEASURE".into(),
312 value: Box::new(Value::Real(10.0)),
313 },
314 ],
315 );
316 let view = Vector::new(EntityId(2), &v);
317 assert_eq!(view.magnitude().unwrap(), 10.0);
318 assert!(close(view.components(&model).unwrap(), [6.0, 8.0, 0.0]));
319 }
320
321 #[test]
324 fn zero_magnitude_vector_is_legal_and_yields_the_zero_vector() {
325 let mut model = Model::new();
326 model.insert(EntityId(1), direction_entity(&[0.0, 0.0, 1.0]));
327 let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(0.0)]);
328 assert_eq!(
329 Vector::new(EntityId(2), &v).components(&model).unwrap(),
330 [0.0, 0.0, 0.0]
331 );
332 }
333
334 #[test]
335 fn a_vector_pointing_at_a_non_direction_reports_the_wrong_type() {
336 let mut model = Model::new();
337 model.insert(EntityId(1), Entity::new("IFCCARTESIANPOINT", vec![]));
338 let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(1.0)]);
339 let err = Vector::new(EntityId(2), &v).components(&model).unwrap_err();
340 assert!(matches!(
341 err,
342 GeometryError::WrongEntityType {
343 expected: "IfcDirection",
344 ..
345 }
346 ));
347 }
348
349 #[test]
350 fn a_dangling_direction_reference_names_the_referrer() {
351 let model = Model::new();
352 let err = resolve_unit(&model, EntityId(5), EntityId(99)).unwrap_err();
353 assert_eq!(err.entity(), Some(EntityId(5)));
354 }
355
356 #[test]
357 fn resolving_ratios_keeps_them_unnormalized() {
358 let mut model = Model::new();
359 model.insert(EntityId(1), direction_entity(&[0.0, 2.0]));
360 assert_eq!(
361 resolve_ratios_3d(&model, EntityId(2), EntityId(1)).unwrap(),
362 [0.0, 2.0, 0.0]
363 );
364 }
365}