ifc_geometry/solid/surface_model.rs
1//! Surface models and geometric sets: collections that are **not** solids.
2//!
3//! # Why these are grouped
4//!
5//! All four entities here are collections of geometry with no enclosed volume
6//! guaranteed. That distinction is the point of the module: a
7//! `IfcShellBasedSurfaceModel` looks like a brep and is not one, and code that
8//! treats it as a solid computes volumes and material quantities from an open
9//! surface.
10//!
11//! - [`ShellBasedSurfaceModel`] holds `IfcShell` (open **or** closed shells).
12//! Even a closed shell here carries no solid semantics: the entity is a
13//! surface model by declaration.
14//! - [`FaceBasedSurfaceModel`] holds `IfcConnectedFaceSet`s, which are not
15//! required to be closed or even connected to each other.
16//! - [`GeometricSet`] is a heterogeneous bag of points, curves and surfaces.
17//! - [`GeometricCurveSet`] is a `GeometricSet` the schema forbids from
18//! containing surfaces -- so it is curves and points only.
19//!
20//! # Shells and face sets are not resolved here
21//!
22//! `IfcClosedShell`, `IfcOpenShell` and `IfcConnectedFaceSet` belong to
23//! `IfcTopologyResource`, owned elsewhere. These views return `EntityId`s.
24
25use crate::error::GeometryResult;
26use crate::slots::Slots;
27use ifc_model::{Entity, EntityId};
28
29/// Surface model and geometric set slots.
30///
31/// EXPRESS (IFC4 ADD2 TC1): all four entities subtype
32/// `IfcGeometricRepresentationItem`, which declares no explicit attributes, so
33/// each one's single collection attribute is absolute slot 0.
34/// `IfcGeometricCurveSet` adds nothing and inherits `Elements` at slot 0.
35pub(crate) mod slot {
36 /// `SbsmBoundary : SET [1:?] OF IfcShell`, on `IfcShellBasedSurfaceModel`.
37 pub const SBSM_BOUNDARY: usize = 0;
38 /// `FbsmFaces : SET [1:?] OF IfcConnectedFaceSet`, on the face-based
39 /// surface model.
40 pub const FBSM_FACES: usize = 0;
41 /// `Elements : SET [1:?] OF IfcGeometricSetSelect`, on `IfcGeometricSet`.
42 pub const ELEMENTS: usize = 0;
43}
44
45/// `IfcShellBasedSurfaceModel`: a surface described by one or more shells.
46///
47/// # Not a solid
48///
49/// `SbsmBoundary` is an `IfcShell` SELECT, so its members may be
50/// `IfcClosedShell` or `IfcOpenShell`. Even when every shell is closed, the
51/// entity declares a **surface model**, not a `IfcSolidModel`, and it is not a
52/// legal boolean operand. Promoting it to a brep produces volume figures that a
53/// quantity takeoff will happily report.
54#[derive(Debug, Clone, Copy)]
55pub struct ShellBasedSurfaceModel<'m> {
56 slots: Slots<'m>,
57}
58
59impl<'m> ShellBasedSurfaceModel<'m> {
60 /// Wrap an entity assumed to be an `IfcShellBasedSurfaceModel`.
61 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
62 Self {
63 slots: Slots::new(id, entity),
64 }
65 }
66
67 /// The entity id.
68 pub fn id(&self) -> EntityId {
69 self.slots.id()
70 }
71
72 /// The `IfcShell` references bounding the model.
73 ///
74 /// Each is an `IfcClosedShell` or an `IfcOpenShell`; the caller must check
75 /// the resolved type rather than assuming either.
76 pub fn shells(&self) -> GeometryResult<Vec<EntityId>> {
77 self.slots.req_ref_list(slot::SBSM_BOUNDARY, "SbsmBoundary")
78 }
79}
80
81/// `IfcFaceBasedSurfaceModel`: a surface described by connected face sets.
82///
83/// The face sets need not be closed and need not connect to one another, so
84/// this is the loosest surface container in the schema. It is frequently what
85/// an exporter falls back to when it cannot produce a valid solid, which makes
86/// its presence a useful signal about a file's quality.
87#[derive(Debug, Clone, Copy)]
88pub struct FaceBasedSurfaceModel<'m> {
89 slots: Slots<'m>,
90}
91
92impl<'m> FaceBasedSurfaceModel<'m> {
93 /// Wrap an entity assumed to be an `IfcFaceBasedSurfaceModel`.
94 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
95 Self {
96 slots: Slots::new(id, entity),
97 }
98 }
99
100 /// The entity id.
101 pub fn id(&self) -> EntityId {
102 self.slots.id()
103 }
104
105 /// The `IfcConnectedFaceSet` references making up the model.
106 pub fn face_sets(&self) -> GeometryResult<Vec<EntityId>> {
107 self.slots.req_ref_list(slot::FBSM_FACES, "FbsmFaces")
108 }
109}
110
111/// `IfcGeometricSet`: a heterogeneous collection of points, curves and
112/// surfaces.
113///
114/// `Elements` is an `IfcGeometricSetSelect`, whose members may be `IfcPoint`,
115/// `IfcCurve` or `IfcSurface`. The schema requires every member to share the
116/// same dimensionality, but nothing more: a set may mix a curve and a surface
117/// freely. Consumers must dispatch per element rather than sampling the first.
118#[derive(Debug, Clone, Copy)]
119pub struct GeometricSet<'m> {
120 slots: Slots<'m>,
121}
122
123impl<'m> GeometricSet<'m> {
124 /// Wrap an entity assumed to be an `IfcGeometricSet` or subtype.
125 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
126 Self {
127 slots: Slots::new(id, entity),
128 }
129 }
130
131 /// The entity id.
132 pub fn id(&self) -> EntityId {
133 self.slots.id()
134 }
135
136 /// The IFC type name, naming the concrete subtype.
137 pub fn type_name(&self) -> &'m str {
138 self.slots.type_name()
139 }
140
141 /// The `IfcGeometricSetSelect` references in the set.
142 ///
143 /// Returned unresolved and unsorted: each may be a point, a curve or (on
144 /// the base type only) a surface.
145 pub fn elements(&self) -> GeometryResult<Vec<EntityId>> {
146 self.slots.req_ref_list(slot::ELEMENTS, "Elements")
147 }
148
149 /// Is this the curves-and-points-only specialisation?
150 pub fn is_curve_set(&self) -> bool {
151 self.type_name()
152 .eq_ignore_ascii_case("IFCGEOMETRICCURVESET")
153 }
154}
155
156/// `IfcGeometricCurveSet`: a geometric set with no surfaces.
157///
158/// Adds no attributes; it is the EXPRESS `NoSurfaces` WHERE rule made into a
159/// type. That guarantee is worth having because it lets a consumer skip surface
160/// handling for the annotation and 2D-plan geometry these usually carry.
161#[derive(Debug, Clone, Copy)]
162pub struct GeometricCurveSet<'m> {
163 slots: Slots<'m>,
164}
165
166impl<'m> GeometricCurveSet<'m> {
167 /// Wrap an entity assumed to be an `IfcGeometricCurveSet`.
168 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
169 Self {
170 slots: Slots::new(id, entity),
171 }
172 }
173
174 /// The entity id.
175 pub fn id(&self) -> EntityId {
176 self.slots.id()
177 }
178
179 /// The inherited `IfcGeometricSet` attributes.
180 pub fn base(&self) -> GeometricSet<'m> {
181 GeometricSet { slots: self.slots }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::solid::testkit::{entity, refs};
189
190 /// A shell-based surface model is not a brep, even when its shells are
191 /// closed; it declares no volume.
192 #[test]
193 fn shell_based_surface_model_exposes_shells_without_claiming_a_solid() {
194 let e = entity("IFCSHELLBASEDSURFACEMODEL", vec![refs(&[10, 11])]);
195 let view = ShellBasedSurfaceModel::new(EntityId(1), &e);
196 assert_eq!(view.shells().unwrap(), vec![EntityId(10), EntityId(11)]);
197 }
198
199 #[test]
200 fn face_based_surface_model_exposes_its_connected_face_sets() {
201 let e = entity("IFCFACEBASEDSURFACEMODEL", vec![refs(&[20, 21, 22])]);
202 let view = FaceBasedSurfaceModel::new(EntityId(1), &e);
203 assert_eq!(
204 view.face_sets().unwrap(),
205 vec![EntityId(20), EntityId(21), EntityId(22)]
206 );
207 }
208
209 #[test]
210 fn geometric_set_elements_are_returned_unresolved_and_in_order() {
211 let e = entity("IFCGEOMETRICSET", vec![refs(&[30, 31, 32])]);
212 let view = GeometricSet::new(EntityId(1), &e);
213 assert_eq!(
214 view.elements().unwrap(),
215 vec![EntityId(30), EntityId(31), EntityId(32)]
216 );
217 assert!(!view.is_curve_set());
218 }
219
220 /// The curve set shares the base layout exactly, so Elements is still
221 /// slot 0 on the subtype.
222 #[test]
223 fn curve_set_inherits_elements_at_the_same_slot() {
224 let e = entity("IFCGEOMETRICCURVESET", vec![refs(&[40, 41])]);
225 let view = GeometricCurveSet::new(EntityId(1), &e);
226 assert_eq!(
227 view.base().elements().unwrap(),
228 vec![EntityId(40), EntityId(41)]
229 );
230 assert!(view.base().is_curve_set());
231 }
232
233 #[test]
234 fn an_empty_collection_slot_reports_the_missing_attribute() {
235 let e = entity("IFCSHELLBASEDSURFACEMODEL", vec![]);
236 let err = ShellBasedSurfaceModel::new(EntityId(5), &e)
237 .shells()
238 .unwrap_err();
239 assert_eq!(err.entity(), Some(EntityId(5)));
240 assert!(err.to_string().contains("SbsmBoundary"));
241 }
242}