Skip to main content

ifc_geometry/select/
aggregate_selects.rs

1//! The remaining geometry `SELECT` types plus the two indexed defined types.
2//!
3//! Split from `entity_selects` to keep both files well under the 800-line cap
4//! and because these share a theme: they are the selects used by curves on
5//! surfaces, geometric sets, grids and indexed polycurves.
6
7use crate::error::{GeometryError, GeometryResult};
8use crate::select::subtype::is_a;
9use ifc_model::{Entity, EntityId, Model, Value};
10
11/// Resolve a reference then classify it, reporting dangling refs uniformly.
12fn classify<T>(
13    model: &Model,
14    referrer: EntityId,
15    target: EntityId,
16    expected: &'static str,
17    f: impl Fn(&str) -> Option<T>,
18) -> GeometryResult<T> {
19    let entity: &Entity = model.get(target).ok_or(GeometryError::MissingEntity {
20        referrer,
21        missing: target,
22    })?;
23    f(&entity.type_name).ok_or_else(|| GeometryError::WrongEntityType {
24        entity: target,
25        actual: entity.type_name.to_string(),
26        expected,
27    })
28}
29
30/// `IfcCurveOnSurface` = `IfcCompositeCurveOnSurface` | `IfcPcurve` |
31/// `IfcSurfaceCurve`.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CurveOnSurface {
34    /// A composite curve whose segments all lie on the surface.
35    Composite(EntityId),
36    /// A curve defined in the surface's parameter space.
37    PCurve(EntityId),
38    /// A curve defined in 3D that lies on the surface.
39    SurfaceCurve(EntityId),
40}
41
42impl CurveOnSurface {
43    /// Classify the reference.
44    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
45        classify(model, referrer, target, "IfcCurveOnSurface", |t| {
46            // Checked most-derived first: IfcCompositeCurveOnSurface is a
47            // subtype of IfcCompositeCurve, not of the other two.
48            if is_a(t, "IFCCOMPOSITECURVEONSURFACE") {
49                Some(Self::Composite(target))
50            } else if is_a(t, "IFCPCURVE") {
51                Some(Self::PCurve(target))
52            } else if is_a(t, "IFCSURFACECURVE") {
53                Some(Self::SurfaceCurve(target))
54            } else {
55                None
56            }
57        })
58    }
59
60    /// The referenced entity.
61    pub fn id(&self) -> EntityId {
62        match self {
63            Self::Composite(id) | Self::PCurve(id) | Self::SurfaceCurve(id) => *id,
64        }
65    }
66}
67
68/// `IfcCurveOrEdgeCurve` = `IfcBoundedCurve` | `IfcEdgeCurve`.
69///
70/// The topological branch matters: an `IfcEdgeCurve` carries orientation from
71/// its topology, so a sweep along one may run opposite to the underlying
72/// geometric curve.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CurveOrEdgeCurve {
75    /// Pure geometry.
76    Bounded(EntityId),
77    /// Topological edge with an underlying curve.
78    EdgeCurve(EntityId),
79}
80
81impl CurveOrEdgeCurve {
82    /// Classify the reference.
83    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
84        classify(model, referrer, target, "IfcCurveOrEdgeCurve", |t| {
85            if is_a(t, "IFCEDGECURVE") {
86                Some(Self::EdgeCurve(target))
87            } else if is_a(t, "IFCBOUNDEDCURVE") {
88                Some(Self::Bounded(target))
89            } else {
90                None
91            }
92        })
93    }
94
95    /// The referenced entity.
96    pub fn id(&self) -> EntityId {
97        match self {
98            Self::Bounded(id) | Self::EdgeCurve(id) => *id,
99        }
100    }
101}
102
103/// `IfcGeometricSetSelect` = `IfcCurve` | `IfcPoint` | `IfcSurface`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum GeometricSetSelect {
106    /// A curve element.
107    Curve(EntityId),
108    /// A point element.
109    Point(EntityId),
110    /// A surface element.
111    Surface(EntityId),
112}
113
114impl GeometricSetSelect {
115    /// Classify the reference.
116    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
117        classify(model, referrer, target, "IfcGeometricSetSelect", |t| {
118            if is_a(t, "IFCCURVE") {
119                Some(Self::Curve(target))
120            } else if is_a(t, "IFCPOINT") {
121                Some(Self::Point(target))
122            } else if is_a(t, "IFCSURFACE") {
123                Some(Self::Surface(target))
124            } else {
125                None
126            }
127        })
128    }
129
130    /// The referenced entity.
131    pub fn id(&self) -> EntityId {
132        match self {
133            Self::Curve(id) | Self::Point(id) | Self::Surface(id) => *id,
134        }
135    }
136}
137
138/// `IfcSurfaceOrFaceSurface` = `IfcFaceBasedSurfaceModel` | `IfcFaceSurface` |
139/// `IfcSurface`.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum SurfaceOrFaceSurface {
142    /// A shell of faces.
143    FaceBasedSurfaceModel(EntityId),
144    /// A topological face carrying a surface.
145    FaceSurface(EntityId),
146    /// A pure geometric surface.
147    Surface(EntityId),
148}
149
150impl SurfaceOrFaceSurface {
151    /// Classify the reference.
152    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
153        classify(model, referrer, target, "IfcSurfaceOrFaceSurface", |t| {
154            if is_a(t, "IFCFACEBASEDSURFACEMODEL") {
155                Some(Self::FaceBasedSurfaceModel(target))
156            } else if is_a(t, "IFCFACESURFACE") {
157                Some(Self::FaceSurface(target))
158            } else if is_a(t, "IFCSURFACE") {
159                Some(Self::Surface(target))
160            } else {
161                None
162            }
163        })
164    }
165
166    /// The referenced entity.
167    pub fn id(&self) -> EntityId {
168        match self {
169            Self::FaceBasedSurfaceModel(id) | Self::FaceSurface(id) | Self::Surface(id) => *id,
170        }
171    }
172}
173
174/// `IfcPointOrVertexPoint` = `IfcPoint` | `IfcVertexPoint`.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum PointOrVertexPoint {
177    /// A geometric point.
178    Point(EntityId),
179    /// A topological vertex carrying a point.
180    VertexPoint(EntityId),
181}
182
183impl PointOrVertexPoint {
184    /// Classify the reference.
185    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
186        classify(model, referrer, target, "IfcPointOrVertexPoint", |t| {
187            if is_a(t, "IFCVERTEXPOINT") {
188                Some(Self::VertexPoint(target))
189            } else if is_a(t, "IFCPOINT") {
190                Some(Self::Point(target))
191            } else {
192                None
193            }
194        })
195    }
196
197    /// The referenced entity.
198    pub fn id(&self) -> EntityId {
199        match self {
200            Self::Point(id) | Self::VertexPoint(id) => *id,
201        }
202    }
203}
204
205/// `IfcGridPlacementDirectionSelect` = `IfcDirection` |
206/// `IfcVirtualGridIntersection`.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum GridPlacementDirectionSelect {
209    /// An explicit direction.
210    Direction(EntityId),
211    /// A second grid intersection to point at.
212    GridIntersection(EntityId),
213}
214
215impl GridPlacementDirectionSelect {
216    /// Classify the reference.
217    pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
218        classify(
219            model,
220            referrer,
221            target,
222            "IfcGridPlacementDirectionSelect",
223            |t| {
224                if is_a(t, "IFCVIRTUALGRIDINTERSECTION") {
225                    Some(Self::GridIntersection(target))
226                } else if is_a(t, "IFCDIRECTION") {
227                    Some(Self::Direction(target))
228                } else {
229                    None
230                }
231            },
232        )
233    }
234
235    /// The referenced entity.
236    pub fn id(&self) -> EntityId {
237        match self {
238            Self::Direction(id) | Self::GridIntersection(id) => *id,
239        }
240    }
241}
242
243/// `IfcLineIndex` = `LIST [2:?] OF IfcPositiveInteger`.
244///
245/// # 1-based, and that is not a detail
246///
247/// Indices point into an `IfcCartesianPointList`, and EXPRESS aggregates are
248/// 1-based. Rust slices are 0-based. Every index must be decremented exactly
249/// once, and doing it twice or not at all shifts the entire polyline by one
250/// vertex without any error being raised.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct LineIndex(Vec<usize>);
253
254/// `IfcArcIndex` = `LIST [3:3] OF IfcPositiveInteger`.
255///
256/// Exactly three: start, a point on the arc, and end.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub struct ArcIndex([usize; 3]);
259
260/// One entry of `IfcIndexedPolyCurve.Segments`.
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum SegmentIndexSelect {
263    /// A straight run through two or more points.
264    Line(LineIndex),
265    /// A circular arc through exactly three points.
266    Arc(ArcIndex),
267}
268
269impl LineIndex {
270    /// Zero-based indices into the point list.
271    pub fn as_zero_based(&self) -> &[usize] {
272        &self.0
273    }
274
275    /// How many points this run covers.
276    pub fn len(&self) -> usize {
277        self.0.len()
278    }
279
280    /// Is the run empty? Never true for a schema-valid index.
281    pub fn is_empty(&self) -> bool {
282        self.0.is_empty()
283    }
284}
285
286impl ArcIndex {
287    /// Zero-based `[start, mid, end]`.
288    pub fn as_zero_based(&self) -> [usize; 3] {
289        self.0
290    }
291}
292
293impl SegmentIndexSelect {
294    /// Parse one `IfcSegmentIndexSelect` value.
295    ///
296    /// The discriminator is the typed wrapper written by the codec:
297    /// `IFCLINEINDEX((1,2,3))` versus `IFCARCINDEX((3,4,5))`. Without the
298    /// wrapper the two are indistinguishable lists of integers, so an
299    /// untagged value is rejected rather than guessed.
300    pub fn from_value(entity: EntityId, value: &Value) -> GeometryResult<Self> {
301        let (tag, inner) = match value {
302            Value::Typed { type_name, value } => (type_name.to_ascii_uppercase(), value.as_ref()),
303            _ => {
304                return Err(GeometryError::Degenerate {
305                    entity,
306                    type_name: "IFCSEGMENTINDEXSELECT".into(),
307                    detail: "segment is not tagged IFCLINEINDEX or IFCARCINDEX, so its \
308                             kind cannot be determined"
309                        .into(),
310                })
311            }
312        };
313
314        let raw = inner.as_list().ok_or_else(|| GeometryError::Degenerate {
315            entity,
316            type_name: tag.clone(),
317            detail: "segment index is not a list".into(),
318        })?;
319
320        // EXPRESS aggregates are 1-based; convert exactly once, here.
321        let mut indices = Vec::with_capacity(raw.len());
322        for value in raw {
323            let n = match value.unwrap_typed() {
324                Value::Integer(i) => *i,
325                other => {
326                    return Err(GeometryError::Degenerate {
327                        entity,
328                        type_name: tag.clone(),
329                        detail: format!("segment index is not an integer: {other:?}"),
330                    })
331                }
332            };
333            if n < 1 {
334                return Err(GeometryError::Degenerate {
335                    entity,
336                    type_name: tag.clone(),
337                    detail: format!("index {n} is not a positive integer (EXPRESS is 1-based)"),
338                });
339            }
340            indices.push((n - 1) as usize);
341        }
342
343        match tag.as_str() {
344            "IFCARCINDEX" => {
345                let three: [usize; 3] =
346                    indices
347                        .as_slice()
348                        .try_into()
349                        .map_err(|_| GeometryError::Degenerate {
350                            entity,
351                            type_name: tag.clone(),
352                            detail: format!(
353                                "IfcArcIndex requires exactly 3 indices, found {}",
354                                indices.len()
355                            ),
356                        })?;
357                Ok(Self::Arc(ArcIndex(three)))
358            }
359            "IFCLINEINDEX" => {
360                if indices.len() < 2 {
361                    return Err(GeometryError::Degenerate {
362                        entity,
363                        type_name: tag,
364                        detail: format!(
365                            "IfcLineIndex requires at least 2 indices, found {}",
366                            indices.len()
367                        ),
368                    });
369                }
370                Ok(Self::Line(LineIndex(indices)))
371            }
372            other => Err(GeometryError::Degenerate {
373                entity,
374                type_name: other.to_string(),
375                detail: "not a member of IfcSegmentIndexSelect".into(),
376            }),
377        }
378    }
379
380    /// Zero-based indices, whichever branch this is.
381    pub fn indices(&self) -> Vec<usize> {
382        match self {
383            Self::Line(l) => l.0.clone(),
384            Self::Arc(a) => a.0.to_vec(),
385        }
386    }
387}
388
389/// `IfcDimensionCount` = `INTEGER` with `WHERE WR1: { 0 < SELF <= 3 }`.
390///
391/// The where-rule is the whole point of the type, so it is enforced at
392/// construction and the invalid states are unrepresentable thereafter.
393#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
394pub struct DimensionCount(u8);
395
396impl DimensionCount {
397    /// Construct, enforcing `0 < count <= 3`.
398    pub fn new(count: i64) -> Option<Self> {
399        (1..=3).contains(&count).then_some(Self(count as u8))
400    }
401
402    /// The dimension as a number.
403    pub fn get(&self) -> usize {
404        self.0 as usize
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    fn model_with(id: u64, type_name: &str) -> Model {
413        let mut model = Model::new();
414        model.insert(EntityId(id), Entity::new(type_name, vec![Value::Null; 4]));
415        model
416    }
417
418    fn tagged(tag: &str, values: &[i64]) -> Value {
419        Value::Typed {
420            type_name: tag.into(),
421            value: Box::new(Value::List(
422                values.iter().map(|v| Value::Integer(*v)).collect(),
423            )),
424        }
425    }
426
427    /// The off-by-one that silently shifts every polyline.
428    #[test]
429    fn one_based_express_indices_become_zero_based_exactly_once() {
430        let seg = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[1, 2, 3]))
431            .unwrap();
432        assert_eq!(
433            seg.indices(),
434            vec![0, 1, 2],
435            "IfcLineIndex((1,2,3)) addresses points 0,1,2"
436        );
437    }
438
439    #[test]
440    fn arc_indices_require_exactly_three_points() {
441        let ok = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4, 5]));
442        assert_eq!(ok.unwrap().indices(), vec![2, 3, 4]);
443
444        let short = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4]));
445        assert!(short.is_err(), "two indices cannot define an arc");
446    }
447
448    #[test]
449    fn line_indices_require_at_least_two_points() {
450        assert!(
451            SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[7])).is_err()
452        );
453    }
454
455    /// Index 0 is invalid in a 1-based schema and would underflow.
456    #[test]
457    fn a_zero_index_is_rejected_rather_than_underflowing() {
458        let err = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[0, 1]))
459            .unwrap_err();
460        assert!(err.to_string().contains("1-based"), "got {err}");
461    }
462
463    /// Untagged lists are ambiguous between line and arc.
464    #[test]
465    fn an_untagged_segment_is_rejected_rather_than_guessed() {
466        let bare = Value::List(vec![Value::Integer(1), Value::Integer(2)]);
467        assert!(SegmentIndexSelect::from_value(EntityId(1), &bare).is_err());
468    }
469
470    #[test]
471    fn dimension_count_enforces_its_where_rule() {
472        assert_eq!(DimensionCount::new(3).map(|d| d.get()), Some(3));
473        assert_eq!(DimensionCount::new(1).map(|d| d.get()), Some(1));
474        assert_eq!(DimensionCount::new(0), None, "0 < SELF is required");
475        assert_eq!(DimensionCount::new(4), None, "SELF <= 3 is required");
476        assert_eq!(DimensionCount::new(-1), None);
477    }
478
479    #[test]
480    fn geometric_set_members_classify_by_family() {
481        let curve = model_with(5, "IFCPOLYLINE");
482        assert_eq!(
483            GeometricSetSelect::resolve(&curve, EntityId(1), EntityId(5)).unwrap(),
484            GeometricSetSelect::Curve(EntityId(5))
485        );
486        let surface = model_with(5, "IFCPLANE");
487        assert_eq!(
488            GeometricSetSelect::resolve(&surface, EntityId(1), EntityId(5)).unwrap(),
489            GeometricSetSelect::Surface(EntityId(5))
490        );
491        let point = model_with(5, "IFCCARTESIANPOINT");
492        assert_eq!(
493            GeometricSetSelect::resolve(&point, EntityId(1), EntityId(5)).unwrap(),
494            GeometricSetSelect::Point(EntityId(5))
495        );
496    }
497
498    /// A composite curve on a surface must not classify as a plain surface
499    /// curve; the most derived branch wins.
500    #[test]
501    fn curve_on_surface_picks_the_most_derived_branch() {
502        let model = model_with(5, "IFCCOMPOSITECURVEONSURFACE");
503        assert_eq!(
504            CurveOnSurface::resolve(&model, EntityId(1), EntityId(5)).unwrap(),
505            CurveOnSurface::Composite(EntityId(5))
506        );
507    }
508    /// `IfcFaceSurface` IS-A `IfcSurface`, so branch order is load-bearing.
509    ///
510    /// Testing the general branch first would classify every face surface as
511    /// plain geometry and silently discard its topological face bounds.
512    #[test]
513    fn a_face_surface_is_not_classified_as_a_plain_surface() {
514        let model = model_with(1, "IFCFACESURFACE");
515        let resolved = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
516            .expect("face surface resolves");
517        assert_eq!(resolved, SurfaceOrFaceSurface::FaceSurface(EntityId(1)));
518
519        let plain = model_with(2, "IFCPLANE");
520        let resolved = SurfaceOrFaceSurface::resolve(&plain, EntityId(9), EntityId(2))
521            .expect("plane resolves");
522        assert_eq!(resolved, SurfaceOrFaceSurface::Surface(EntityId(2)));
523    }
524
525    /// A type outside the select is named, not silently defaulted.
526    #[test]
527    fn a_non_surface_is_rejected_by_the_surface_select() {
528        let model = model_with(1, "IFCPOLYLINE");
529        let error = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
530            .expect_err("a curve is not a surface");
531        assert_eq!(error.entity(), Some(EntityId(1)));
532    }
533
534    /// `IfcVertexPoint` IS-A `IfcPoint`: the topological branch must win.
535    #[test]
536    fn a_vertex_point_keeps_its_topological_identity() {
537        let model = model_with(1, "IFCVERTEXPOINT");
538        let resolved = PointOrVertexPoint::resolve(&model, EntityId(9), EntityId(1))
539            .expect("vertex point resolves");
540        assert_eq!(resolved, PointOrVertexPoint::VertexPoint(EntityId(1)));
541
542        let plain = model_with(2, "IFCCARTESIANPOINT");
543        let resolved = PointOrVertexPoint::resolve(&plain, EntityId(9), EntityId(2))
544            .expect("cartesian point resolves");
545        assert_eq!(resolved, PointOrVertexPoint::Point(EntityId(2)));
546    }
547
548    /// `IfcEdgeCurve` IS-A neither branch by accident: it is topology.
549    #[test]
550    fn an_edge_curve_is_not_classified_as_a_bounded_curve() {
551        let model = model_with(1, "IFCEDGECURVE");
552        let resolved = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
553            .expect("edge curve resolves");
554        assert_eq!(resolved, CurveOrEdgeCurve::EdgeCurve(EntityId(1)));
555
556        let plain = model_with(2, "IFCPOLYLINE");
557        let resolved =
558            CurveOrEdgeCurve::resolve(&plain, EntityId(9), EntityId(2)).expect("polyline resolves");
559        assert_eq!(resolved, CurveOrEdgeCurve::Bounded(EntityId(2)));
560    }
561
562    /// An unbounded surface is not a bounded curve, and must be named.
563    #[test]
564    fn a_surface_is_rejected_by_the_curve_select() {
565        let model = model_with(1, "IFCPLANE");
566        let error = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
567            .expect_err("a plane is not a curve");
568        assert_eq!(error.entity(), Some(EntityId(1)));
569    }
570}