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 | "IFCSWEPTDISKSOLID"
148 | "IFCSWEPTDISKSOLIDPOLYGONAL" => Self::Swept,
149 "IFCSECTIONEDSPINE" => Self::SectionedSpine,
150 "IFCMANIFOLDSOLIDBREP"
151 | "IFCFACETEDBREP"
152 | "IFCFACETEDBREPWITHVOIDS"
153 | "IFCADVANCEDBREP"
154 | "IFCADVANCEDBREPWITHVOIDS" => Self::Brep,
155 "IFCCSGSOLID"
156 | "IFCCSGPRIMITIVE3D"
157 | "IFCBLOCK"
158 | "IFCRECTANGULARPYRAMID"
159 | "IFCRIGHTCIRCULARCONE"
160 | "IFCRIGHTCIRCULARCYLINDER"
161 | "IFCSPHERE" => Self::Csg,
162 "IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
163 Self::HalfSpace
164 }
165 "IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => Self::Boolean,
166 "IFCTESSELLATEDITEM"
167 | "IFCTESSELLATEDFACESET"
168 | "IFCTRIANGULATEDFACESET"
169 | "IFCPOLYGONALFACESET"
170 | "IFCINDEXEDPOLYGONALFACE"
171 | "IFCINDEXEDPOLYGONALFACEWITHVOIDS" => Self::Tessellated,
172 "IFCSHELLBASEDSURFACEMODEL"
173 | "IFCFACEBASEDSURFACEMODEL"
174 | "IFCGEOMETRICSET"
175 | "IFCGEOMETRICCURVESET" => Self::SurfaceModel,
176 "IFCBOUNDINGBOX" => Self::BoundingBox,
177 _ => return None,
178 };
179 Some(kind)
180 }
181
182 /// Does this family enclose a finite volume on its own?
183 ///
184 /// `false` for half spaces (infinite), booleans (depends on the operands),
185 /// tessellated sets (depends on `Closed`), surface models and bounding
186 /// boxes. A `true` here is the only safe licence to compute a volume
187 /// without further checks.
188 pub fn is_finite_solid(self) -> bool {
189 matches!(
190 self,
191 Self::Swept | Self::Brep | Self::Csg | Self::SectionedSpine
192 )
193 }
194
195 /// Must this be routed through a boolean rather than built directly?
196 ///
197 /// `true` only for half spaces. Anything else may be built standalone,
198 /// though not everything built will have a volume.
199 pub fn requires_boolean_context(self) -> bool {
200 matches!(self, Self::HalfSpace)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 /// Every entity this module claims to cover must classify. The list is the
209 /// module's contract against the schema, so a gap here is a gap in the
210 /// coverage claim.
211 #[test]
212 fn every_geometric_model_resource_entity_classifies() {
213 let expected: &[(&str, SolidKind)] = &[
214 // swept: 10
215 ("IfcSweptAreaSolid", SolidKind::Swept),
216 ("IfcExtrudedAreaSolid", SolidKind::Swept),
217 ("IfcExtrudedAreaSolidTapered", SolidKind::Swept),
218 ("IfcRevolvedAreaSolid", SolidKind::Swept),
219 ("IfcRevolvedAreaSolidTapered", SolidKind::Swept),
220 ("IfcSurfaceCurveSweptAreaSolid", SolidKind::Swept),
221 ("IfcFixedReferenceSweptAreaSolid", SolidKind::Swept),
222 ("IfcSweptDiskSolid", SolidKind::Swept),
223 ("IfcSweptDiskSolidPolygonal", SolidKind::Swept),
224 ("IfcSectionedSpine", SolidKind::SectionedSpine),
225 // brep: 5
226 ("IfcManifoldSolidBrep", SolidKind::Brep),
227 ("IfcFacetedBrep", SolidKind::Brep),
228 ("IfcFacetedBrepWithVoids", SolidKind::Brep),
229 ("IfcAdvancedBrep", SolidKind::Brep),
230 ("IfcAdvancedBrepWithVoids", SolidKind::Brep),
231 // csg: 7
232 ("IfcCsgSolid", SolidKind::Csg),
233 ("IfcCsgPrimitive3D", SolidKind::Csg),
234 ("IfcBlock", SolidKind::Csg),
235 ("IfcRectangularPyramid", SolidKind::Csg),
236 ("IfcRightCircularCone", SolidKind::Csg),
237 ("IfcRightCircularCylinder", SolidKind::Csg),
238 ("IfcSphere", SolidKind::Csg),
239 // halfspace: 3
240 ("IfcHalfSpaceSolid", SolidKind::HalfSpace),
241 ("IfcBoxedHalfSpace", SolidKind::HalfSpace),
242 ("IfcPolygonalBoundedHalfSpace", SolidKind::HalfSpace),
243 // boolean: 2
244 ("IfcBooleanResult", SolidKind::Boolean),
245 ("IfcBooleanClippingResult", SolidKind::Boolean),
246 // tessellated: 6
247 ("IfcTessellatedItem", SolidKind::Tessellated),
248 ("IfcTessellatedFaceSet", SolidKind::Tessellated),
249 ("IfcTriangulatedFaceSet", SolidKind::Tessellated),
250 ("IfcPolygonalFaceSet", SolidKind::Tessellated),
251 ("IfcIndexedPolygonalFace", SolidKind::Tessellated),
252 ("IfcIndexedPolygonalFaceWithVoids", SolidKind::Tessellated),
253 // surface models and sets: 4
254 ("IfcShellBasedSurfaceModel", SolidKind::SurfaceModel),
255 ("IfcFaceBasedSurfaceModel", SolidKind::SurfaceModel),
256 ("IfcGeometricSet", SolidKind::SurfaceModel),
257 ("IfcGeometricCurveSet", SolidKind::SurfaceModel),
258 // bbox: 1
259 ("IfcBoundingBox", SolidKind::BoundingBox),
260 ];
261
262 for (name, kind) in expected {
263 assert_eq!(
264 SolidKind::classify(name),
265 Some(*kind),
266 "{name} must classify"
267 );
268 // STEP is case-insensitive, and files are written upper-cased.
269 assert_eq!(
270 SolidKind::classify(&name.to_ascii_uppercase()),
271 Some(*kind),
272 "{name} must classify upper-cased"
273 );
274 }
275
276 // 38 concrete plus abstract types here; the schema's 42 entities also
277 // include IfcSolidModel and the abstract roots handled by their
278 // subtypes, plus IfcCartesianPointList2D/3D which belong to
279 // IfcGeometryResource in this crate's split.
280 assert_eq!(expected.len(), 38, "coverage list must not shrink silently");
281 }
282
283 #[test]
284 fn entities_outside_this_schema_do_not_classify() {
285 for name in ["IfcWall", "IfcPolyline", "IfcCartesianPoint", "IfcPlane"] {
286 assert_eq!(SolidKind::classify(name), None, "{name}");
287 }
288 }
289
290 /// The volume-semantics table from the module docs, as an assertion.
291 #[test]
292 fn only_genuinely_bounded_families_report_a_finite_volume() {
293 for kind in [
294 SolidKind::Swept,
295 SolidKind::Brep,
296 SolidKind::Csg,
297 SolidKind::SectionedSpine,
298 ] {
299 assert!(kind.is_finite_solid(), "{kind:?}");
300 }
301 for kind in [
302 SolidKind::HalfSpace,
303 SolidKind::Boolean,
304 SolidKind::Tessellated,
305 SolidKind::SurfaceModel,
306 SolidKind::BoundingBox,
307 ] {
308 assert!(!kind.is_finite_solid(), "{kind:?}");
309 }
310 }
311
312 /// A half space is the only thing that cannot stand alone at all.
313 #[test]
314 fn only_half_spaces_require_a_boolean_context() {
315 assert!(SolidKind::HalfSpace.requires_boolean_context());
316 for kind in [
317 SolidKind::Swept,
318 SolidKind::Brep,
319 SolidKind::Csg,
320 SolidKind::Boolean,
321 SolidKind::Tessellated,
322 SolidKind::SurfaceModel,
323 SolidKind::BoundingBox,
324 SolidKind::SectionedSpine,
325 ] {
326 assert!(!kind.requires_boolean_context(), "{kind:?}");
327 }
328 }
329
330 /// IfcSectionedSpine is a swept-ish solid with a different slot layout, so
331 /// it must not be lumped in with the IfcSweptAreaSolid branch.
332 #[test]
333 fn sectioned_spine_is_classified_apart_from_the_swept_area_family() {
334 assert_ne!(
335 SolidKind::classify("IfcSectionedSpine"),
336 SolidKind::classify("IfcExtrudedAreaSolid")
337 );
338 }
339}