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            | "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}