1use crate::error::{GeometryError, GeometryResult};
24use crate::slots::Slots;
25use ifc_model::{Entity, EntityId, Model, Value};
26
27pub(crate) mod slot {
29 pub mod cartesian_point {
31 pub const COORDINATES: usize = 0;
33 }
34
35 pub mod point_on_curve {
37 pub const BASIS_CURVE: usize = 0;
39 pub const POINT_PARAMETER: usize = 1;
41 }
42
43 pub mod point_on_surface {
45 pub const BASIS_SURFACE: usize = 0;
47 pub const POINT_PARAMETER_U: usize = 1;
49 pub const POINT_PARAMETER_V: usize = 2;
51 }
52
53 pub mod point_list {
56 pub const COORD_LIST: usize = 0;
58 }
59}
60
61#[derive(Debug, Clone, Copy)]
63pub struct CartesianPoint<'m> {
64 slots: Slots<'m>,
65}
66
67impl<'m> CartesianPoint<'m> {
68 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
70 Self {
71 slots: Slots::new(id, entity),
72 }
73 }
74
75 pub fn id(&self) -> EntityId {
77 self.slots.id()
78 }
79
80 pub fn coordinates(&self) -> GeometryResult<Vec<f64>> {
82 self.slots
83 .req_f64_list(slot::cartesian_point::COORDINATES, "Coordinates")
84 }
85
86 pub fn dimension(&self) -> GeometryResult<usize> {
92 let n = self.coordinates()?.len();
93 match n {
94 2 | 3 => Ok(n),
95 other => Err(self
96 .slots
97 .degenerate(format!("Coordinates has {other} entries, expected 2 or 3"))),
98 }
99 }
100
101 pub fn coordinates_3d(&self) -> GeometryResult<[f64; 3]> {
107 let c = self.coordinates()?;
108 match c.len() {
109 2 => Ok([c[0], c[1], 0.0]),
110 3 => Ok([c[0], c[1], c[2]]),
111 other => Err(self
112 .slots
113 .degenerate(format!("Coordinates has {other} entries, expected 2 or 3"))),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
124pub struct PointOnCurve<'m> {
125 slots: Slots<'m>,
126}
127
128impl<'m> PointOnCurve<'m> {
129 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
131 Self {
132 slots: Slots::new(id, entity),
133 }
134 }
135
136 pub fn id(&self) -> EntityId {
138 self.slots.id()
139 }
140
141 pub fn basis_curve(&self) -> GeometryResult<EntityId> {
143 self.slots
144 .req_ref(slot::point_on_curve::BASIS_CURVE, "BasisCurve")
145 }
146
147 pub fn point_parameter(&self) -> GeometryResult<f64> {
152 self.slots
153 .req_f64(slot::point_on_curve::POINT_PARAMETER, "PointParameter")
154 }
155}
156
157#[derive(Debug, Clone, Copy)]
159pub struct PointOnSurface<'m> {
160 slots: Slots<'m>,
161}
162
163impl<'m> PointOnSurface<'m> {
164 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
166 Self {
167 slots: Slots::new(id, entity),
168 }
169 }
170
171 pub fn id(&self) -> EntityId {
173 self.slots.id()
174 }
175
176 pub fn basis_surface(&self) -> GeometryResult<EntityId> {
178 self.slots
179 .req_ref(slot::point_on_surface::BASIS_SURFACE, "BasisSurface")
180 }
181
182 pub fn parameters(&self) -> GeometryResult<(f64, f64)> {
184 let u = self
185 .slots
186 .req_f64(slot::point_on_surface::POINT_PARAMETER_U, "PointParameterU")?;
187 let v = self
188 .slots
189 .req_f64(slot::point_on_surface::POINT_PARAMETER_V, "PointParameterV")?;
190 Ok((u, v))
191 }
192}
193
194#[derive(Debug, Clone, Copy)]
196pub struct CartesianPointList2D<'m> {
197 slots: Slots<'m>,
198}
199
200impl<'m> CartesianPointList2D<'m> {
201 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
203 Self {
204 slots: Slots::new(id, entity),
205 }
206 }
207
208 pub fn id(&self) -> EntityId {
210 self.slots.id()
211 }
212
213 pub fn coordinates(&self) -> GeometryResult<Vec<[f64; 2]>> {
218 rows::<2>(&self.slots, slot::point_list::COORD_LIST, "CoordList")
219 }
220
221 pub fn point(&self, one_based: usize) -> GeometryResult<Option<[f64; 2]>> {
226 let coords = self.coordinates()?;
227 Ok(one_based
228 .checked_sub(1)
229 .and_then(|i| coords.get(i).copied()))
230 }
231}
232
233#[derive(Debug, Clone, Copy)]
235pub struct CartesianPointList3D<'m> {
236 slots: Slots<'m>,
237}
238
239impl<'m> CartesianPointList3D<'m> {
240 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
242 Self {
243 slots: Slots::new(id, entity),
244 }
245 }
246
247 pub fn id(&self) -> EntityId {
249 self.slots.id()
250 }
251
252 pub fn coordinates(&self) -> GeometryResult<Vec<[f64; 3]>> {
254 rows::<3>(&self.slots, slot::point_list::COORD_LIST, "CoordList")
255 }
256
257 pub fn point(&self, one_based: usize) -> GeometryResult<Option<[f64; 3]>> {
259 let coords = self.coordinates()?;
260 Ok(one_based
261 .checked_sub(1)
262 .and_then(|i| coords.get(i).copied()))
263 }
264}
265
266pub fn cartesian_point_3d(
272 model: &Model,
273 referrer: EntityId,
274 id: EntityId,
275) -> GeometryResult<[f64; 3]> {
276 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
277 referrer,
278 missing: id,
279 })?;
280 if !entity.is_type("IFCCARTESIANPOINT") {
281 return Err(GeometryError::WrongEntityType {
282 entity: id,
283 actual: entity.type_name.to_string(),
284 expected: "IfcCartesianPoint",
285 });
286 }
287 CartesianPoint::new(id, entity).coordinates_3d()
288}
289
290fn rows<const N: usize>(
292 slots: &Slots<'_>,
293 index: usize,
294 name: &'static str,
295) -> GeometryResult<Vec<[f64; N]>> {
296 let value = slots.req(index, name)?;
297 let outer = value
298 .as_list()
299 .ok_or_else(|| wrong_kind(slots, name, "a list of coordinate rows", value))?;
300
301 let mut out = Vec::with_capacity(outer.len());
302 for row in outer {
303 let items = row
304 .as_list()
305 .ok_or_else(|| wrong_kind(slots, name, "a list of coordinate rows", row))?;
306 if items.len() != N {
307 return Err(slots.degenerate(format!(
308 "{name} row has {} entries, expected {N}",
309 items.len()
310 )));
311 }
312 let mut coords = [0.0; N];
313 for (dst, src) in coords.iter_mut().zip(items) {
314 *dst = src
315 .unwrap_typed()
316 .as_f64()
317 .ok_or_else(|| wrong_kind(slots, name, "numeric coordinates", src))?;
318 }
319 out.push(coords);
320 }
321 Ok(out)
322}
323
324fn wrong_kind(
329 slots: &Slots<'_>,
330 attribute: &'static str,
331 expected: &'static str,
332 found: &Value,
333) -> GeometryError {
334 GeometryError::WrongValueKind {
335 entity: slots.id(),
336 type_name: slots.type_name().to_string(),
337 attribute,
338 expected,
339 found: format!("{found:?}"),
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 fn reals(values: &[f64]) -> Value {
348 Value::List(values.iter().copied().map(Value::Real).collect())
349 }
350
351 fn point_entity(values: &[f64]) -> Entity {
352 Entity::new("IFCCARTESIANPOINT", vec![reals(values)])
353 }
354
355 #[test]
356 fn two_dimensional_points_are_not_silently_promoted_to_3d() {
357 let e = point_entity(&[1.0, 2.0]);
358 let p = CartesianPoint::new(EntityId(1), &e);
359 assert_eq!(p.dimension().unwrap(), 2, "the file wrote two coordinates");
360 assert_eq!(p.coordinates().unwrap(), vec![1.0, 2.0]);
361 assert_eq!(p.coordinates_3d().unwrap(), [1.0, 2.0, 0.0]);
363 }
364
365 #[test]
366 fn three_dimensional_points_keep_their_z() {
367 let e = point_entity(&[1.0, 2.0, 3.0]);
368 let p = CartesianPoint::new(EntityId(1), &e);
369 assert_eq!(p.dimension().unwrap(), 3);
370 assert_eq!(p.coordinates_3d().unwrap(), [1.0, 2.0, 3.0]);
371 }
372
373 #[test]
376 fn a_single_coordinate_is_degenerate_rather_than_zero_padded() {
377 let e = point_entity(&[1.0]);
378 let p = CartesianPoint::new(EntityId(9), &e);
379 assert!(p.dimension().is_err());
380 assert!(p.coordinates_3d().is_err());
381 }
382
383 #[test]
384 fn a_missing_coordinate_list_names_the_entity() {
385 let e = Entity::new("IFCCARTESIANPOINT", vec![]);
386 let err = CartesianPoint::new(EntityId(7), &e)
387 .coordinates()
388 .unwrap_err();
389 assert!(err.to_string().contains("#7"), "got: {err}");
390 assert!(err.to_string().contains("Coordinates"), "got: {err}");
391 }
392
393 #[test]
394 fn typed_length_measures_do_not_hide_the_number() {
395 let e = Entity::new(
396 "IFCCARTESIANPOINT",
397 vec![Value::List(vec![
398 Value::Typed {
399 type_name: "IFCLENGTHMEASURE".into(),
400 value: Box::new(Value::Real(4.0)),
401 },
402 Value::Integer(0),
403 ])],
404 );
405 let p = CartesianPoint::new(EntityId(1), &e);
406 assert_eq!(p.coordinates_3d().unwrap(), [4.0, 0.0, 0.0]);
407 }
408
409 #[test]
410 fn point_on_curve_exposes_its_basis_and_parameter() {
411 let e = Entity::new(
412 "IFCPOINTONCURVE",
413 vec![
414 Value::Ref(EntityId(5)),
415 Value::Typed {
416 type_name: "IFCPARAMETERVALUE".into(),
417 value: Box::new(Value::Real(0.25)),
418 },
419 ],
420 );
421 let p = PointOnCurve::new(EntityId(1), &e);
422 assert_eq!(p.basis_curve().unwrap(), EntityId(5));
423 assert_eq!(p.point_parameter().unwrap(), 0.25);
424 }
425
426 #[test]
427 fn point_on_surface_exposes_both_parameters_in_order() {
428 let e = Entity::new(
429 "IFCPOINTONSURFACE",
430 vec![
431 Value::Ref(EntityId(5)),
432 Value::Real(0.25),
433 Value::Real(0.75),
434 ],
435 );
436 let p = PointOnSurface::new(EntityId(1), &e);
437 assert_eq!(p.basis_surface().unwrap(), EntityId(5));
438 assert_eq!(p.parameters().unwrap(), (0.25, 0.75));
439 }
440
441 #[test]
442 fn point_lists_read_every_row_in_file_order() {
443 let e = Entity::new(
444 "IFCCARTESIANPOINTLIST3D",
445 vec![Value::List(vec![
446 reals(&[0.0, 0.0, 0.0]),
447 reals(&[1.0, 0.0, 0.0]),
448 reals(&[1.0, 1.0, 0.0]),
449 ])],
450 );
451 let list = CartesianPointList3D::new(EntityId(1), &e);
452 let coords = list.coordinates().unwrap();
453 assert_eq!(coords.len(), 3);
454 assert_eq!(coords[2], [1.0, 1.0, 0.0]);
455 }
456
457 #[test]
460 fn point_list_indices_are_one_based_and_zero_is_out_of_range() {
461 let e = Entity::new(
462 "IFCCARTESIANPOINTLIST2D",
463 vec![Value::List(vec![reals(&[7.0, 8.0]), reals(&[9.0, 10.0])])],
464 );
465 let list = CartesianPointList2D::new(EntityId(1), &e);
466 assert_eq!(list.point(1).unwrap(), Some([7.0, 8.0]));
467 assert_eq!(list.point(2).unwrap(), Some([9.0, 10.0]));
468 assert_eq!(list.point(0).unwrap(), None, "there is no index 0 in IFC");
469 assert_eq!(list.point(3).unwrap(), None);
470 }
471
472 #[test]
475 fn a_row_of_the_wrong_width_fails_instead_of_being_truncated() {
476 let e = Entity::new(
477 "IFCCARTESIANPOINTLIST2D",
478 vec![Value::List(vec![reals(&[1.0, 2.0, 3.0])])],
479 );
480 let err = CartesianPointList2D::new(EntityId(4), &e)
481 .coordinates()
482 .unwrap_err();
483 assert!(err.to_string().contains("#4"), "got: {err}");
484 }
485
486 #[test]
487 fn resolving_a_point_reference_rejects_the_wrong_entity_type() {
488 let mut model = Model::new();
489 model.insert(EntityId(1), Entity::new("IFCDIRECTION", vec![]));
490 let err = cartesian_point_3d(&model, EntityId(2), EntityId(1)).unwrap_err();
491 assert!(matches!(
492 err,
493 GeometryError::WrongEntityType {
494 expected: "IfcCartesianPoint",
495 ..
496 }
497 ));
498 }
499
500 #[test]
501 fn resolving_a_dangling_point_reference_names_the_referrer() {
502 let model = Model::new();
503 let err = cartesian_point_3d(&model, EntityId(2), EntityId(99)).unwrap_err();
504 assert_eq!(err.entity(), Some(EntityId(2)));
505 }
506
507 #[test]
508 fn resolving_a_point_reference_yields_its_coordinates() {
509 let mut model = Model::new();
510 model.insert(EntityId(1), point_entity(&[1.0, 2.0, 3.0]));
511 assert_eq!(
512 cartesian_point_3d(&model, EntityId(2), EntityId(1)).unwrap(),
513 [1.0, 2.0, 3.0]
514 );
515 }
516}