Skip to main content

ifc_geometry/solid/
brep.rs

1//! Boundary representations: `IfcManifoldSolidBrep` and its subtypes.
2//!
3//! # The model
4//!
5//! A B-rep solid is one `Outer` closed shell, optionally with inner shells
6//! that carve cavities out of it. Faceted breps use planar polygonal faces;
7//! advanced breps use `IfcAdvancedFace` with analytic or NURBS surfaces and
8//! properly curved edges.
9//!
10//! # The voids trap
11//!
12//! `IfcFacetedBrepWithVoids.Voids` and `IfcAdvancedBrepWithVoids.Voids` are
13//! **inner** shells to be subtracted, not additional outer shells. Appending
14//! them to the outer shell yields a solid with its cavities rendered as extra
15//! surface -- visually plausible, volumetrically wrong, and it silently
16//! corrupts any quantity takeoff computed from the mesh.
17//!
18//! # Shells are not resolved here
19//!
20//! `IfcClosedShell` belongs to `IfcTopologyResource`, owned elsewhere. These
21//! views return shell `EntityId`s; walking into `CfsFaces` is the topology
22//! layer's job.
23
24use crate::error::GeometryResult;
25use crate::slots::Slots;
26use ifc_model::{Entity, EntityId, Model, Value};
27
28/// `IfcManifoldSolidBrep` attribute slots.
29///
30/// EXPRESS (IFC4 ADD2 TC1): `IfcManifoldSolidBrep` declares `Outer` and its
31/// supertype `IfcSolidModel` declares no explicit attributes, so `Outer` is
32/// absolute slot 0 for every brep in the family.
33pub(crate) mod slot {
34    /// `Outer : IfcClosedShell`, declared on `IfcManifoldSolidBrep`.
35    pub const OUTER: usize = 0;
36    /// `Voids : SET [1:?] OF IfcClosedShell`, on the `WithVoids` subtypes.
37    ///
38    /// Absolute slot 1 in both `IfcFacetedBrepWithVoids` (which inherits only
39    /// `Outer` through `IfcFacetedBrep`) and `IfcAdvancedBrepWithVoids`.
40    pub const VOIDS: usize = 1;
41}
42
43/// `IfcManifoldSolidBrep`: the abstract brep, giving access to `Outer`.
44///
45/// Usable over any concrete brep because `Outer` sits at slot 0 in all of
46/// them. `IfcFacetedBrep` and `IfcAdvancedBrep` add no attributes of their own,
47/// so this view is the whole of their content.
48#[derive(Debug, Clone, Copy)]
49pub struct ManifoldSolidBrep<'m> {
50    slots: Slots<'m>,
51}
52
53impl<'m> ManifoldSolidBrep<'m> {
54    /// Wrap an entity assumed to be an `IfcManifoldSolidBrep` subtype.
55    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
56        Self {
57            slots: Slots::new(id, entity),
58        }
59    }
60
61    /// The entity id.
62    pub fn id(&self) -> EntityId {
63        self.slots.id()
64    }
65
66    /// The IFC type name, naming the concrete subtype.
67    pub fn type_name(&self) -> &'m str {
68        self.slots.type_name()
69    }
70
71    /// The `IfcClosedShell` reference bounding the solid from outside.
72    pub fn outer(&self) -> GeometryResult<EntityId> {
73        self.slots.req_ref(slot::OUTER, "Outer")
74    }
75
76    /// Does the concrete type carry `Voids`?
77    ///
78    /// Lets a caller branch without re-deriving the subtype lattice, and keeps
79    /// the `WithVoids` string comparison in exactly one place.
80    pub fn has_voids(&self) -> bool {
81        let name = self.type_name();
82        name.eq_ignore_ascii_case("IFCFACETEDBREPWITHVOIDS")
83            || name.eq_ignore_ascii_case("IFCADVANCEDBREPWITHVOIDS")
84    }
85}
86
87/// `IfcFacetedBrep`: a brep whose every face is a planar polygon.
88///
89/// Adds no attributes over [`ManifoldSolidBrep`]; the distinction is a promise
90/// about the faces, which lets a consumer skip surface evaluation entirely.
91#[derive(Debug, Clone, Copy)]
92pub struct FacetedBrep<'m> {
93    slots: Slots<'m>,
94}
95
96impl<'m> FacetedBrep<'m> {
97    /// Wrap an entity assumed to be an `IfcFacetedBrep`.
98    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
99        Self {
100            slots: Slots::new(id, entity),
101        }
102    }
103
104    /// The entity id.
105    pub fn id(&self) -> EntityId {
106        self.slots.id()
107    }
108
109    /// The inherited `IfcManifoldSolidBrep` attributes.
110    pub fn base(&self) -> ManifoldSolidBrep<'m> {
111        ManifoldSolidBrep { slots: self.slots }
112    }
113}
114
115/// `IfcFacetedBrepWithVoids`: a faceted brep with internal cavities.
116///
117/// See the module docs: `Voids` are **subtracted** inner shells. They are also
118/// each independently closed, so a void is a cavity, not a dent.
119#[derive(Debug, Clone, Copy)]
120pub struct FacetedBrepWithVoids<'m> {
121    slots: Slots<'m>,
122}
123
124impl<'m> FacetedBrepWithVoids<'m> {
125    /// Wrap an entity assumed to be an `IfcFacetedBrepWithVoids`.
126    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
127        Self {
128            slots: Slots::new(id, entity),
129        }
130    }
131
132    /// The entity id.
133    pub fn id(&self) -> EntityId {
134        self.slots.id()
135    }
136
137    /// The inherited `IfcFacetedBrep` attributes.
138    pub fn base(&self) -> FacetedBrep<'m> {
139        FacetedBrep { slots: self.slots }
140    }
141
142    /// The `IfcClosedShell` references to subtract from the outer shell.
143    ///
144    /// Never concatenate these with `Outer`: they are holes, not more surface.
145    pub fn voids(&self) -> GeometryResult<Vec<EntityId>> {
146        self.slots.req_ref_list(slot::VOIDS, "Voids")
147    }
148}
149
150/// `IfcAdvancedBrep`: a brep whose faces are `IfcAdvancedFace`.
151///
152/// The faces carry real surface geometry (planes, cylinders, B-splines) and
153/// edges with curve geometry, so unlike [`FacetedBrep`] the boundaries are not
154/// implied by their vertices. A consumer that treats the vertex loops as
155/// polygons loses every curved edge, which is how a filleted steel section
156/// renders as a crude prism.
157#[derive(Debug, Clone, Copy)]
158pub struct AdvancedBrep<'m> {
159    slots: Slots<'m>,
160}
161
162impl<'m> AdvancedBrep<'m> {
163    /// Wrap an entity assumed to be an `IfcAdvancedBrep`.
164    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
165        Self {
166            slots: Slots::new(id, entity),
167        }
168    }
169
170    /// The entity id.
171    pub fn id(&self) -> EntityId {
172        self.slots.id()
173    }
174
175    /// The inherited `IfcManifoldSolidBrep` attributes.
176    pub fn base(&self) -> ManifoldSolidBrep<'m> {
177        ManifoldSolidBrep { slots: self.slots }
178    }
179}
180
181/// `IfcAdvancedBrepWithVoids`: an advanced brep with internal cavities.
182///
183/// Same subtraction semantics as [`FacetedBrepWithVoids`], with the extra
184/// schema requirement that every void shell's faces are also advanced faces.
185#[derive(Debug, Clone, Copy)]
186pub struct AdvancedBrepWithVoids<'m> {
187    slots: Slots<'m>,
188}
189
190impl<'m> AdvancedBrepWithVoids<'m> {
191    /// Wrap an entity assumed to be an `IfcAdvancedBrepWithVoids`.
192    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
193        Self {
194            slots: Slots::new(id, entity),
195        }
196    }
197
198    /// The entity id.
199    pub fn id(&self) -> EntityId {
200        self.slots.id()
201    }
202
203    /// The inherited `IfcAdvancedBrep` attributes.
204    pub fn base(&self) -> AdvancedBrep<'m> {
205        AdvancedBrep { slots: self.slots }
206    }
207
208    /// The `IfcClosedShell` references to subtract from the outer shell.
209    pub fn voids(&self) -> GeometryResult<Vec<EntityId>> {
210        self.slots.req_ref_list(slot::VOIDS, "Voids")
211    }
212}
213
214/// Faces of a shell that are *not* `IfcAdvancedFace`, in file order.
215///
216/// `IfcConnectedFaceSet.CfsFaces` is slot 0, and `IfcClosedShell` adds no
217/// attributes of its own, so the same read serves both shell kinds.
218///
219/// A missing or unreadable shell yields an empty vector: absence is not a
220/// non-advanced face, and reporting one would invent a violation the file
221/// does not contain.
222pub fn non_advanced_faces(model: &Model, shell: EntityId) -> Vec<EntityId> {
223    let Some(entity) = model.get(shell) else {
224        return Vec::new();
225    };
226    let Some(Value::List(faces)) = entity.attribute(0).map(|v| v.unwrap_typed()) else {
227        return Vec::new();
228    };
229    faces
230        .iter()
231        .filter_map(|v| match v.unwrap_typed() {
232            Value::Ref(id) => Some(*id),
233            _ => None,
234        })
235        .filter(|face| {
236            // An unresolvable face is not evidence of a non-advanced face.
237            model.get(*face).is_some_and(|f| {
238                !crate::select::is_a(&f.type_name.to_ascii_uppercase(), "IFCADVANCEDFACE")
239            })
240        })
241        .collect()
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use crate::solid::testkit::{entity, refs};
248
249    #[test]
250    fn outer_shell_is_slot_zero_for_every_brep_subtype() {
251        for name in [
252            "IFCFACETEDBREP",
253            "IFCADVANCEDBREP",
254            "IFCFACETEDBREPWITHVOIDS",
255            "IFCADVANCEDBREPWITHVOIDS",
256        ] {
257            let e = entity(name, vec![crate::solid::testkit::r(100), refs(&[200])]);
258            let view = ManifoldSolidBrep::new(EntityId(1), &e);
259            assert_eq!(view.outer().unwrap(), EntityId(100), "{name}");
260        }
261    }
262
263    /// Voids are inner shells to subtract; they must stay separate from Outer
264    /// or the cavity renders as extra outward surface.
265    #[test]
266    fn voids_are_kept_separate_from_the_outer_shell() {
267        let e = entity(
268            "IFCFACETEDBREPWITHVOIDS",
269            vec![crate::solid::testkit::r(100), refs(&[201, 202])],
270        );
271        let view = FacetedBrepWithVoids::new(EntityId(1), &e);
272        let outer = view.base().base().outer().unwrap();
273        let voids = view.voids().unwrap();
274
275        assert_eq!(outer, EntityId(100));
276        assert_eq!(voids, vec![EntityId(201), EntityId(202)]);
277        assert!(!voids.contains(&outer), "a void is never the outer shell");
278    }
279
280    #[test]
281    fn advanced_brep_with_voids_uses_the_same_slot_layout() {
282        let e = entity(
283            "IFCADVANCEDBREPWITHVOIDS",
284            vec![crate::solid::testkit::r(7), refs(&[8, 9])],
285        );
286        let view = AdvancedBrepWithVoids::new(EntityId(1), &e);
287        assert_eq!(view.base().base().outer().unwrap(), EntityId(7));
288        assert_eq!(view.voids().unwrap(), vec![EntityId(8), EntityId(9)]);
289    }
290
291    /// The void-carrying subtypes are distinguishable without the caller
292    /// re-deriving the subtype lattice.
293    #[test]
294    fn only_the_with_voids_subtypes_report_carrying_voids() {
295        let plain = entity("IFCFACETEDBREP", vec![crate::solid::testkit::r(1)]);
296        let voided = entity(
297            "IFCFACETEDBREPWITHVOIDS",
298            vec![crate::solid::testkit::r(1), refs(&[2])],
299        );
300        assert!(!ManifoldSolidBrep::new(EntityId(1), &plain).has_voids());
301        assert!(ManifoldSolidBrep::new(EntityId(1), &voided).has_voids());
302    }
303
304    #[test]
305    fn a_brep_missing_its_outer_shell_reports_the_entity() {
306        let e = entity("IFCFACETEDBREP", vec![]);
307        let err = FacetedBrep::new(EntityId(42), &e)
308            .base()
309            .outer()
310            .unwrap_err();
311        assert_eq!(err.entity(), Some(EntityId(42)));
312        assert!(err.to_string().contains("Outer"));
313    }
314}