Skip to main content

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}