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
266#[derive(Debug, Clone, Copy)]
272pub enum CartesianPointList<'m> {
273 TwoD(CartesianPointList2D<'m>),
275 ThreeD(CartesianPointList3D<'m>),
277}
278
279impl CartesianPointList<'_> {
280 pub fn id(&self) -> EntityId {
282 match self {
283 Self::TwoD(list) => list.id(),
284 Self::ThreeD(list) => list.id(),
285 }
286 }
287
288 pub fn dimension(&self) -> usize {
290 match self {
291 Self::TwoD(_) => 2,
292 Self::ThreeD(_) => 3,
293 }
294 }
295}
296
297pub fn cartesian_point_3d(
303 model: &Model,
304 referrer: EntityId,
305 id: EntityId,
306) -> GeometryResult<[f64; 3]> {
307 crate::resource::resolve::cartesian_point(model, referrer, id)?.coordinates_3d()
308}
309
310fn rows<const N: usize>(
312 slots: &Slots<'_>,
313 index: usize,
314 name: &'static str,
315) -> GeometryResult<Vec<[f64; N]>> {
316 let value = slots.req(index, name)?;
317 let outer = value
318 .as_list()
319 .ok_or_else(|| wrong_kind(slots, name, "a list of coordinate rows", value))?;
320
321 let mut out = Vec::with_capacity(outer.len());
322 for row in outer {
323 let items = row
324 .as_list()
325 .ok_or_else(|| wrong_kind(slots, name, "a list of coordinate rows", row))?;
326 if items.len() != N {
327 return Err(slots.degenerate(format!(
328 "{name} row has {} entries, expected {N}",
329 items.len()
330 )));
331 }
332 let mut coords = [0.0; N];
333 for (dst, src) in coords.iter_mut().zip(items) {
334 *dst = src
335 .unwrap_typed()
336 .as_f64()
337 .ok_or_else(|| wrong_kind(slots, name, "numeric coordinates", src))?;
338 }
339 out.push(coords);
340 }
341 Ok(out)
342}
343
344fn wrong_kind(
349 slots: &Slots<'_>,
350 attribute: &'static str,
351 expected: &'static str,
352 found: &Value,
353) -> GeometryError {
354 GeometryError::WrongValueKind {
355 entity: slots.id(),
356 type_name: slots.type_name().to_string(),
357 attribute,
358 expected,
359 found: format!("{found:?}"),
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn reals(values: &[f64]) -> Value {
368 Value::List(values.iter().copied().map(Value::Real).collect())
369 }
370
371 fn point_entity(values: &[f64]) -> Entity {
372 Entity::new("IFCCARTESIANPOINT", vec![reals(values)])
373 }
374
375 #[test]
376 fn two_dimensional_points_are_not_silently_promoted_to_3d() {
377 let e = point_entity(&[1.0, 2.0]);
378 let p = CartesianPoint::new(EntityId(1), &e);
379 assert_eq!(p.dimension().unwrap(), 2, "the file wrote two coordinates");
380 assert_eq!(p.coordinates().unwrap(), vec![1.0, 2.0]);
381 assert_eq!(p.coordinates_3d().unwrap(), [1.0, 2.0, 0.0]);
383 }
384
385 #[test]
386 fn three_dimensional_points_keep_their_z() {
387 let e = point_entity(&[1.0, 2.0, 3.0]);
388 let p = CartesianPoint::new(EntityId(1), &e);
389 assert_eq!(p.dimension().unwrap(), 3);
390 assert_eq!(p.coordinates_3d().unwrap(), [1.0, 2.0, 3.0]);
391 }
392
393 #[test]
396 fn a_single_coordinate_is_degenerate_rather_than_zero_padded() {
397 let e = point_entity(&[1.0]);
398 let p = CartesianPoint::new(EntityId(9), &e);
399 assert!(p.dimension().is_err());
400 assert!(p.coordinates_3d().is_err());
401 }
402
403 #[test]
404 fn a_missing_coordinate_list_names_the_entity() {
405 let e = Entity::new("IFCCARTESIANPOINT", vec![]);
406 let err = CartesianPoint::new(EntityId(7), &e)
407 .coordinates()
408 .unwrap_err();
409 assert!(err.to_string().contains("#7"), "got: {err}");
410 assert!(err.to_string().contains("Coordinates"), "got: {err}");
411 }
412
413 #[test]
414 fn typed_length_measures_do_not_hide_the_number() {
415 let e = Entity::new(
416 "IFCCARTESIANPOINT",
417 vec![Value::List(vec![
418 Value::Typed {
419 type_name: "IFCLENGTHMEASURE".into(),
420 value: Box::new(Value::Real(4.0)),
421 },
422 Value::Integer(0),
423 ])],
424 );
425 let p = CartesianPoint::new(EntityId(1), &e);
426 assert_eq!(p.coordinates_3d().unwrap(), [4.0, 0.0, 0.0]);
427 }
428
429 #[test]
430 fn point_on_curve_exposes_its_basis_and_parameter() {
431 let e = Entity::new(
432 "IFCPOINTONCURVE",
433 vec![
434 Value::Ref(EntityId(5)),
435 Value::Typed {
436 type_name: "IFCPARAMETERVALUE".into(),
437 value: Box::new(Value::Real(0.25)),
438 },
439 ],
440 );
441 let p = PointOnCurve::new(EntityId(1), &e);
442 assert_eq!(p.basis_curve().unwrap(), EntityId(5));
443 assert_eq!(p.point_parameter().unwrap(), 0.25);
444 }
445
446 #[test]
447 fn point_on_surface_exposes_both_parameters_in_order() {
448 let e = Entity::new(
449 "IFCPOINTONSURFACE",
450 vec![
451 Value::Ref(EntityId(5)),
452 Value::Real(0.25),
453 Value::Real(0.75),
454 ],
455 );
456 let p = PointOnSurface::new(EntityId(1), &e);
457 assert_eq!(p.basis_surface().unwrap(), EntityId(5));
458 assert_eq!(p.parameters().unwrap(), (0.25, 0.75));
459 }
460
461 #[test]
462 fn point_lists_read_every_row_in_file_order() {
463 let e = Entity::new(
464 "IFCCARTESIANPOINTLIST3D",
465 vec![Value::List(vec![
466 reals(&[0.0, 0.0, 0.0]),
467 reals(&[1.0, 0.0, 0.0]),
468 reals(&[1.0, 1.0, 0.0]),
469 ])],
470 );
471 let list = CartesianPointList3D::new(EntityId(1), &e);
472 let coords = list.coordinates().unwrap();
473 assert_eq!(coords.len(), 3);
474 assert_eq!(coords[2], [1.0, 1.0, 0.0]);
475 }
476
477 #[test]
480 fn point_list_indices_are_one_based_and_zero_is_out_of_range() {
481 let e = Entity::new(
482 "IFCCARTESIANPOINTLIST2D",
483 vec![Value::List(vec![reals(&[7.0, 8.0]), reals(&[9.0, 10.0])])],
484 );
485 let list = CartesianPointList2D::new(EntityId(1), &e);
486 assert_eq!(list.point(1).unwrap(), Some([7.0, 8.0]));
487 assert_eq!(list.point(2).unwrap(), Some([9.0, 10.0]));
488 assert_eq!(list.point(0).unwrap(), None, "there is no index 0 in IFC");
489 assert_eq!(list.point(3).unwrap(), None);
490 }
491
492 #[test]
495 fn a_row_of_the_wrong_width_fails_instead_of_being_truncated() {
496 let e = Entity::new(
497 "IFCCARTESIANPOINTLIST2D",
498 vec![Value::List(vec![reals(&[1.0, 2.0, 3.0])])],
499 );
500 let err = CartesianPointList2D::new(EntityId(4), &e)
501 .coordinates()
502 .unwrap_err();
503 assert!(err.to_string().contains("#4"), "got: {err}");
504 }
505
506 #[test]
507 fn resolving_a_point_reference_rejects_the_wrong_entity_type() {
508 let mut model = Model::new();
509 model.insert(EntityId(1), Entity::new("IFCDIRECTION", vec![]));
510 let err = cartesian_point_3d(&model, EntityId(2), EntityId(1)).unwrap_err();
511 assert!(matches!(
512 err,
513 GeometryError::WrongEntityType {
514 expected: "IfcCartesianPoint",
515 ..
516 }
517 ));
518 }
519
520 #[test]
521 fn resolving_a_dangling_point_reference_names_the_referrer() {
522 let model = Model::new();
523 let err = cartesian_point_3d(&model, EntityId(2), EntityId(99)).unwrap_err();
524 assert_eq!(err.entity(), Some(EntityId(2)));
525 }
526
527 #[test]
528 fn resolving_a_point_reference_yields_its_coordinates() {
529 let mut model = Model::new();
530 model.insert(EntityId(1), point_entity(&[1.0, 2.0, 3.0]));
531 assert_eq!(
532 cartesian_point_3d(&model, EntityId(2), EntityId(1)).unwrap(),
533 [1.0, 2.0, 3.0]
534 );
535 }
536}