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}