Skip to main content

ifc_geometry/surface/
mod.rs

1//! Typed read-only views of the `IfcSurface` family.
2//!
3//! # The three branches
4//!
5//! `IfcSurface` is abstract and splits into:
6//!
7//! - [`elementary`]: analytic surfaces placed by an `IfcAxis2Placement3D`.
8//!   Infinite. A file that means a finite piece wraps them.
9//! - [`swept`]: a profile moved along a line or around an axis.
10//! - [`bounded`]: finite patches, bounded either by curves or by a rectangle
11//!   in parameter space.
12//! - [`bspline`]: free-form NURBS patches.
13//!
14//! # Everything except the bounded branch is infinite
15//!
16//! `IfcPlane`, `IfcCylindricalSurface`, `IfcSphericalSurface` and
17//! `IfcToroidalSurface` have no extent of their own; the sphere and torus are
18//! closed and so are finite in area, but the plane and cylinder are not. They
19//! appear as the `BasisSurface` of a bounded surface, as the base of an
20//! `IfcHalfSpaceSolid`, or as the carrier of an `IfcFaceSurface`, and it is
21//! that container which supplies the extent. Rendering a bare `IfcPlane`
22//! produces a plane the size of the world.
23//!
24//! # Parameter units are not all lengths
25//!
26//! Every surface has a `(u, v)` parameterisation, and on the non-planar
27//! elementary surfaces one or both parameters are *angles*. Applying the
28//! model's length scale to them corrupts the geometry silently. See
29//! [`ParameterKind`], which each elementary surface reports.
30//!
31//! # What is deliberately absent
32//!
33//! No evaluation, no tessellation, no NURBS basis functions. These views hand
34//! a kernel the parameters; the kernel builds the surface.
35
36pub mod basis;
37pub mod bounded;
38pub mod bspline;
39pub mod elementary;
40pub mod swept;
41
42pub use bounded::{
43    CurveBoundedPlane, CurveBoundedSurface, RectangularTrimmedSurface, TrimRectangle,
44};
45pub use bspline::{BSplineSurface, BSplineSurfaceForm, ControlPointGrid};
46pub use elementary::{CylindricalSurface, ParameterKind, Plane, SphericalSurface, ToroidalSurface};
47pub use swept::{SurfaceOfLinearExtrusion, SurfaceOfRevolution};
48
49/// Which `IfcSurface` subtype an entity is.
50///
51/// Dispatch on the type name once, then construct the matching view. Returning
52/// `None` rather than an error keeps "not a surface" separate from "a surface
53/// we cannot read", which callers scanning a representation need.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum SurfaceKind {
56    /// `IfcPlane`.
57    Plane,
58    /// `IfcCylindricalSurface`.
59    CylindricalSurface,
60    /// `IfcSphericalSurface`.
61    SphericalSurface,
62    /// `IfcToroidalSurface`.
63    ToroidalSurface,
64    /// `IfcSurfaceOfLinearExtrusion`.
65    SurfaceOfLinearExtrusion,
66    /// `IfcSurfaceOfRevolution`.
67    SurfaceOfRevolution,
68    /// `IfcCurveBoundedPlane`.
69    CurveBoundedPlane,
70    /// `IfcCurveBoundedSurface`.
71    CurveBoundedSurface,
72    /// `IfcRectangularTrimmedSurface`.
73    RectangularTrimmedSurface,
74    /// `IfcBSplineSurface` and its non-knotted form.
75    BSplineSurface,
76    /// `IfcBSplineSurfaceWithKnots`.
77    BSplineSurfaceWithKnots,
78    /// `IfcRationalBSplineSurfaceWithKnots`.
79    RationalBSplineSurfaceWithKnots,
80}
81
82impl SurfaceKind {
83    /// Classify by IFC type name, `None` if not an `IfcSurface` subtype.
84    pub fn classify(type_name: &str) -> Option<Self> {
85        match type_name.to_ascii_uppercase().as_str() {
86            "IFCPLANE" => Some(Self::Plane),
87            "IFCCYLINDRICALSURFACE" => Some(Self::CylindricalSurface),
88            "IFCSPHERICALSURFACE" => Some(Self::SphericalSurface),
89            "IFCTOROIDALSURFACE" => Some(Self::ToroidalSurface),
90            "IFCSURFACEOFLINEAREXTRUSION" => Some(Self::SurfaceOfLinearExtrusion),
91            "IFCSURFACEOFREVOLUTION" => Some(Self::SurfaceOfRevolution),
92            "IFCCURVEBOUNDEDPLANE" => Some(Self::CurveBoundedPlane),
93            "IFCCURVEBOUNDEDSURFACE" => Some(Self::CurveBoundedSurface),
94            "IFCRECTANGULARTRIMMEDSURFACE" => Some(Self::RectangularTrimmedSurface),
95            "IFCBSPLINESURFACE" => Some(Self::BSplineSurface),
96            "IFCBSPLINESURFACEWITHKNOTS" => Some(Self::BSplineSurfaceWithKnots),
97            "IFCRATIONALBSPLINESURFACEWITHKNOTS" => Some(Self::RationalBSplineSurfaceWithKnots),
98            _ => None,
99        }
100    }
101
102    /// Is this an `IfcElementarySurface`?
103    ///
104    /// The distinguishing property is not "analytic" but *unbounded*: these
105    /// are the surfaces that need a container to supply their extent.
106    pub fn is_elementary(self) -> bool {
107        matches!(
108            self,
109            Self::Plane | Self::CylindricalSurface | Self::SphericalSurface | Self::ToroidalSurface
110        )
111    }
112
113    /// Is this an `IfcBoundedSurface`?
114    ///
115    /// The only branch that carries its own extent, so the only branch that
116    /// can be meshed without asking its container how far to go.
117    pub fn is_bounded(self) -> bool {
118        matches!(
119            self,
120            Self::CurveBoundedPlane
121                | Self::CurveBoundedSurface
122                | Self::RectangularTrimmedSurface
123                | Self::BSplineSurface
124                | Self::BSplineSurfaceWithKnots
125                | Self::RationalBSplineSurfaceWithKnots
126        )
127    }
128
129    /// Is this an `IfcSweptSurface`?
130    pub fn is_swept(self) -> bool {
131        matches!(
132            self,
133            Self::SurfaceOfLinearExtrusion | Self::SurfaceOfRevolution
134        )
135    }
136
137    /// Does this surface delegate its shape to another surface?
138    ///
139    /// True for the trimming and curve-bounding wrappers, whose `BasisSurface`
140    /// must be resolved before anything can be evaluated. A consumer that does
141    /// not follow the chain silently drops the geometry.
142    pub fn has_basis_surface(self) -> bool {
143        matches!(
144            self,
145            Self::CurveBoundedPlane | Self::CurveBoundedSurface | Self::RectangularTrimmedSurface
146        )
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn every_surface_entity_in_the_schema_is_classified() {
156        let names = [
157            "IFCPLANE",
158            "IFCCYLINDRICALSURFACE",
159            "IFCSPHERICALSURFACE",
160            "IFCTOROIDALSURFACE",
161            "IFCSURFACEOFLINEAREXTRUSION",
162            "IFCSURFACEOFREVOLUTION",
163            "IFCCURVEBOUNDEDPLANE",
164            "IFCCURVEBOUNDEDSURFACE",
165            "IFCRECTANGULARTRIMMEDSURFACE",
166            "IFCBSPLINESURFACE",
167            "IFCBSPLINESURFACEWITHKNOTS",
168            "IFCRATIONALBSPLINESURFACEWITHKNOTS",
169        ];
170        for name in names {
171            assert!(
172                SurfaceKind::classify(name).is_some(),
173                "{name} is unclassified"
174            );
175        }
176    }
177
178    /// A curve is not a surface: misclassifying would send it to a surface
179    /// path that reads slot 0 as a placement.
180    #[test]
181    fn non_surface_entities_are_not_classified_as_surfaces() {
182        for name in ["IFCPOLYLINE", "IFCCIRCLE", "IFCWALL", "IFCBSPLINECURVE"] {
183            assert_eq!(SurfaceKind::classify(name), None, "{name}");
184        }
185    }
186
187    #[test]
188    fn type_names_are_matched_case_insensitively() {
189        assert_eq!(SurfaceKind::classify("IfcPlane"), Some(SurfaceKind::Plane));
190    }
191
192    /// Only the bounded branch carries its own extent; everything elementary
193    /// needs a container to say how far it goes.
194    #[test]
195    fn elementary_surfaces_are_never_self_bounding() {
196        for kind in [
197            SurfaceKind::Plane,
198            SurfaceKind::CylindricalSurface,
199            SurfaceKind::SphericalSurface,
200            SurfaceKind::ToroidalSurface,
201        ] {
202            assert!(kind.is_elementary(), "{kind:?}");
203            assert!(!kind.is_bounded(), "{kind:?}");
204            assert!(!kind.is_swept(), "{kind:?}");
205        }
206    }
207
208    #[test]
209    fn bounded_surfaces_include_the_bspline_patches() {
210        for kind in [
211            SurfaceKind::CurveBoundedPlane,
212            SurfaceKind::CurveBoundedSurface,
213            SurfaceKind::RectangularTrimmedSurface,
214            SurfaceKind::BSplineSurfaceWithKnots,
215            SurfaceKind::RationalBSplineSurfaceWithKnots,
216        ] {
217            assert!(kind.is_bounded(), "{kind:?}");
218            assert!(!kind.is_elementary(), "{kind:?}");
219        }
220    }
221
222    #[test]
223    fn swept_surfaces_are_the_two_profile_driven_kinds() {
224        assert!(SurfaceKind::SurfaceOfLinearExtrusion.is_swept());
225        assert!(SurfaceKind::SurfaceOfRevolution.is_swept());
226        assert!(!SurfaceKind::Plane.is_swept());
227    }
228
229    /// A basis surface must be followed; a NURBS patch has no basis to follow.
230    #[test]
231    fn only_the_wrapping_surfaces_report_a_basis_surface() {
232        assert!(SurfaceKind::RectangularTrimmedSurface.has_basis_surface());
233        assert!(SurfaceKind::CurveBoundedPlane.has_basis_surface());
234        assert!(SurfaceKind::CurveBoundedSurface.has_basis_surface());
235        assert!(!SurfaceKind::BSplineSurfaceWithKnots.has_basis_surface());
236        assert!(!SurfaceKind::Plane.has_basis_surface());
237    }
238}