Skip to main content

egml_core/model/geometry/primitives/
solid.rs

1use crate::error::Error;
2use crate::model::base::HasAssociationAttributes;
3use crate::model::common::{
4    ApplyTransform, ComputeEnvelope, IterGeometries, Triangulate, Triangulation,
5};
6use crate::model::geometry::primitives::shell_property::ShellProperty;
7use crate::model::geometry::primitives::{
8    AbstractSolid, AsAbstractSolid, AsAbstractSolidMut, TriangulatedSurface,
9};
10use crate::model::geometry::refs::AbstractGeometryKindRef;
11use crate::model::geometry::{DirectPosition, Envelope};
12use crate::{impl_abstract_solid_mut_traits, impl_abstract_solid_traits, impl_has_geometry_type};
13use nalgebra::{Isometry3, Rotation3, Scale3, Transform3, Vector3};
14use rayon::prelude::*;
15
16/// A 3-D geometry bounded by one or more surfaces.
17///
18/// 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
19/// stored as [`ShellProperty`] members and may be of any [`AbstractSurfaceKind`](crate::model::geometry::primitives::AbstractSurfaceKind).
20#[derive(Debug, Clone, PartialEq)]
21pub struct Solid {
22    pub abstract_solid: AbstractSolid,
23    exterior: Option<ShellProperty>,
24    interior: Vec<ShellProperty>,
25}
26
27impl Solid {
28    /// Creates a new `Solid` from its bounding surfaces.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`Error::TooFewElements`] if `members` is empty.
33    pub fn new(exterior: Option<ShellProperty>) -> Result<Self, Error> {
34        Ok(Self {
35            abstract_solid: AbstractSolid::default(),
36            exterior,
37            interior: Vec::new(),
38        })
39    }
40
41    pub fn from_abstract_solid(
42        abstract_solid: AbstractSolid,
43        exterior: Option<ShellProperty>,
44    ) -> Self {
45        Self {
46            abstract_solid,
47            exterior,
48            interior: Vec::new(),
49        }
50    }
51
52    pub fn exterior(&self) -> Option<&ShellProperty> {
53        self.exterior.as_ref()
54    }
55
56    pub fn interior(&self) -> &[ShellProperty] {
57        &self.interior
58    }
59
60    pub fn set_interior(&mut self, interior: Vec<ShellProperty>) {
61        self.interior = interior;
62    }
63
64    pub fn push_interior(&mut self, interior: ShellProperty) {
65        self.interior.push(interior);
66    }
67
68    pub fn extend_interiors(&mut self, interiors: impl IntoIterator<Item = ShellProperty>) {
69        self.interior.extend(interiors);
70    }
71}
72
73impl Solid {
74    pub fn points(&self) -> Vec<&DirectPosition> {
75        if let Some(exterior) = &self.exterior
76            && let Some(object) = exterior.object()
77        {
78            object.points()
79        } else {
80            Vec::new()
81        }
82    }
83
84    /// Returns the volume of this solid.
85    ///
86    /// # Errors
87    ///
88    /// Returns [`Error::MissingExteriorShell`] if the solid has no exterior shell property.
89    /// Returns [`Error::UnresolvedShellReference`] if the shell property carries only an
90    /// xlink:href that has not been resolved into an inline object.
91    /// Propagates any error from triangulating the bounding surfaces.
92    pub fn volume_3d(&self) -> Result<f64, Error> {
93        let shell_property = self.exterior.as_ref().ok_or(Error::MissingExteriorShell)?;
94        let shell = shell_property
95            .object()
96            .ok_or_else(|| Error::UnresolvedShellReference {
97                href: shell_property.href().map(|h| h.to_string()),
98            })?;
99        shell.volume_3d()
100    }
101}
102
103impl ApplyTransform for Solid {
104    fn apply_transform(&mut self, transform: Transform3<f64>) {
105        if let Some(exterior) = self.exterior.as_mut()
106            && let Some(object) = exterior.object_mut()
107        {
108            object.apply_transform(transform)
109        }
110
111        self.interior.par_iter_mut().for_each(|p| {
112            if let Some(object) = p.object_mut() {
113                object.apply_transform(transform);
114            }
115        });
116    }
117
118    fn apply_isometry(&mut self, isometry: Isometry3<f64>) {
119        if let Some(exterior) = self.exterior.as_mut()
120            && let Some(object) = exterior.object_mut()
121        {
122            object.apply_isometry(isometry)
123        }
124
125        self.interior.par_iter_mut().for_each(|p| {
126            if let Some(object) = p.object_mut() {
127                object.apply_isometry(isometry);
128            }
129        });
130    }
131
132    fn apply_translation(&mut self, vector: Vector3<f64>) {
133        if let Some(exterior) = self.exterior.as_mut()
134            && let Some(object) = exterior.object_mut()
135        {
136            object.apply_translation(vector)
137        }
138
139        self.interior.par_iter_mut().for_each(|p| {
140            if let Some(object) = p.object_mut() {
141                object.apply_translation(vector);
142            }
143        });
144    }
145
146    fn apply_rotation(&mut self, rotation: Rotation3<f64>) {
147        if let Some(exterior) = self.exterior.as_mut()
148            && let Some(object) = exterior.object_mut()
149        {
150            object.apply_rotation(rotation)
151        }
152
153        self.interior.par_iter_mut().for_each(|p| {
154            if let Some(object) = p.object_mut() {
155                object.apply_rotation(rotation);
156            }
157        });
158    }
159
160    fn apply_scale(&mut self, scale: Scale3<f64>) {
161        if let Some(exterior) = self.exterior.as_mut()
162            && let Some(object) = exterior.object_mut()
163        {
164            object.apply_scale(scale)
165        }
166
167        self.interior.par_iter_mut().for_each(|p| {
168            if let Some(object) = p.object_mut() {
169                object.apply_scale(scale);
170            }
171        });
172    }
173}
174
175impl ComputeEnvelope for Solid {
176    /// Returns the union of the bounding boxes of all surface members.
177    fn compute_envelope(&self) -> Option<Envelope> {
178        if let Some(exterior) = &self.exterior
179            && let Some(object) = exterior.object()
180        {
181            object.compute_envelope()
182        } else {
183            None
184        }
185    }
186}
187
188impl AsAbstractSolid for Solid {
189    fn abstract_solid(&self) -> &AbstractSolid {
190        &self.abstract_solid
191    }
192}
193
194impl AsAbstractSolidMut for Solid {
195    fn abstract_solid_mut(&mut self) -> &mut AbstractSolid {
196        &mut self.abstract_solid
197    }
198}
199
200impl_abstract_solid_traits!(Solid);
201impl_abstract_solid_mut_traits!(Solid);
202impl_has_geometry_type!(Solid, Solid);
203
204impl IterGeometries for Solid {
205    fn iter_geometries(&self) -> Box<dyn Iterator<Item = AbstractGeometryKindRef<'_>> + '_> {
206        Box::new(
207            std::iter::once(self.into())
208                .chain(
209                    self.exterior
210                        .as_ref()
211                        .and_then(|x| x.object())
212                        .into_iter()
213                        .flat_map(|x| x.iter_geometries()),
214                )
215                .chain(
216                    self.interior
217                        .iter()
218                        .filter_map(|x| x.object())
219                        .flat_map(|x| x.iter_geometries()),
220                ),
221        )
222    }
223}
224
225impl Triangulate for Solid {
226    /// Triangulates the exterior shell and all interior shells into a single
227    /// [`TriangulatedSurface`] covering the full boundary of this solid.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`Error::MissingExteriorShell`] if the solid has no exterior shell.
232    /// Returns [`Error::UnresolvedShellReference`] if any shell property carries only
233    /// an xlink:href that has not been resolved into an inline object.
234    /// Propagates any error from [`Shell::triangulate`].
235    fn triangulate(&self) -> Result<Triangulation, Error> {
236        let exterior_shell_property = self.exterior.as_ref().ok_or(Error::MissingExteriorShell)?;
237        let exterior_shell =
238            exterior_shell_property
239                .object()
240                .ok_or_else(|| Error::UnresolvedShellReference {
241                    href: exterior_shell_property.href().map(|h| h.to_string()),
242                })?;
243
244        let (exterior_surface, mut skipped) = exterior_shell.triangulate()?.into_parts();
245        let mut surfaces = vec![exterior_surface];
246
247        for shell_property in &self.interior {
248            let shell = shell_property
249                .object()
250                .ok_or_else(|| Error::UnresolvedShellReference {
251                    href: shell_property.href().map(|h| h.to_string()),
252                })?;
253            let (surface, nested_skipped) = shell.triangulate()?.into_parts();
254            surfaces.push(surface);
255            skipped.extend(nested_skipped);
256        }
257
258        let combined = TriangulatedSurface::from_triangulated_surfaces(surfaces)?;
259        Ok(Triangulation::new(combined, skipped))
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266    use crate::model::geometry::Envelope;
267    use crate::model::geometry::primitives::ShellProperty;
268
269    #[test]
270    fn volume_3d_unit_cube() {
271        let solid = Envelope::new(
272            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
273            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
274        )
275        .unwrap()
276        .to_solid()
277        .unwrap();
278        assert!((solid.volume_3d().unwrap() - 1.0).abs() < 1e-10);
279    }
280
281    #[test]
282    fn volume_3d_2x3x4_box() {
283        let solid = Envelope::new(
284            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
285            DirectPosition::new(2.0, 3.0, 4.0).unwrap(),
286        )
287        .unwrap()
288        .to_solid()
289        .unwrap();
290        assert!((solid.volume_3d().unwrap() - 24.0).abs() < 1e-10);
291    }
292
293    #[test]
294    fn apply_translation_transforms_interior_shells_too() {
295        let mut solid = Envelope::new(
296            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
297            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
298        )
299        .unwrap()
300        .to_solid()
301        .unwrap();
302
303        let interior_shell = solid.exterior().unwrap().object().cloned().unwrap();
304        solid.push_interior(ShellProperty::from_object(interior_shell));
305
306        let before = solid.interior()[0].object().unwrap().points()[0].x();
307
308        solid.apply_translation(Vector3::new(5.0, 0.0, 0.0));
309
310        let after = solid.interior()[0].object().unwrap().points()[0].x();
311
312        assert!((after - before - 5.0).abs() < 1e-10);
313    }
314
315    #[test]
316    fn iter_geometries_walks_shell_polygons_and_rings() {
317        let solid = Envelope::new(
318            DirectPosition::new(0.0, 0.0, 0.0).unwrap(),
319            DirectPosition::new(1.0, 1.0, 1.0).unwrap(),
320        )
321        .unwrap()
322        .to_solid()
323        .unwrap();
324
325        // self (1) + exterior shell (1) + 6 faces (6) + 6 rings, one per face (6) = 14
326        assert_eq!(solid.iter_geometries().count(), 14);
327    }
328
329    #[test]
330    fn volume_3d_missing_exterior_shell() {
331        let solid = Solid::new(None).unwrap();
332        assert_eq!(solid.volume_3d(), Err(Error::MissingExteriorShell));
333    }
334
335    #[test]
336    fn volume_3d_unresolved_shell_reference() {
337        let solid =
338            Solid::new(Some(ShellProperty::from_href("urn:example:shell-1".into()))).unwrap();
339        assert_eq!(
340            solid.volume_3d(),
341            Err(Error::UnresolvedShellReference {
342                href: Some("urn:example:shell-1".to_string())
343            })
344        );
345    }
346}