Skip to main content

ifc_geometry/resource/
topology.rs

1//! `IfcTopologyResource`: the faceted-B-rep entity family.
2//!
3//! # Why these are views
4//!
5//! One `IfcClosedShell` in the corpus holds 169 faces, each with bounds and
6//! a loop of shared points. Materializing every level eagerly would copy the
7//! same 196-point pool 12 times. These borrow the model and resolve on
8//! demand, so the lowerer decides what to intern.
9//!
10//! Slot indices follow STEP inheritance: a subtype's own attributes start
11//! after every supertype attribute.
12
13use ifc_model::{Entity, EntityId, Model};
14
15use crate::error::{GeometryError, GeometryResult};
16use crate::slots::Slots;
17
18/// Attribute positions for the topology entities.
19pub mod slot {
20    /// `IfcPolyLoop.Polygon`
21    pub const POLYGON: usize = 0;
22    /// `IfcFaceBound.Bound`
23    pub const BOUND: usize = 0;
24    /// `IfcFaceBound.Orientation`
25    pub const ORIENTATION: usize = 1;
26    /// `IfcFace.Bounds`
27    pub const BOUNDS: usize = 0;
28    /// `IfcConnectedFaceSet.CfsFaces`
29    pub const CFS_FACES: usize = 0;
30    /// `IfcManifoldSolidBrep.Outer`
31    pub const OUTER: usize = 0;
32    /// `IfcFacetedBrepWithVoids.Voids`
33    pub const VOIDS: usize = 1;
34    /// `IfcVertexPoint.VertexGeometry`
35    pub const VERTEX_GEOMETRY: usize = 0;
36    /// `IfcEdge.EdgeStart`
37    pub const EDGE_START: usize = 0;
38    /// `IfcEdge.EdgeEnd`
39    pub const EDGE_END: usize = 1;
40    /// `IfcEdgeCurve.EdgeGeometry`
41    pub const EDGE_GEOMETRY: usize = 2;
42    /// `IfcEdgeCurve.SameSense`
43    pub const EDGE_SAME_SENSE: usize = 3;
44    /// `IfcOrientedEdge.EdgeElement`; slots 0-1 are the inherited, unset
45    /// `IfcEdge` vertices, written `*` in a STEP file.
46    pub const EDGE_ELEMENT: usize = 2;
47    /// `IfcOrientedEdge.Orientation`
48    pub const EDGE_ORIENTATION: usize = 3;
49    /// `IfcSubedge.ParentEdge`; slots 0-1 are the inherited `IfcEdge`
50    /// vertices, which a subedge does state.
51    pub const PARENT_EDGE: usize = 2;
52    /// `IfcEdgeLoop.EdgeList`
53    pub const EDGE_LIST: usize = 0;
54    /// `IfcFaceSurface.FaceSurface`
55    pub const FACE_SURFACE: usize = 1;
56    /// `IfcFaceSurface.SameSense`
57    pub const FACE_SAME_SENSE: usize = 2;
58}
59
60/// `IfcPolyLoop`: a closed wire given as an ordered point list.
61#[derive(Debug, Clone, Copy)]
62pub struct PolyLoop<'m> {
63    slots: Slots<'m>,
64}
65
66impl<'m> PolyLoop<'m> {
67    /// Wrap an entity assumed to be an `IfcPolyLoop`.
68    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
69        Self {
70            slots: Slots::new(id, entity),
71        }
72    }
73
74    /// The entity id.
75    pub fn id(&self) -> EntityId {
76        self.slots.id()
77    }
78
79    /// The polygon point references in file order.
80    ///
81    /// The schema requires at least three unique points; a shorter list
82    /// bounds no area and is rejected rather than silently skipped.
83    pub fn polygon(&self) -> GeometryResult<Vec<EntityId>> {
84        let points = self.slots.req_ref_list(slot::POLYGON, "Polygon")?;
85        if points.len() < 3 {
86            return Err(self.slots.degenerate(format!(
87                "polygon has {} points; a loop needs at least 3",
88                points.len()
89            )));
90        }
91        Ok(points)
92    }
93}
94
95/// `IfcFaceBound` and its `IfcFaceOuterBound` subtype.
96#[derive(Debug, Clone, Copy)]
97pub struct FaceBound<'m> {
98    slots: Slots<'m>,
99}
100
101impl<'m> FaceBound<'m> {
102    /// Wrap an entity assumed to be an `IfcFaceBound` subtype.
103    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
104        Self {
105            slots: Slots::new(id, entity),
106        }
107    }
108
109    /// The entity id.
110    pub fn id(&self) -> EntityId {
111        self.slots.id()
112    }
113
114    /// The bounding `IfcLoop`.
115    pub fn bound(&self) -> GeometryResult<EntityId> {
116        self.slots.req_ref(slot::BOUND, "Bound")
117    }
118
119    /// Whether the loop orientation agrees with the face normal.
120    ///
121    /// `.F.` means the sense is reversed: the loop must be traversed backwards
122    /// to bound the face correctly. Defaulting a missing value to `true` would
123    /// silently flip such a face inside out, so absence is an error.
124    pub fn orientation(&self) -> GeometryResult<bool> {
125        self.slots.req_bool(slot::ORIENTATION, "Orientation")
126    }
127
128    /// Whether this is the outer bound rather than a hole.
129    pub fn is_outer(&self) -> bool {
130        self.slots
131            .type_name()
132            .eq_ignore_ascii_case("IFCFACEOUTERBOUND")
133    }
134}
135
136/// `IfcFace`: a bounded region, possibly with holes.
137#[derive(Debug, Clone, Copy)]
138pub struct Face<'m> {
139    slots: Slots<'m>,
140}
141
142impl<'m> Face<'m> {
143    /// Wrap an entity assumed to be an `IfcFace` subtype.
144    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
145        Self {
146            slots: Slots::new(id, entity),
147        }
148    }
149
150    /// The entity id.
151    pub fn id(&self) -> EntityId {
152        self.slots.id()
153    }
154
155    /// The bound references. The schema requires at least one.
156    pub fn bounds(&self) -> GeometryResult<Vec<EntityId>> {
157        let bounds = self.slots.req_ref_list(slot::BOUNDS, "Bounds")?;
158        if bounds.is_empty() {
159            return Err(self.slots.degenerate("face has no bounds"));
160        }
161        Ok(bounds)
162    }
163}
164
165/// `IfcConnectedFaceSet` and its `IfcClosedShell`/`IfcOpenShell` subtypes.
166#[derive(Debug, Clone, Copy)]
167pub struct ConnectedFaceSet<'m> {
168    slots: Slots<'m>,
169}
170
171impl<'m> ConnectedFaceSet<'m> {
172    /// Wrap an entity assumed to be an `IfcConnectedFaceSet` subtype.
173    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
174        Self {
175            slots: Slots::new(id, entity),
176        }
177    }
178
179    /// The entity id.
180    pub fn id(&self) -> EntityId {
181        self.slots.id()
182    }
183
184    /// The member face references.
185    pub fn faces(&self) -> GeometryResult<Vec<EntityId>> {
186        let faces = self.slots.req_ref_list(slot::CFS_FACES, "CfsFaces")?;
187        if faces.is_empty() {
188            return Err(self.slots.degenerate("face set has no faces"));
189        }
190        Ok(faces)
191    }
192
193    /// Whether the source asserts this shell is closed.
194    ///
195    /// Only `IfcClosedShell` carries that guarantee. Reporting an open shell
196    /// as closed would let a downstream boolean assume a valid interior.
197    pub fn is_closed(&self) -> bool {
198        self.slots
199            .type_name()
200            .eq_ignore_ascii_case("IFCCLOSEDSHELL")
201    }
202}
203
204/// `IfcManifoldSolidBrep` and its `IfcFacetedBrep`/`WithVoids` subtypes.
205#[derive(Debug, Clone, Copy)]
206pub struct ManifoldSolidBrep<'m> {
207    slots: Slots<'m>,
208}
209
210impl<'m> ManifoldSolidBrep<'m> {
211    /// Wrap an entity assumed to be an `IfcManifoldSolidBrep` subtype.
212    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
213        Self {
214            slots: Slots::new(id, entity),
215        }
216    }
217
218    /// The entity id.
219    pub fn id(&self) -> EntityId {
220        self.slots.id()
221    }
222
223    /// The outer boundary shell.
224    pub fn outer(&self) -> GeometryResult<EntityId> {
225        self.slots.req_ref(slot::OUTER, "Outer")
226    }
227
228    /// Interior void shells, empty unless this is an `IfcFacetedBrepWithVoids`.
229    ///
230    /// Voids are what make a brick a hollow block. Dropping them yields a
231    /// solid that is visually identical from outside and wrong by volume, so
232    /// the attribute is read whenever the subtype declares it.
233    pub fn voids(&self) -> GeometryResult<Vec<EntityId>> {
234        if !self
235            .slots
236            .type_name()
237            .eq_ignore_ascii_case("IFCFACETEDBREPWITHVOIDS")
238        {
239            return Ok(Vec::new());
240        }
241        let voids = self.slots.req_ref_list(slot::VOIDS, "Voids")?;
242        if voids.is_empty() {
243            return Err(self
244                .slots
245                .degenerate("IfcFacetedBrepWithVoids declares no voids"));
246        }
247        Ok(voids)
248    }
249}
250
251/// Resolve an entity and confirm it belongs to an expected type family.
252pub fn expect_type<'m>(
253    model: &'m Model,
254    referrer: EntityId,
255    id: EntityId,
256    accepted: &[&str],
257    expected: &'static str,
258) -> GeometryResult<&'m Entity> {
259    let entity = model.get(id).ok_or(GeometryError::MissingEntity {
260        referrer,
261        missing: id,
262    })?;
263    if accepted
264        .iter()
265        .any(|name| entity.type_name.eq_ignore_ascii_case(name))
266    {
267        return Ok(entity);
268    }
269    Err(GeometryError::WrongEntityType {
270        entity: id,
271        actual: entity.type_name.to_string(),
272        expected,
273    })
274}
275
276/// `IfcVertexPoint`: a topological vertex carrying its geometric point.
277#[derive(Debug, Clone, Copy)]
278pub struct VertexPoint<'m> {
279    slots: Slots<'m>,
280}
281
282impl<'m> VertexPoint<'m> {
283    /// Wrap an entity assumed to be an `IfcVertexPoint`.
284    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
285        Self {
286            slots: Slots::new(id, entity),
287        }
288    }
289
290    /// The entity id.
291    pub fn id(&self) -> EntityId {
292        self.slots.id()
293    }
294
295    /// The `IfcCartesianPoint` this vertex sits on.
296    pub fn vertex_geometry(&self) -> GeometryResult<EntityId> {
297        self.slots.req_ref(slot::VERTEX_GEOMETRY, "VertexGeometry")
298    }
299}
300
301/// `IfcSubedge`: an edge carved from a longer parent edge.
302///
303/// The subedge states its own `EdgeStart`/`EdgeEnd`; `ParentEdge` supplies
304/// the carrier geometry the subedge is a piece of. The parent may itself be
305/// a subedge, so resolving the carrier is a walk, not a single hop.
306#[derive(Debug, Clone, Copy)]
307pub struct Subedge<'m> {
308    slots: Slots<'m>,
309}
310
311impl<'m> Subedge<'m> {
312    /// Wrap an entity assumed to be an `IfcSubedge`.
313    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
314        Self {
315            slots: Slots::new(id, entity),
316        }
317    }
318
319    /// The entity id.
320    pub fn id(&self) -> EntityId {
321        self.slots.id()
322    }
323
324    /// `EdgeStart`, the vertex the carved piece starts at.
325    pub fn start(&self) -> GeometryResult<EntityId> {
326        self.slots.req_ref(slot::EDGE_START, "EdgeStart")
327    }
328
329    /// `EdgeEnd`, the vertex the carved piece ends at.
330    pub fn end(&self) -> GeometryResult<EntityId> {
331        self.slots.req_ref(slot::EDGE_END, "EdgeEnd")
332    }
333
334    /// `ParentEdge`, mandatory: without it a subedge carves nothing.
335    pub fn parent_edge(&self) -> GeometryResult<EntityId> {
336        self.slots.req_ref(slot::PARENT_EDGE, "ParentEdge")
337    }
338}
339
340/// `IfcEdge` and its `IfcEdgeCurve` subtype: a bounded piece of a curve.
341///
342/// `EdgeStart`/`EdgeEnd` are `IfcVertex` references, not points. An
343/// `IfcEdgeCurve` adds the supporting curve and a sense flag saying whether
344/// the edge runs along the curve or against it.
345#[derive(Debug, Clone, Copy)]
346pub struct EdgeCurve<'m> {
347    slots: Slots<'m>,
348}
349
350impl<'m> EdgeCurve<'m> {
351    /// Wrap an entity assumed to be an `IfcEdge` or `IfcEdgeCurve`.
352    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
353        Self {
354            slots: Slots::new(id, entity),
355        }
356    }
357
358    /// The entity id.
359    pub fn id(&self) -> EntityId {
360        self.slots.id()
361    }
362
363    /// The start vertex reference.
364    pub fn start(&self) -> GeometryResult<EntityId> {
365        self.slots.req_ref(slot::EDGE_START, "EdgeStart")
366    }
367
368    /// The end vertex reference.
369    pub fn end(&self) -> GeometryResult<EntityId> {
370        self.slots.req_ref(slot::EDGE_END, "EdgeEnd")
371    }
372
373    /// The supporting curve, absent on a plain `IfcEdge`.
374    ///
375    /// A plain edge is straight between its vertices, so `None` is a complete
376    /// description rather than a missing value.
377    pub fn edge_geometry(&self) -> Option<EntityId> {
378        self.slots.opt_ref(slot::EDGE_GEOMETRY)
379    }
380
381    /// Does the edge run along the curve's own direction?
382    ///
383    /// Defaults to true when absent. A false flag reverses the edge relative
384    /// to its curve, which matters for any parameterised traversal.
385    pub fn same_sense(&self) -> bool {
386        self.slots.opt_bool(slot::EDGE_SAME_SENSE).unwrap_or(true)
387    }
388}
389
390/// `IfcOrientedEdge`: a reuse of an edge, possibly reversed.
391///
392/// This is the entity that makes edge sharing explicit. Two faces meeting at
393/// one edge each hold an oriented edge pointing at the SAME `IfcEdgeCurve`,
394/// with opposite `Orientation`. Resolving through to the underlying edge is
395/// what preserves the manifold; treating each use as its own edge silently
396/// disconnects the solid.
397#[derive(Debug, Clone, Copy)]
398pub struct OrientedEdge<'m> {
399    slots: Slots<'m>,
400}
401
402impl<'m> OrientedEdge<'m> {
403    /// Wrap an entity assumed to be an `IfcOrientedEdge`.
404    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
405        Self {
406            slots: Slots::new(id, entity),
407        }
408    }
409
410    /// The underlying edge this use points at.
411    pub fn edge_element(&self) -> GeometryResult<EntityId> {
412        self.slots.req_ref(slot::EDGE_ELEMENT, "EdgeElement")
413    }
414
415    /// Does this use run along the underlying edge, or against it?
416    pub fn orientation(&self) -> bool {
417        self.slots.opt_bool(slot::EDGE_ORIENTATION).unwrap_or(true)
418    }
419}
420
421/// `IfcEdgeLoop`: a closed wire given as a list of oriented edges.
422///
423/// The curved counterpart of `IfcPolyLoop`. Unlike a poly loop the closure
424/// is explicit: the last edge's end vertex is the first edge's start, and no
425/// implied closing segment is added.
426#[derive(Debug, Clone, Copy)]
427pub struct EdgeLoop<'m> {
428    slots: Slots<'m>,
429}
430
431impl<'m> EdgeLoop<'m> {
432    /// Wrap an entity assumed to be an `IfcEdgeLoop`.
433    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
434        Self {
435            slots: Slots::new(id, entity),
436        }
437    }
438
439    /// The oriented edges in traversal order.
440    ///
441    /// A loop needs at least one edge; an empty list bounds nothing and is
442    /// rejected rather than producing a face with no boundary.
443    pub fn edge_list(&self) -> GeometryResult<Vec<EntityId>> {
444        let edges = self.slots.req_ref_list(slot::EDGE_LIST, "EdgeList")?;
445        if edges.is_empty() {
446            return Err(self.slots.degenerate("edge loop has no edges"));
447        }
448        Ok(edges)
449    }
450}
451
452/// `IfcFaceSurface` and its `IfcAdvancedFace` subtype.
453///
454/// Adds a support surface and a sense flag to `IfcFace`. `SameSense` says
455/// whether the face normal agrees with the surface normal; ignoring it yields
456/// an inside-out face that still passes every structural check.
457#[derive(Debug, Clone, Copy)]
458pub struct FaceSurface<'m> {
459    slots: Slots<'m>,
460}
461
462impl<'m> FaceSurface<'m> {
463    /// Wrap an entity assumed to be an `IfcFaceSurface` or `IfcAdvancedFace`.
464    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
465        Self {
466            slots: Slots::new(id, entity),
467        }
468    }
469
470    /// The supporting surface reference.
471    pub fn face_surface(&self) -> GeometryResult<EntityId> {
472        self.slots.req_ref(slot::FACE_SURFACE, "FaceSurface")
473    }
474
475    /// Does the face normal agree with the surface normal?
476    pub fn same_sense(&self) -> bool {
477        self.slots.opt_bool(slot::FACE_SAME_SENSE).unwrap_or(true)
478    }
479}