ifc_geometry/solid/mod.rs
1//! `IfcGeometricModelResource`: solids, tessellation and booleans.
2//!
3//! # What this module is
4//!
5//! Read-only typed views over the 42 entities of the IFC4 ADD2 TC1
6//! `IfcGeometricModelResource` schema. Each view is a newtype over
7//! `(EntityId, &Entity)` with named accessors and a `mod slot` block citing the
8//! EXPRESS declaration. Views borrow and own nothing, so constructing one is
9//! free and the model remains the single source of truth.
10//!
11//! # What this module deliberately does NOT do
12//!
13//! It does not triangulate, evaluate booleans, build meshes, or resolve units.
14//! Those belong to a geometry kernel and to [`crate::units`]. A view's job is
15//! to say faithfully what the file contains, including the parts a naive reader
16//! gets wrong.
17//!
18//! # The three counting conventions that must not be confused
19//!
20//! 1. **Attribute slots are absolute.** Inherited attributes come first, so
21//! `IfcExtrudedAreaSolid.SweptArea` is slot 0 (inherited from
22//! `IfcSweptAreaSolid`) and `Depth` is slot 3. Local, per-subtype numbering
23//! is never used anywhere in this module.
24//! 2. **Vertex indices are 1-based.** `CoordIndex`, `PnIndex` and
25//! `InnerCoordIndices` all count from 1. See [`tessellated`].
26//! 3. **Measures are in file units.** Lengths and angles are returned raw;
27//! `IfcRevolvedAreaSolid.Angle` is very often in degrees.
28//!
29//! # The map from schema to module
30//!
31//! | Module | Entities |
32//! | --- | --- |
33//! | [`swept`] | the 9 swept solids plus `IfcSectionedSpine` |
34//! | [`brep`] | the 5 `IfcManifoldSolidBrep` types |
35//! | [`csg`] | `IfcCsgSolid` plus the 6 primitive types |
36//! | [`halfspace`] | the 3 half spaces |
37//! | [`boolean`] | `IfcBooleanResult`, `IfcBooleanClippingResult` |
38//! | [`tessellated`] | the 6 tessellation types |
39//! | [`surface_model`] | the 2 surface models and 2 geometric sets |
40//! | [`bbox`] | `IfcBoundingBox` |
41//!
42//! # Volume semantics, in one place
43//!
44//! Only some of these entities enclose a volume, and confusing the categories
45//! is the most consequential mistake a consumer can make:
46//!
47//! - **Finite solids**: swept solids, breps, CSG solids and primitives.
48//! - **Infinite**: `IfcHalfSpaceSolid` and its subtypes. Only ever a boolean
49//! operand; never tessellate one on its own. See [`halfspace`].
50//! - **Conditionally solid**: the tessellated face sets, but only when
51//! `Closed` is TRUE.
52//! - **Never solid**: the surface models and geometric sets in
53//! [`surface_model`], and `IfcBoundingBox`, which is a proxy extent.
54//!
55//! [`SolidKind::classify`] turns that table into a dispatch a caller can use
56//! rather than re-deriving from type names.
57
58pub mod bbox;
59pub mod boolean;
60pub mod brep;
61pub mod csg;
62pub mod halfspace;
63pub mod surface_model;
64pub mod swept;
65pub mod tessellated;
66
67#[cfg(test)]
68pub(crate) mod testkit;
69
70pub use bbox::BoundingBox;
71pub use boolean::{
72 BooleanClippingResult, BooleanOperator, BooleanResult, IfcBooleanOperator, OperandKind,
73 ParseIfcBooleanOperatorError,
74};
75pub use brep::{
76 AdvancedBrep, AdvancedBrepWithVoids, FacetedBrep, FacetedBrepWithVoids, ManifoldSolidBrep,
77};
78pub use csg::{
79 Block, CsgPrimitive3D, CsgSolid, RectangularPyramid, RightCircularCone, RightCircularCylinder,
80 Sphere,
81};
82pub use halfspace::{BoxedHalfSpace, HalfSpaceSolid, PolygonalBoundedHalfSpace};
83pub use surface_model::{
84 FaceBasedSurfaceModel, GeometricCurveSet, GeometricSet, ShellBasedSurfaceModel,
85};
86pub use swept::{
87 ExtrudedAreaSolid, ExtrudedAreaSolidTapered, FixedReferenceSweptAreaSolid, RevolvedAreaSolid,
88 RevolvedAreaSolidTapered, SectionedSpine, SurfaceCurveSweptAreaSolid, SweptAreaSolid,
89 SweptDiskSolid, SweptDiskSolidPolygonal, TrimMeasure,
90};
91pub use tessellated::{
92 IndexedPolygonalFace, IndexedPolygonalFaceWithVoids, PolygonalFaceSet, TessellatedFaceSet,
93 TessellatedItem, TriangulatedFaceSet,
94};
95
96/// Which family of `IfcGeometricModelResource` an entity belongs to.
97///
98/// # Why classify at all
99///
100/// A representation item arrives as an untyped entity, and the correct handling
101/// differs by family in ways that are not recoverable later: a half space must
102/// be routed into a boolean, a boolean must be walked as a tree, a face set
103/// must have its 1-based indices converted. Deciding once, by name, keeps that
104/// dispatch out of every call site and keeps the volume semantics table in a
105/// single place.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum SolidKind {
108 /// A swept solid: extrusion, revolution, directrix sweep or swept disk.
109 Swept,
110 /// A boundary representation.
111 Brep,
112 /// A CSG solid or one of the analytic primitives.
113 Csg,
114 /// A half space. **Infinite**; only valid as a boolean operand.
115 HalfSpace,
116 /// A boolean result. Its operands form a tree and may nest.
117 Boolean,
118 /// A tessellated face set. Solid only when its `Closed` flag is TRUE.
119 Tessellated,
120 /// A surface model or geometric set. Never encloses a volume.
121 SurfaceModel,
122 /// An `IfcBoundingBox`: a proxy extent, not geometry to build.
123 BoundingBox,
124 /// `IfcSectionedSpine`: cross sections along a spine.
125 ///
126 /// Kept apart from [`Self::Swept`] because it subtypes
127 /// `IfcGeometricRepresentationItem` directly, has a different slot layout,
128 /// and needs interpolation a plain sweep does not.
129 SectionedSpine,
130}
131
132impl SolidKind {
133 /// Classify an IFC type name, case-insensitively.
134 ///
135 /// Returns `None` for anything outside this schema, so a caller can pass
136 /// any representation item and route the rest elsewhere.
137 pub fn classify(type_name: &str) -> Option<Self> {
138 let n = type_name.to_ascii_uppercase();
139 let kind = match n.as_str() {
140 "IFCSWEPTAREASOLID"
141 | "IFCEXTRUDEDAREASOLID"
142 | "IFCEXTRUDEDAREASOLIDTAPERED"
143 | "IFCREVOLVEDAREASOLID"
144 | "IFCREVOLVEDAREASOLIDTAPERED"
145 | "IFCSURFACECURVESWEPTAREASOLID"
146 | "IFCFIXEDREFERENCESWEPTAREASOLID"
147 | "IFCDIRECTRIXDERIVEDREFERENCESWEPTAREASOLID"
148 | "IFCSWEPTDISKSOLID"
149 | "IFCSWEPTDISKSOLIDPOLYGONAL" => Self::Swept,
150 "IFCSECTIONEDSPINE" => Self::SectionedSpine,
151 "IFCMANIFOLDSOLIDBREP"
152 | "IFCFACETEDBREP"
153 | "IFCFACETEDBREPWITHVOIDS"
154 | "IFCADVANCEDBREP"
155 | "IFCADVANCEDBREPWITHVOIDS" => Self::Brep,
156 "IFCCSGSOLID"
157 | "IFCCSGPRIMITIVE3D"
158 | "IFCBLOCK"
159 | "IFCRECTANGULARPYRAMID"
160 | "IFCRIGHTCIRCULARCONE"
161 | "IFCRIGHTCIRCULARCYLINDER"
162 | "IFCSPHERE" => Self::Csg,
163 "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
164 Self::HalfSpace
165 }
166 "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => Self::Boolean,
167 "IFCTESSELLATEDITEM"
168 | "IFCTESSELLATEDFACESET"
169 | "IFCTRIANGULATEDFACESET"
170 | "IFCTRIANGULATEDIRREGULARNETWORK"
171 | "IFCPOLYGONALFACESET"
172 | "IFCINDEXEDPOLYGONALFACE"
173 | "IFCINDEXEDPOLYGONALFACEWITHVOIDS" => Self::Tessellated,
174 "IFCSHELLBASEDSURFACEMODEL"
175 | "IFCFACEBASEDSURFACEMODEL"
176 | "IFCGEOMETRICSET"
177 | "IFCGEOMETRICCURVESET" => Self::SurfaceModel,
178 "IFCBOUNDINGBOX" => Self::BoundingBox,
179 _ => return None,
180 };
181 Some(kind)
182 }
183
184 /// Does this family enclose a finite volume on its own?
185 ///
186 /// `false` for half spaces (infinite), booleans (depends on the operands),
187 /// tessellated sets (depends on `Closed`), surface models and bounding
188 /// boxes. A `true` here is the only safe licence to compute a volume
189 /// without further checks.
190 pub fn is_finite_solid(self) -> bool {
191 matches!(
192 self,
193 Self::Swept | Self::Brep | Self::Csg | Self::SectionedSpine
194 )
195 }
196
197 /// Must this be routed through a boolean rather than built directly?
198 ///
199 /// `true` only for half spaces. Anything else may be built standalone,
200 /// though not everything built will have a volume.
201 pub fn requires_boolean_context(self) -> bool {
202 matches!(self, Self::HalfSpace)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 /// Every entity this module claims to cover must classify. The list is the
211 /// module's contract against the schema, so a gap here is a gap in the
212 /// coverage claim.
213 #[test]
214 fn every_geometric_model_resource_entity_classifies() {
215 let expected: &[(&str, SolidKind)] = &[
216 // swept: 10
217 ("IfcSweptAreaSolid", SolidKind::Swept),
218 ("IfcExtrudedAreaSolid", SolidKind::Swept),
219 ("IfcExtrudedAreaSolidTapered", SolidKind::Swept),
220 ("IfcRevolvedAreaSolid", SolidKind::Swept),
221 ("IfcRevolvedAreaSolidTapered", SolidKind::Swept),
222 ("IfcSurfaceCurveSweptAreaSolid", SolidKind::Swept),
223 ("IfcFixedReferenceSweptAreaSolid", SolidKind::Swept),
224 ("IfcSweptDiskSolid", SolidKind::Swept),
225 ("IfcSweptDiskSolidPolygonal", SolidKind::Swept),
226 ("IfcSectionedSpine", SolidKind::SectionedSpine),
227 // brep: 5
228 ("IfcManifoldSolidBrep", SolidKind::Brep),
229 ("IfcFacetedBrep", SolidKind::Brep),
230 ("IfcFacetedBrepWithVoids", SolidKind::Brep),
231 ("IfcAdvancedBrep", SolidKind::Brep),
232 ("IfcAdvancedBrepWithVoids", SolidKind::Brep),
233 // csg: 7
234 ("IfcCsgSolid", SolidKind::Csg),
235 ("IfcCsgPrimitive3D", SolidKind::Csg),
236 ("IfcBlock", SolidKind::Csg),
237 ("IfcRectangularPyramid", SolidKind::Csg),
238 ("IfcRightCircularCone", SolidKind::Csg),
239 ("IfcRightCircularCylinder", SolidKind::Csg),
240 ("IfcSphere", SolidKind::Csg),
241 // halfspace: 3
242 ("IfcHalfSpaceSolid", SolidKind::HalfSpace),
243 ("IfcBoxedHalfSpace", SolidKind::HalfSpace),
244 ("IfcPolygonalBoundedHalfSpace", SolidKind::HalfSpace),
245 // boolean: 2
246 ("IfcBooleanResult", SolidKind::Boolean),
247 ("IfcBooleanClippingResult", SolidKind::Boolean),
248 // tessellated: 6
249 ("IfcTessellatedItem", SolidKind::Tessellated),
250 ("IfcTessellatedFaceSet", SolidKind::Tessellated),
251 ("IfcTriangulatedFaceSet", SolidKind::Tessellated),
252 ("IfcPolygonalFaceSet", SolidKind::Tessellated),
253 ("IfcIndexedPolygonalFace", SolidKind::Tessellated),
254 ("IfcIndexedPolygonalFaceWithVoids", SolidKind::Tessellated),
255 // surface models and sets: 4
256 ("IfcShellBasedSurfaceModel", SolidKind::SurfaceModel),
257 ("IfcFaceBasedSurfaceModel", SolidKind::SurfaceModel),
258 ("IfcGeometricSet", SolidKind::SurfaceModel),
259 ("IfcGeometricCurveSet", SolidKind::SurfaceModel),
260 // bbox: 1
261 ("IfcBoundingBox", SolidKind::BoundingBox),
262 ];
263
264 for (name, kind) in expected {
265 assert_eq!(
266 SolidKind::classify(name),
267 Some(*kind),
268 "{name} must classify"
269 );
270 // STEP is case-insensitive, and files are written upper-cased.
271 assert_eq!(
272 SolidKind::classify(&name.to_ascii_uppercase()),
273 Some(*kind),
274 "{name} must classify upper-cased"
275 );
276 }
277
278 // 38 concrete plus abstract types here; the schema's 42 entities also
279 // include IfcSolidModel and the abstract roots handled by their
280 // subtypes, plus IfcCartesianPointList2D/3D which belong to
281 // IfcGeometryResource in this crate's split.
282 assert_eq!(expected.len(), 38, "coverage list must not shrink silently");
283 }
284
285 #[test]
286 fn entities_outside_this_schema_do_not_classify() {
287 for name in ["IfcWall", "IfcPolyline", "IfcCartesianPoint", "IfcPlane"] {
288 assert_eq!(SolidKind::classify(name), None, "{name}");
289 }
290 }
291
292 /// The volume-semantics table from the module docs, as an assertion.
293 #[test]
294 fn only_genuinely_bounded_families_report_a_finite_volume() {
295 for kind in [
296 SolidKind::Swept,
297 SolidKind::Brep,
298 SolidKind::Csg,
299 SolidKind::SectionedSpine,
300 ] {
301 assert!(kind.is_finite_solid(), "{kind:?}");
302 }
303 for kind in [
304 SolidKind::HalfSpace,
305 SolidKind::Boolean,
306 SolidKind::Tessellated,
307 SolidKind::SurfaceModel,
308 SolidKind::BoundingBox,
309 ] {
310 assert!(!kind.is_finite_solid(), "{kind:?}");
311 }
312 }
313
314 /// A half space is the only thing that cannot stand alone at all.
315 #[test]
316 fn only_half_spaces_require_a_boolean_context() {
317 assert!(SolidKind::HalfSpace.requires_boolean_context());
318 for kind in [
319 SolidKind::Swept,
320 SolidKind::Brep,
321 SolidKind::Csg,
322 SolidKind::Boolean,
323 SolidKind::Tessellated,
324 SolidKind::SurfaceModel,
325 SolidKind::BoundingBox,
326 SolidKind::SectionedSpine,
327 ] {
328 assert!(!kind.requires_boolean_context(), "{kind:?}");
329 }
330 }
331
332 /// IfcSectionedSpine is a swept-ish solid with a different slot layout, so
333 /// it must not be lumped in with the IfcSweptAreaSolid branch.
334 #[test]
335 fn sectioned_spine_is_classified_apart_from_the_swept_area_family() {
336 assert_ne!(
337 SolidKind::classify("IfcSectionedSpine"),
338 SolidKind::classify("IfcExtrudedAreaSolid")
339 );
340 }
341}