1use crate::error::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 crate::resource::resolve::direction(model, referrer, id)
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::error::GeometryError;
218 use ifc_model::Value;
219
220 fn direction_entity(values: &[f64]) -> Entity {
221 Entity::new(
222 "IFCDIRECTION",
223 vec![Value::List(
224 values.iter().copied().map(Value::Real).collect(),
225 )],
226 )
227 }
228
229 fn close(a: [f64; 3], b: [f64; 3]) -> bool {
230 a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-12)
231 }
232
233 #[test]
235 fn direction_ratios_are_returned_unnormalized() {
236 let e = direction_entity(&[3.0, 4.0, 0.0]);
237 let d = Direction::new(EntityId(1), &e);
238 assert_eq!(d.ratios().unwrap(), vec![3.0, 4.0, 0.0]);
239 assert!(close(d.unit().unwrap(), [0.6, 0.8, 0.0]));
240 }
241
242 #[test]
243 fn two_dimensional_directions_report_their_dimension() {
244 let e = direction_entity(&[1.0, 0.0]);
245 let d = Direction::new(EntityId(1), &e);
246 assert_eq!(d.dimension().unwrap(), 2);
247 assert_eq!(d.ratios_3d().unwrap(), [1.0, 0.0, 0.0]);
248 }
249
250 #[test]
253 fn zero_length_direction_is_degenerate_rather_than_nan() {
254 let e = direction_entity(&[0.0, 0.0, 0.0]);
255 let d = Direction::new(EntityId(3), &e);
256 let err = d.unit().unwrap_err();
257 assert!(matches!(err, GeometryError::Degenerate { .. }));
258 assert!(err.to_string().contains("#3"), "got: {err}");
259 }
260
261 #[test]
263 fn finite_extreme_ratios_normalize_without_underflow_or_overflow() {
264 let tiny = direction_entity(&[1e-300, 0.0, 0.0]);
265 assert_eq!(
266 Direction::new(EntityId(1), &tiny).unit().unwrap(),
267 [1.0, 0.0, 0.0]
268 );
269
270 let huge = direction_entity(&[f64::MAX, f64::MAX, 0.0]);
271 let unit = Direction::new(EntityId(2), &huge).unit().unwrap();
272 let expected = 1.0 / 2.0_f64.sqrt();
273 assert!(close(unit, [expected, expected, 0.0]));
274 }
275
276 #[test]
277 fn a_single_ratio_is_degenerate() {
278 let e = direction_entity(&[1.0]);
279 let d = Direction::new(EntityId(1), &e);
280 assert!(d.dimension().is_err());
281 assert!(d.ratios_3d().is_err());
282 }
283
284 #[test]
285 fn missing_direction_ratios_names_the_entity_and_attribute() {
286 let e = Entity::new("IFCDIRECTION", vec![]);
287 let err = Direction::new(EntityId(8), &e).ratios().unwrap_err();
288 assert!(err.to_string().contains("#8"), "got: {err}");
289 assert!(err.to_string().contains("DirectionRatios"), "got: {err}");
290 }
291
292 #[test]
293 fn vector_components_are_orientation_times_magnitude() {
294 let mut model = Model::new();
295 model.insert(EntityId(1), direction_entity(&[3.0, 4.0, 0.0]));
296 let v = Entity::new(
297 "IFCVECTOR",
298 vec![
299 Value::Ref(EntityId(1)),
300 Value::Typed {
301 type_name: "IFCLENGTHMEASURE".into(),
302 value: Box::new(Value::Real(10.0)),
303 },
304 ],
305 );
306 let view = Vector::new(EntityId(2), &v);
307 assert_eq!(view.magnitude().unwrap(), 10.0);
308 assert!(close(view.components(&model).unwrap(), [6.0, 8.0, 0.0]));
309 }
310
311 #[test]
314 fn zero_magnitude_vector_is_legal_and_yields_the_zero_vector() {
315 let mut model = Model::new();
316 model.insert(EntityId(1), direction_entity(&[0.0, 0.0, 1.0]));
317 let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(0.0)]);
318 assert_eq!(
319 Vector::new(EntityId(2), &v).components(&model).unwrap(),
320 [0.0, 0.0, 0.0]
321 );
322 }
323
324 #[test]
325 fn a_vector_pointing_at_a_non_direction_reports_the_wrong_type() {
326 let mut model = Model::new();
327 model.insert(EntityId(1), Entity::new("IFCCARTESIANPOINT", vec![]));
328 let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(1.0)]);
329 let err = Vector::new(EntityId(2), &v).components(&model).unwrap_err();
330 assert!(matches!(
331 err,
332 GeometryError::WrongEntityType {
333 expected: "IfcDirection",
334 ..
335 }
336 ));
337 }
338
339 #[test]
340 fn a_dangling_direction_reference_names_the_referrer() {
341 let model = Model::new();
342 let err = resolve_unit(&model, EntityId(5), EntityId(99)).unwrap_err();
343 assert_eq!(err.entity(), Some(EntityId(5)));
344 }
345
346 #[test]
347 fn resolving_ratios_keeps_them_unnormalized() {
348 let mut model = Model::new();
349 model.insert(EntityId(1), direction_entity(&[0.0, 2.0]));
350 assert_eq!(
351 resolve_ratios_3d(&model, EntityId(2), EntityId(1)).unwrap(),
352 [0.0, 2.0, 0.0]
353 );
354 }
355}