Skip to main content

egml_core/model/geometry/primitives/
solid.rs

1use crate::error::Error;
2use crate::model::common::{
3    ApplyTransform, ComputeEnvelope, IterGeometries, Triangulate, Triangulation,
4};
5use crate::model::geometry::primitives::{
6    AbstractSolid, AsAbstractSolid, AsAbstractSolidMut, Shell, TriangulatedSurface,
7};
8use crate::model::geometry::refs::AbstractGeometryKindRef;
9use crate::model::geometry::{DirectPosition, Envelope};
10use crate::{impl_abstract_solid_mut_traits, impl_abstract_solid_traits, impl_has_geometry_type};
11use nalgebra::{Isometry3, Rotation3, Scale3, Transform3, Vector3};
12use rayon::prelude::*;
13
14/// A 3-D geometry bounded by one or more surfaces.
15///
16/// Corresponds to `gml:Solid` in [OGC 07-036 ยง10.6.4](https://docs.ogc.org/is/07-036/07-036.pdf).  The bounding surfaces are
17/// stored as [`Shell`] members and may be of any [`AbstractSurfaceKind`](crate::model::geometry::primitives::AbstractSurfaceKind).
18#[derive(Debug, Clone, PartialEq)]
19pub struct Solid {
20    pub abstract_solid: AbstractSolid,
21    exterior: Option<Shell>,
22    interior: Vec<Shell>,
23}
24
25impl Solid {
26    /// Creates a new `Solid` from its bounding surfaces.
27    ///
28    /// # Errors
29    ///
30    /// Returns [`Error::TooFewElements`] if `members` is empty.
31    pub fn new(exterior: Option<Shell>) -> Result<Self, Error> {
32        Ok(Self {
33            abstract_solid: AbstractSolid::default(),
34            exterior,
35            interior: Vec::new(),
36        })
37    }
38
39    pub fn from_abstract_solid(abstract_solid: AbstractSolid, exterior: Option<Shell>) -> Self {
40        Self {
41            abstract_solid,
42            exterior,
43            interior: Vec::new(),
44        }
45    }
46
47    pub fn exterior(&self) -> Option<&Shell> {
48        self.exterior.as_ref()
49    }
50
51    pub fn interior(&self) -> &[Shell] {
52        &self.interior
53    }
54
55    pub fn set_interior(&mut self, interior: Vec<Shell>) {
56        self.interior = interior;
57    }
58
59    pub fn push_interior(&mut self, interior: Shell) {
60        self.interior.push(interior);
61    }
62
63    pub fn extend_interiors(&mut self, interiors: impl IntoIterator<Item = Shell>) {
64        self.interior.extend(interiors);
65    }
66}
67
68impl Solid {
69    pub fn points(&self) -> Vec<&DirectPosition> {
70        self.exterior
71            .as_ref()
72            .map(|exterior| exterior.points())
73            .unwrap_or_default()
74    }
75
76    /// Returns the volume of this solid.
77    ///
78    /// # Errors
79    ///
80    /// Returns [`Error::MissingExteriorShell`] if the solid has no exterior shell property.
81    /// Propagates any error from triangulating the bounding surfaces.
82    pub fn volume_3d(&self) -> Result<f64, Error> {
83        self.exterior
84            .as_ref()
85            .ok_or(Error::MissingExteriorShell)?
86            .volume_3d()
87    }
88}
89
90impl ApplyTransform for Solid {
91    fn apply_transform(&mut self, transform: Transform3<f64>) {
92        if let Some(exterior) = self.exterior.as_mut() {
93            exterior.apply_transform(transform);
94        }
95        self.interior
96            .par_iter_mut()
97            .for_each(|p| p.apply_transform(transform));
98    }
99
100    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
101        if let Some(exterior) = self.exterior.as_mut() {
102            exterior.apply_isometry(isometry);
103        }
104        self.interior
105            .par_iter_mut()
106            .for_each(|p| p.apply_isometry(isometry));
107    }
108
109    fn apply_translation(&mut self, vector: Vector3<f64>) {
110        if let Some(exterior) = self.exterior.as_mut() {
111            exterior.apply_translation(vector);
112        }
113        self.interior
114            .par_iter_mut()
115            .for_each(|p| p.apply_translation(vector));
116    }
117
118    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
119        if let Some(exterior) = self.exterior.as_mut() {
120            exterior.apply_rotation(rotation);
121        }
122        self.interior
123            .par_iter_mut()
124            .for_each(|p| p.apply_rotation(rotation));
125    }
126
127    fn apply_scale(&mut self, scale: Scale3<f64>) {
128        if let Some(exterior) = self.exterior.as_mut() {
129            exterior.apply_scale(scale);
130        }
131        self.interior
132            .par_iter_mut()
133            .for_each(|p| p.apply_scale(scale));
134    }
135}
136
137impl ComputeEnvelope for Solid {
138    /// Returns the union of the bounding boxes of all surface members.
139    fn compute_envelope(&self) -> Option<Envelope> {
140        self.exterior.as_ref()?.compute_envelope()
141    }
142}
143
144impl AsAbstractSolid for Solid {
145    fn abstract_solid(&self) -> &AbstractSolid {
146        &self.abstract_solid
147    }
148}
149
150impl AsAbstractSolidMut for Solid {
151    fn abstract_solid_mut(&mut self) -> &mut AbstractSolid {
152        &mut self.abstract_solid
153    }
154}
155
156impl_abstract_solid_traits!(Solid);
157impl_abstract_solid_mut_traits!(Solid);
158impl_has_geometry_type!(Solid, Solid);
159
160impl IterGeometries for Solid {
161    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
162        Box::new(
163            std::iter::once(self.into())
164                .chain(
165                    self.exterior
166                        .as_ref()
167                        .into_iter()
168                        .flat_map(|x| x.iter_geometries()),
169                )
170                .chain(self.interior.iter().flat_map(|x| x.iter_geometries())),
171        )
172    }
173}
174
175impl Triangulate for Solid {
176    /// Triangulates the exterior shell and all interior shells into a single
177    /// [`TriangulatedSurface`] covering the full boundary of this solid.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`Error::MissingExteriorShell`] if the solid has no exterior shell.
182    /// Propagates any error from [`Shell::triangulate`].
183    fn triangulate(&self) -> Result<Triangulation, Error> {
184        let exterior_shell = self.exterior.as_ref().ok_or(Error::MissingExteriorShell)?;
185
186        let (exterior_surface, mut skipped) = exterior_shell.triangulate()?.into_parts();
187        let mut surfaces = vec![exterior_surface];
188
189        for shell in &self.interior {
190            let (surface, nested_skipped) = shell.triangulate()?.into_parts();
191            surfaces.push(surface);
192            skipped.extend(nested_skipped);
193        }
194
195        let combined = TriangulatedSurface::from_triangulated_surfaces(surfaces)?;
196        Ok(Triangulation::new(combined, skipped))
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::model::geometry::Envelope;
204
205    #[test]
206    fn volume_3d_unit_cube() {
207        let solid = Envelope::new(
208            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
209            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
210        )
211        .unwrap()
212        .to_solid()
213        .unwrap();
214        assert!((solid.volume_3d().unwrap() - 1.0).abs() < 1e-10);
215    }
216
217    #[test]
218    fn volume_3d_2x3x4_box() {
219        let solid = Envelope::new(
220            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
221            DirectPosition::new(2.0, 3.0, 4.0).unwrap(),
222        )
223        .unwrap()
224        .to_solid()
225        .unwrap();
226        assert!((solid.volume_3d().unwrap() - 24.0).abs() < 1e-10);
227    }
228
229    #[test]
230    fn apply_translation_transforms_interior_shells_too() {
231        let mut solid = Envelope::new(
232            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
233            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
234        )
235        .unwrap()
236        .to_solid()
237        .unwrap();
238
239        let interior_shell = solid.exterior().cloned().unwrap();
240        solid.push_interior(interior_shell);
241
242        let before = solid.interior()[0].points()[0].x();
243
244        solid.apply_translation(Vector3::new(5.0, 0.0, 0.0));
245
246        let after = solid.interior()[0].points()[0].x();
247
248        assert!((after - before - 5.0).abs() < 1e-10);
249    }
250
251    #[test]
252    fn iter_geometries_walks_shell_polygons_and_rings() {
253        let solid = Envelope::new(
254            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
255            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
256        )
257        .unwrap()
258        .to_solid()
259        .unwrap();
260
261        // self (1) + exterior shell (1) + 6 faces (6) + 6 rings, one per face (6) = 14
262        assert_eq!(solid.iter_geometries().count(), 14);
263    }
264
265    #[test]
266    fn volume_3d_missing_exterior_shell() {
267        let solid = Solid::new(None).unwrap();
268        assert_eq!(solid.volume_3d(), Err(Error::MissingExteriorShell));
269    }
270}