Skip to main content

egml_core/model/geometry/
envelope.rs

1use crate::error::Error;
2use crate::model::geometry::DirectPosition;
3use crate::model::geometry::primitives::{
4    LinearRing, Polygon, RingProperty, Solid, SurfaceProperty, TriangulatedSurface,
5};
6use crate::model::geometry::primitives::{RingKind, Shell};
7use crate::model::geometry::primitives::{ShellProperty, SurfaceKind};
8use nalgebra::{Isometry3, Point3, Vector3};
9use std::fmt;
10
11/// Axis-aligned bounding box in 3-D space.
12///
13/// An `Envelope` is defined by a lower corner and an upper corner such that
14/// each coordinate component of the lower corner is ≤ the corresponding
15/// component of the upper corner.
16///
17/// Corresponds to `gml:EnvelopeType` in ISO 19136.
18#[derive(Debug, Clone, PartialEq, Default)]
19pub struct Envelope {
20    lower_corner: DirectPosition,
21    upper_corner: DirectPosition,
22
23    srs_name: Option<String>,
24    srs_dimension: Option<u8>,
25}
26
27impl Envelope {
28    /// Creates an envelope from explicit lower and upper corners.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`Error::InvalidEnvelopeBounds`] if any coordinate
33    /// component of `lower_corner` is strictly greater than the corresponding
34    /// component of `upper_corner`.
35    ///
36    /// # Examples
37    ///
38    /// ```rust
39    /// use egml_core::model::geometry::{DirectPosition, Envelope};
40    ///
41    /// let lo = DirectPosition::new(0.0, 0.0, 0.0).unwrap();
42    /// let hi = DirectPosition::new(1.0, 2.0, 3.0).unwrap();
43    /// let env = Envelope::new(lo, hi).unwrap();
44    /// assert_eq!(env.size_x(), 1.0);
45    /// ```
46    pub fn new(lower_corner: DirectPosition, upper_corner: DirectPosition) -> Result<Self, Error> {
47        if lower_corner.x() > upper_corner.x() {
48            return Err(Error::InvalidEnvelopeBounds {
49                axis: "x",
50                lower: lower_corner.x(),
51                upper: upper_corner.x(),
52            });
53        }
54        if lower_corner.y() > upper_corner.y() {
55            return Err(Error::InvalidEnvelopeBounds {
56                axis: "y",
57                lower: lower_corner.y(),
58                upper: upper_corner.y(),
59            });
60        }
61        if lower_corner.z() > upper_corner.z() {
62            return Err(Error::InvalidEnvelopeBounds {
63                axis: "z",
64                lower: lower_corner.z(),
65                upper: upper_corner.z(),
66            });
67        }
68
69        Ok(Self {
70            lower_corner,
71            upper_corner,
72            srs_name: None,
73            srs_dimension: None,
74        })
75    }
76
77    /// Creates an `Envelope` without validating that `lower_corner <= upper_corner`.
78    ///
79    /// # Safety (logical)
80    /// The caller must ensure that each component of `lower_corner` is less than
81    /// or equal to the corresponding component of `upper_corner`. Violating this
82    /// will not cause undefined behavior, but will break the type's invariants
83    /// and produce incorrect results from methods like `contains`, `size`, etc.
84    pub(crate) fn new_unchecked(
85        lower_corner: DirectPosition,
86        upper_corner: DirectPosition,
87    ) -> Self {
88        debug_assert!(
89            {
90                let lc: Point3<f64> = lower_corner.into();
91                let uc: Point3<f64> = upper_corner.into();
92                lc <= uc
93            },
94            "lower_corner must be <= upper_corner"
95        );
96
97        Self {
98            lower_corner,
99            upper_corner,
100            srs_name: None,
101            srs_dimension: None,
102        }
103    }
104
105    /// Returns the lower (minimum) corner.
106    pub fn lower_corner(&self) -> &DirectPosition {
107        &self.lower_corner
108    }
109
110    /// Returns the upper (maximum) corner.
111    pub fn upper_corner(&self) -> &DirectPosition {
112        &self.upper_corner
113    }
114
115    /// Returns the SRS name identifying the CRS of this envelope's coordinates,
116    /// or `None` if unspecified.
117    pub fn srs_name(&self) -> Option<&str> {
118        self.srs_name.as_deref()
119    }
120
121    /// Returns the coordinate dimension of this envelope's positions,
122    /// or `None` if unspecified.
123    pub fn srs_dimension(&self) -> Option<u8> {
124        self.srs_dimension
125    }
126
127    /// Sets the SRS (Spatial Reference System) name, identifying the CRS in which
128    /// this envelope's coordinates are expressed (e.g. `"urn:ogc:def:crs:EPSG::25832"`).
129    /// Pass `None` to leave the CRS unspecified.
130    pub fn set_srs_name(&mut self, srs_name: Option<String>) {
131        self.srs_name = srs_name;
132    }
133
134    /// Sets the coordinate dimension of this envelope's positions (typically `2` or `3`).
135    /// Pass `None` to leave the dimension implicit.
136    pub fn set_srs_dimension(&mut self, srs_dimension: Option<u8>) {
137        self.srs_dimension = srs_dimension;
138    }
139
140    /// Returns the diagonal vector from the lower corner to the upper corner.
141    pub fn size(&self) -> Vector3<f64> {
142        let lower_corner_point: Point3<f64> = self.lower_corner.into();
143        let upper_corner_point: Point3<f64> = self.upper_corner.into();
144        upper_corner_point - lower_corner_point
145    }
146
147    /// Returns the extent along the X axis (`upper.x - lower.x`).
148    pub fn size_x(&self) -> f64 {
149        self.upper_corner.x() - self.lower_corner.x()
150    }
151
152    /// Returns the extent along the Y axis (`upper.y - lower.y`).
153    pub fn size_y(&self) -> f64 {
154        self.upper_corner.y() - self.lower_corner.y()
155    }
156
157    /// Returns the extent along the Z axis (`upper.z - lower.z`).
158    pub fn size_z(&self) -> f64 {
159        self.upper_corner.z() - self.lower_corner.z()
160    }
161
162    /// Returns the volume of the box (`size_x * size_y * size_z`).
163    ///
164    /// Returns `0.0` for degenerate envelopes where one or more extents are zero.
165    pub fn volume(&self) -> f64 {
166        self.size_x() * self.size_y() * self.size_z()
167    }
168
169    /// Returns `true` if the lower and upper corners are equal, i.e. the envelope collapses to a point.
170    pub fn is_point(&self) -> bool {
171        self.lower_corner == self.upper_corner
172    }
173
174    /// Returns `true` if exactly one axis has non-zero extent (a line segment).
175    #[allow(clippy::nonminimal_bool)]
176    pub fn is_linear(&self) -> bool {
177        let nx = self.size_x() > 0.0;
178        let ny = self.size_y() > 0.0;
179        let nz = self.size_z() > 0.0;
180        (nx && !ny && !nz) || (!nx && ny && !nz) || (!nx && !ny && nz)
181    }
182
183    /// Returns `true` if exactly two axes have non-zero extent (a flat rectangle).
184    #[allow(clippy::nonminimal_bool)]
185    pub fn is_surface(&self) -> bool {
186        let nx = self.size_x() > 0.0;
187        let ny = self.size_y() > 0.0;
188        let nz = self.size_z() > 0.0;
189        (nx && ny && !nz) || (nx && !ny && nz) || (!nx && ny && nz)
190    }
191
192    /// Returns `true` if all three axes have non-zero extent.
193    pub fn is_volume(&self) -> bool {
194        self.size_x() > 0.0 && self.size_y() > 0.0 && self.size_z() > 0.0
195    }
196
197    fn non_zero_extents(&self) -> u8 {
198        [self.size_x(), self.size_y(), self.size_z()]
199            .iter()
200            .filter(|&&s| s > 0.0)
201            .count() as u8
202    }
203
204    /// Returns the center point of the envelope.
205    ///
206    /// Computed as `lower + size / 2` to avoid overflow with large coordinates.
207    pub fn center(&self) -> DirectPosition {
208        DirectPosition::new(
209            self.lower_corner.x() + self.size_x() / 2.0,
210            self.lower_corner.y() + self.size_y() / 2.0,
211            self.lower_corner.z() + self.size_z() / 2.0,
212        )
213        .expect("envelope corners are finite")
214    }
215
216    /// Returns `true` if `point` lies inside or on the boundary of this envelope.
217    pub fn contains(&self, point: &DirectPosition) -> bool {
218        let lower_corner: Point3<f64> = self.lower_corner.into();
219        let upper_corner: Point3<f64> = self.upper_corner.into();
220        let point: Point3<f64> = (*point).into();
221
222        lower_corner <= point && point <= upper_corner
223    }
224
225    /// Returns `true` if `envelope` is fully contained within (or touches the boundary of) `self`.
226    pub fn contains_envelope(&self, envelope: &Envelope) -> bool {
227        self.contains(&envelope.lower_corner) && self.contains(&envelope.upper_corner)
228    }
229
230    /// Returns `true` if any corner of `envelope` lies inside or on the boundary of `self`.
231    pub fn contains_envelope_partially(&self, envelope: &Envelope) -> bool {
232        self.contains(&envelope.lower_corner) || self.contains(&envelope.upper_corner)
233    }
234
235    /// Returns a new envelope expanded by `distance` in every direction.
236    ///
237    /// Each lower-corner coordinate is decreased by `distance` and each
238    /// upper-corner coordinate is increased by `distance`.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`Error::NonFiniteCoordinate`] if `distance` is NaN or infinite,
243    /// or if the resulting coordinates would overflow `f64::MAX`.
244    pub fn enlarge(&self, distance: f64) -> Result<Envelope, Error> {
245        let lower_corner = DirectPosition::new(
246            self.lower_corner.x() - distance,
247            self.lower_corner.y() - distance,
248            self.lower_corner.z() - distance,
249        )?;
250        let upper_corner = DirectPosition::new(
251            self.upper_corner.x() + distance,
252            self.upper_corner.y() + distance,
253            self.upper_corner.z() + distance,
254        )?;
255
256        Envelope::new(lower_corner, upper_corner)
257    }
258
259    /// Applies a rigid-body isometry (rotation + translation) to this envelope in place.
260    ///
261    /// Both corners are transformed and the result is re-fitted as an axis-aligned bounding
262    /// box by taking per-axis minima/maxima. This keeps the AABB invariant valid after
263    /// rotation, at the cost of a potentially larger box for non-axis-aligned rotations.
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// use egml_core::model::geometry::{DirectPosition, Envelope};
269    /// use nalgebra::{Isometry3, Vector3};
270    ///
271    /// let lo = DirectPosition::new(0.0, 0.0, 0.0).unwrap();
272    /// let hi = DirectPosition::new(1.0, 2.0, 3.0).unwrap();
273    /// let mut env = Envelope::new(lo, hi).unwrap();
274    ///
275    /// let translation = Isometry3::translation(10.0, 0.0, 0.0);
276    /// env.apply_transform(&translation);
277    ///
278    /// assert_eq!(env.lower_corner().x(), 10.0);
279    /// assert_eq!(env.upper_corner().x(), 11.0);
280    /// ```
281    pub fn apply_transform(&mut self, m: &Isometry3<f64>) {
282        let transformed_lower_corner: Point3<f64> = m * Point3::from(self.lower_corner);
283        let transformed_upper_corner: Point3<f64> = m * Point3::from(self.upper_corner);
284
285        self.lower_corner = DirectPosition::new(
286            transformed_lower_corner.x.min(transformed_upper_corner.x),
287            transformed_lower_corner.y.min(transformed_upper_corner.y),
288            transformed_lower_corner.z.min(transformed_upper_corner.z),
289        )
290        .expect("envelope corners are finite");
291        self.upper_corner = DirectPosition::new(
292            transformed_lower_corner.x.max(transformed_upper_corner.x),
293            transformed_lower_corner.y.max(transformed_upper_corner.y),
294            transformed_lower_corner.z.max(transformed_upper_corner.z),
295        )
296        .expect("envelope corners are finite");
297    }
298}
299
300impl Envelope {
301    /// Computes the union of a slice of envelopes.
302    ///
303    /// Returns `None` if `envelopes` is empty; otherwise returns the smallest
304    /// envelope that contains all envelopes in the slice.
305    pub fn from_envelopes(envelopes: &[Self]) -> Option<Self> {
306        let first = envelopes.first()?;
307
308        let (lower, upper) = envelopes.iter().skip(1).fold(
309            (first.lower_corner, first.upper_corner),
310            |(lo, hi), e| {
311                let new_lo = DirectPosition::new(
312                    lo.x().min(e.lower_corner.x()),
313                    lo.y().min(e.lower_corner.y()),
314                    lo.z().min(e.lower_corner.z()),
315                )
316                .unwrap();
317                let new_hi = DirectPosition::new(
318                    hi.x().max(e.upper_corner.x()),
319                    hi.y().max(e.upper_corner.y()),
320                    hi.z().max(e.upper_corner.z()),
321                )
322                .unwrap();
323                (new_lo, new_hi)
324            },
325        );
326
327        Some(Envelope::new_unchecked(lower, upper))
328    }
329
330    /// Computes the smallest envelope that contains all `points`.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`Error::TooFewElements`] if `points` is empty.
335    pub fn from_points(points: &[DirectPosition]) -> Result<Self, Error> {
336        if points.is_empty() {
337            return Err(Error::TooFewElements {
338                geometry: "Envelope::from_points",
339                minimum: 1,
340                spec: None,
341                id: None,
342                detail: None,
343            });
344        }
345
346        let first = &points[0];
347        let (mut min_x, mut min_y, mut min_z) = (first.x(), first.y(), first.z());
348        let (mut max_x, mut max_y, mut max_z) = (first.x(), first.y(), first.z());
349
350        for point in points.iter().skip(1) {
351            min_x = min_x.min(point.x());
352            min_y = min_y.min(point.y());
353            min_z = min_z.min(point.z());
354            max_x = max_x.max(point.x());
355            max_y = max_y.max(point.y());
356            max_z = max_z.max(point.z());
357        }
358
359        let lower_corner = DirectPosition::new(min_x, min_y, min_z)?;
360        let upper_corner = DirectPosition::new(max_x, max_y, max_z)?;
361
362        Ok(Self::new_unchecked(lower_corner, upper_corner))
363    }
364}
365
366impl Envelope {
367    /// Constructs a [`Solid`] whose boundary is the six faces of the bounding box.
368    ///
369    /// Each face is a [`Polygon`] with an outward-facing [`LinearRing`] exterior.
370    /// Faces are ordered: bottom (−z), top (+z), front (−y), back (+y), left (−x), right (+x).
371    ///
372    /// # Errors
373    ///
374    /// Returns [`Error::NotAVolume`] if the envelope does not have all three extents non-zero.
375    pub fn to_solid(&self) -> Result<Solid, Error> {
376        if !self.is_volume() {
377            return Err(Error::NotAVolume {
378                non_zero_extents: self.non_zero_extents(),
379            });
380        }
381
382        let (lx, ly, lz) = (
383            self.lower_corner.x(),
384            self.lower_corner.y(),
385            self.lower_corner.z(),
386        );
387        let (hx, hy, hz) = (
388            self.upper_corner.x(),
389            self.upper_corner.y(),
390            self.upper_corner.z(),
391        );
392
393        let p000 = DirectPosition::new(lx, ly, lz).expect("envelope corners are finite");
394        let p100 = DirectPosition::new(hx, ly, lz).expect("envelope corners are finite");
395        let p110 = DirectPosition::new(hx, hy, lz).expect("envelope corners are finite");
396        let p010 = DirectPosition::new(lx, hy, lz).expect("envelope corners are finite");
397        let p001 = DirectPosition::new(lx, ly, hz).expect("envelope corners are finite");
398        let p101 = DirectPosition::new(hx, ly, hz).expect("envelope corners are finite");
399        let p111 = DirectPosition::new(hx, hy, hz).expect("envelope corners are finite");
400        let p011 = DirectPosition::new(lx, hy, hz).expect("envelope corners are finite");
401
402        let face_rings: [Vec<DirectPosition>; 6] = [
403            vec![p000, p010, p110, p100], // bottom (−z)
404            vec![p001, p101, p111, p011], // top    (+z)
405            vec![p000, p100, p101, p001], // front  (−y)
406            vec![p010, p011, p111, p110], // back   (+y)
407            vec![p000, p001, p011, p010], // left   (−x)
408            vec![p100, p110, p111, p101], // right  (+x)
409        ];
410
411        let members: Vec<SurfaceProperty> = face_rings
412            .into_iter()
413            .map(|points| {
414                let ring = LinearRing::new(points).ok()?;
415                let polygon =
416                    Polygon::new(Some(RingProperty::new(RingKind::LinearRing(ring))), vec![])
417                        .ok()?;
418                Some(SurfaceProperty::new(SurfaceKind::Polygon(polygon)))
419            })
420            .collect::<Option<_>>()
421            .expect("envelope corners are finite and valid");
422        let shell = Shell::new(members).expect("envelope is valid");
423        let shell_property = ShellProperty::new(shell);
424
425        let solid = Solid::new(Some(shell_property)).expect("envelope is valid");
426        Ok(solid)
427    }
428
429    /// Constructs a [`Polygon`] from the flat rectangle of this envelope.
430    ///
431    /// The four corners are wound counter-clockwise when viewed from the
432    /// positive side of the collapsed axis (i.e. outward-facing normal).
433    ///
434    /// # Errors
435    ///
436    /// Returns [`Error::NotASurface`] if the envelope does not have exactly two non-zero extents.
437    pub fn to_polygon(&self) -> Result<Polygon, Error> {
438        if !self.is_surface() {
439            return Err(Error::NotASurface {
440                non_zero_extents: self.non_zero_extents(),
441            });
442        }
443
444        let (lx, ly, lz) = (
445            self.lower_corner.x(),
446            self.lower_corner.y(),
447            self.lower_corner.z(),
448        );
449        let (hx, hy, hz) = (
450            self.upper_corner.x(),
451            self.upper_corner.y(),
452            self.upper_corner.z(),
453        );
454
455        let points = if self.size_z() == 0.0 {
456            // XY plane — normal along +Z
457            vec![
458                DirectPosition::new(lx, ly, lz).expect("envelope corners are finite"),
459                DirectPosition::new(hx, ly, lz).expect("envelope corners are finite"),
460                DirectPosition::new(hx, hy, lz).expect("envelope corners are finite"),
461                DirectPosition::new(lx, hy, lz).expect("envelope corners are finite"),
462            ]
463        } else if self.size_y() == 0.0 {
464            // XZ plane — normal along +Y
465            vec![
466                DirectPosition::new(lx, ly, lz).expect("envelope corners are finite"),
467                DirectPosition::new(lx, ly, hz).expect("envelope corners are finite"),
468                DirectPosition::new(hx, ly, hz).expect("envelope corners are finite"),
469                DirectPosition::new(hx, ly, lz).expect("envelope corners are finite"),
470            ]
471        } else {
472            // YZ plane — normal along +X
473            vec![
474                DirectPosition::new(lx, ly, lz).expect("envelope corners are finite"),
475                DirectPosition::new(lx, hy, lz).expect("envelope corners are finite"),
476                DirectPosition::new(lx, hy, hz).expect("envelope corners are finite"),
477                DirectPosition::new(lx, ly, hz).expect("envelope corners are finite"),
478            ]
479        };
480
481        let ring = LinearRing::new(points).expect("envelope corners are finite and valid");
482        Polygon::new(Some(RingProperty::new(RingKind::LinearRing(ring))), vec![]).map_err(|_| {
483            Error::NotASurface {
484                non_zero_extents: self.non_zero_extents(),
485            }
486        })
487    }
488
489    /// Triangulates the envelope into a [`TriangulatedSurface`].
490    ///
491    /// - For a surface envelope (`is_surface()`): triangulates the single rectangular face.
492    /// - For a volume envelope (`is_volume()`): triangulates all six bounding faces and merges them.
493    ///
494    /// # Errors
495    ///
496    /// Returns [`Error::NotSurfaceOrVolume`] if the envelope is a point or line segment.
497    pub fn to_triangulated_surface(&self) -> Result<TriangulatedSurface, Error> {
498        if self.is_surface() {
499            self.to_polygon()?.triangulate()
500        } else if self.is_volume() {
501            self.to_solid()?
502                .exterior()
503                .as_ref()
504                .expect("must be created")
505                .object
506                .as_ref()
507                .expect("must be created")
508                .triangulate()
509        } else {
510            Err(Error::NotSurfaceOrVolume {
511                non_zero_extents: self.non_zero_extents(),
512            })
513        }
514    }
515}
516
517impl fmt::Display for Envelope {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        write!(
520            f,
521            "Envelope[{}, {}, {} -> {}, {}, {}]",
522            self.lower_corner.x(),
523            self.lower_corner.y(),
524            self.lower_corner.z(),
525            self.upper_corner.x(),
526            self.upper_corner.y(),
527            self.upper_corner.z()
528        )
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535
536    fn pos(x: f64, y: f64, z: f64) -> DirectPosition {
537        DirectPosition::new(x, y, z).unwrap()
538    }
539
540    fn env(lx: f64, ly: f64, lz: f64, ux: f64, uy: f64, uz: f64) -> Envelope {
541        Envelope::new(pos(lx, ly, lz), pos(ux, uy, uz)).unwrap()
542    }
543
544    #[test]
545    fn from_envelopes_empty_returns_none() {
546        let result = Envelope::from_envelopes(&[]);
547        assert!(result.is_none());
548    }
549
550    #[test]
551    fn from_envelopes_single_returns_same_envelope() {
552        let e = env(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
553        let result = Envelope::from_envelopes(&[e.clone()]).unwrap();
554
555        assert_eq!(result, e);
556    }
557
558    #[test]
559    fn from_envelopes_two_disjoint() {
560        let a = env(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
561        let b = env(5.0, 6.0, 7.0, 8.0, 9.0, 10.0);
562        let result = Envelope::from_envelopes(&[a, b]).unwrap();
563
564        assert_eq!(result, env(0.0, 0.0, 0.0, 8.0, 9.0, 10.0));
565    }
566
567    #[test]
568    fn from_envelopes_overlapping() {
569        let a = env(0.0, 0.0, 0.0, 5.0, 5.0, 5.0);
570        let b = env(3.0, 3.0, 3.0, 7.0, 7.0, 7.0);
571        let result = Envelope::from_envelopes(&[a, b]).unwrap();
572
573        assert_eq!(result, env(0.0, 0.0, 0.0, 7.0, 7.0, 7.0));
574    }
575
576    #[test]
577    fn from_envelopes_one_contains_the_other() {
578        let outer = env(0.0, 0.0, 0.0, 10.0, 10.0, 10.0);
579        let inner = env(2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
580        let result = Envelope::from_envelopes(&[outer.clone(), inner]).unwrap();
581
582        assert_eq!(result, outer);
583    }
584
585    #[test]
586    fn from_envelopes_multiple() {
587        let a = env(0.0, 10.0, 20.0, 1.0, 11.0, 21.0);
588        let b = env(-5.0, 8.0, 25.0, 2.0, 12.0, 30.0);
589        let c = env(1.0, 9.0, 18.0, 3.0, 15.0, 22.0);
590        let result = Envelope::from_envelopes(&[a, b, c]).unwrap();
591
592        assert_eq!(result, env(-5.0, 8.0, 18.0, 3.0, 15.0, 30.0));
593    }
594
595    #[test]
596    fn from_envelopes_with_negative_coords() {
597        let a = env(-10.0, -20.0, -30.0, -1.0, -2.0, -3.0);
598        let b = env(-5.0, -25.0, -15.0, 0.0, -1.0, 0.0);
599        let result = Envelope::from_envelopes(&[a, b]).unwrap();
600
601        assert_eq!(result, env(-10.0, -25.0, -30.0, 0.0, -1.0, 0.0));
602    }
603
604    #[test]
605    fn from_envelopes_zero_volume_envelopes() {
606        let a = env(1.0, 1.0, 1.0, 1.0, 1.0, 1.0); // point
607        let b = env(3.0, 3.0, 3.0, 3.0, 3.0, 3.0); // point
608        let result = Envelope::from_envelopes(&[a, b]).unwrap();
609
610        assert_eq!(result, env(1.0, 1.0, 1.0, 3.0, 3.0, 3.0));
611    }
612
613    #[test]
614    fn is_point_when_corners_equal() {
615        let e = env(1.0, 2.0, 3.0, 1.0, 2.0, 3.0);
616        assert!(e.is_point());
617        assert!(!e.is_linear());
618        assert!(!e.is_surface());
619        assert!(!e.is_volume());
620    }
621
622    #[test]
623    fn is_linear_along_x() {
624        let e = env(0.0, 0.0, 0.0, 1.0, 0.0, 0.0);
625        assert!(!e.is_point());
626        assert!(e.is_linear());
627        assert!(!e.is_surface());
628        assert!(!e.is_volume());
629    }
630
631    #[test]
632    fn is_linear_along_y() {
633        let e = env(0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
634        assert!(e.is_linear());
635    }
636
637    #[test]
638    fn is_linear_along_z() {
639        let e = env(0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
640        assert!(e.is_linear());
641    }
642
643    #[test]
644    fn is_surface_xy_plane() {
645        let e = env(0.0, 0.0, 0.0, 1.0, 1.0, 0.0);
646        assert!(!e.is_point());
647        assert!(!e.is_linear());
648        assert!(e.is_surface());
649        assert!(!e.is_volume());
650    }
651
652    #[test]
653    fn is_surface_xz_plane() {
654        let e = env(0.0, 0.0, 0.0, 1.0, 0.0, 1.0);
655        assert!(e.is_surface());
656    }
657
658    #[test]
659    fn is_surface_yz_plane() {
660        let e = env(0.0, 0.0, 0.0, 0.0, 1.0, 1.0);
661        assert!(e.is_surface());
662    }
663
664    #[test]
665    fn is_volume_all_extents_nonzero() {
666        let e = env(0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
667        assert!(!e.is_point());
668        assert!(!e.is_linear());
669        assert!(!e.is_surface());
670        assert!(e.is_volume());
671    }
672
673    #[test]
674    fn to_polygon_returns_err_for_point() {
675        assert_eq!(
676            env(1.0, 1.0, 1.0, 1.0, 1.0, 1.0).to_polygon(),
677            Err(Error::NotASurface {
678                non_zero_extents: 0
679            })
680        );
681    }
682
683    #[test]
684    fn to_polygon_returns_err_for_linear() {
685        assert_eq!(
686            env(0.0, 0.0, 0.0, 1.0, 0.0, 0.0).to_polygon(),
687            Err(Error::NotASurface {
688                non_zero_extents: 1
689            })
690        );
691    }
692
693    #[test]
694    fn to_polygon_returns_err_for_volume() {
695        assert_eq!(
696            env(0.0, 0.0, 0.0, 1.0, 1.0, 1.0).to_polygon(),
697            Err(Error::NotASurface {
698                non_zero_extents: 3
699            })
700        );
701    }
702
703    #[test]
704    fn to_polygon_xy_plane() {
705        assert!(env(0.0, 0.0, 0.0, 2.0, 3.0, 0.0).to_polygon().is_ok());
706    }
707
708    #[test]
709    fn to_polygon_xz_plane() {
710        assert!(env(0.0, 0.0, 0.0, 2.0, 0.0, 3.0).to_polygon().is_ok());
711    }
712
713    #[test]
714    fn to_polygon_yz_plane() {
715        assert!(env(0.0, 0.0, 0.0, 0.0, 2.0, 3.0).to_polygon().is_ok());
716    }
717
718    #[test]
719    fn to_triangulated_surface_returns_err_for_point() {
720        assert_eq!(
721            env(0.0, 0.0, 0.0, 0.0, 0.0, 0.0).to_triangulated_surface(),
722            Err(Error::NotSurfaceOrVolume {
723                non_zero_extents: 0
724            })
725        );
726    }
727
728    #[test]
729    fn to_triangulated_surface_returns_err_for_linear() {
730        assert_eq!(
731            env(0.0, 0.0, 0.0, 1.0, 0.0, 0.0).to_triangulated_surface(),
732            Err(Error::NotSurfaceOrVolume {
733                non_zero_extents: 1
734            })
735        );
736    }
737
738    #[test]
739    fn to_triangulated_surface_surface_has_two_triangles() {
740        let result = env(0.0, 0.0, 0.0, 2.0, 3.0, 0.0)
741            .to_triangulated_surface()
742            .unwrap();
743        assert_eq!(result.triangles().len(), 2);
744    }
745
746    #[test]
747    fn to_triangulated_surface_volume_has_twelve_triangles() {
748        let result = env(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)
749            .to_triangulated_surface()
750            .unwrap();
751        assert_eq!(result.triangles().len(), 12);
752    }
753
754    #[test]
755    fn envelope_contains() {
756        let lower_corner = DirectPosition::new(1.0, 2.0, 3.0).unwrap();
757        let upper_corner = DirectPosition::new(2.0, 3.0, 4.0).unwrap();
758        let envelope = Envelope::new(lower_corner, upper_corner).unwrap();
759        let point_a = DirectPosition::new(1.5, 2.5, 3.5).unwrap();
760        let point_b = DirectPosition::new(2.5, 3.5, 4.5).unwrap();
761
762        assert!(envelope.contains(&point_a));
763        assert!(!envelope.contains(&point_b));
764    }
765}