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}