fj_core/validate/
face.rs

1use fj_math::Winding;
2
3use crate::{
4    geometry::Geometry,
5    objects::Face,
6    validation::{
7        checks::FaceHasNoBoundary, ValidationCheck, ValidationConfig,
8        ValidationError,
9    },
10};
11
12use super::Validate;
13
14impl Validate for Face {
15    fn validate(
16        &self,
17        config: &ValidationConfig,
18        errors: &mut Vec<ValidationError>,
19        geometry: &Geometry,
20    ) {
21        errors.extend(
22            FaceHasNoBoundary::check(self, geometry, config).map(Into::into),
23        );
24        FaceValidationError::check_interior_winding(self, geometry, errors);
25    }
26}
27
28/// [`Face`] validation error
29#[derive(Clone, Debug, thiserror::Error)]
30pub enum FaceValidationError {
31    /// Interior of [`Face`] has invalid winding; must be opposite of exterior
32    #[error(
33        "Interior of `Face` has invalid winding; must be opposite of exterior\n\
34        - Winding of exterior cycle: {exterior_winding:#?}\n\
35        - Winding of interior cycle: {interior_winding:#?}\n\
36        - `Face`: {face:#?}"
37    )]
38    InvalidInteriorWinding {
39        /// The winding of the [`Face`]'s exterior cycle
40        exterior_winding: Winding,
41
42        /// The winding of the invalid interior cycle
43        interior_winding: Winding,
44
45        /// The face
46        face: Face,
47    },
48}
49
50impl FaceValidationError {
51    fn check_interior_winding(
52        face: &Face,
53        geometry: &Geometry,
54        errors: &mut Vec<ValidationError>,
55    ) {
56        if face.region().exterior().half_edges().is_empty() {
57            // Can't determine winding, if the cycle has no edges. Sounds like a
58            // job for a different validation check.
59            return;
60        }
61
62        let exterior_winding = face.region().exterior().winding(geometry);
63
64        for interior in face.region().interiors() {
65            if interior.half_edges().is_empty() {
66                // Can't determine winding, if the cycle has no edges. Sounds
67                // like a job for a different validation check.
68                continue;
69            }
70            let interior_winding = interior.winding(geometry);
71
72            if exterior_winding == interior_winding {
73                errors.push(
74                    Self::InvalidInteriorWinding {
75                        exterior_winding,
76                        interior_winding,
77                        face: face.clone(),
78                    }
79                    .into(),
80                );
81            }
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use crate::{
89        assert_contains_err,
90        objects::{Cycle, Face, Region},
91        operations::{
92            build::{BuildCycle, BuildFace},
93            derive::DeriveFrom,
94            insert::Insert,
95            reverse::Reverse,
96            update::{UpdateFace, UpdateRegion},
97        },
98        validate::{FaceValidationError, Validate},
99        validation::ValidationError,
100        Core,
101    };
102
103    #[test]
104    fn interior_winding() -> anyhow::Result<()> {
105        let mut core = Core::new();
106
107        let valid =
108            Face::unbound(core.layers.objects.surfaces.xy_plane(), &mut core)
109                .update_region(
110                    |region, core| {
111                        region
112                            .update_exterior(
113                                |_, core| {
114                                    Cycle::polygon(
115                                        [[0., 0.], [3., 0.], [0., 3.]],
116                                        core,
117                                    )
118                                },
119                                core,
120                            )
121                            .add_interiors(
122                                [Cycle::polygon(
123                                    [[1., 1.], [1., 2.], [2., 1.]],
124                                    core,
125                                )],
126                                core,
127                            )
128                    },
129                    &mut core,
130                );
131        let invalid = {
132            let interiors = valid
133                .region()
134                .interiors()
135                .iter()
136                .cloned()
137                .map(|cycle| {
138                    cycle
139                        .reverse(&mut core)
140                        .insert(&mut core)
141                        .derive_from(&cycle, &mut core)
142                })
143                .collect::<Vec<_>>();
144
145            let region =
146                Region::new(valid.region().exterior().clone(), interiors)
147                    .insert(&mut core);
148
149            Face::new(valid.surface().clone(), region)
150        };
151
152        valid.validate_and_return_first_error(&core.layers.geometry)?;
153        assert_contains_err!(
154            core,
155            invalid,
156            ValidationError::Face(
157                FaceValidationError::InvalidInteriorWinding { .. }
158            )
159        );
160
161        Ok(())
162    }
163}