fj_kernel/validate/
face.rs

1use fj_math::Winding;
2
3use crate::objects::Face;
4
5use super::{Validate, ValidationConfig, ValidationError};
6
7impl Validate for Face {
8    fn validate_with_config(
9        &self,
10        _: &ValidationConfig,
11        errors: &mut Vec<ValidationError>,
12    ) {
13        FaceValidationError::check_interior_winding(self, errors);
14    }
15}
16
17/// [`Face`] validation error
18#[derive(Clone, Debug, thiserror::Error)]
19pub enum FaceValidationError {
20    /// Interior of [`Face`] has invalid winding; must be opposite of exterior
21    #[error(
22        "Interior of `Face` has invalid winding; must be opposite of exterior\n\
23        - Winding of exterior cycle: {exterior_winding:#?}\n\
24        - Winding of interior cycle: {interior_winding:#?}\n\
25        - `Face`: {face:#?}"
26    )]
27    InvalidInteriorWinding {
28        /// The winding of the [`Face`]'s exterior cycle
29        exterior_winding: Winding,
30
31        /// The winding of the invalid interior cycle
32        interior_winding: Winding,
33
34        /// The face
35        face: Face,
36    },
37}
38
39impl FaceValidationError {
40    fn check_interior_winding(face: &Face, errors: &mut Vec<ValidationError>) {
41        if face.exterior().half_edges().count() == 0 {
42            // Can't determine winding, if the cycle has no half-edges. Sounds
43            // like a job for a different validation check.
44            return;
45        }
46
47        let exterior_winding = face.exterior().winding();
48
49        for interior in face.interiors() {
50            if interior.half_edges().count() == 0 {
51                // Can't determine winding, if the cycle has no half-edges.
52                // Sounds like a job for a different validation check.
53                continue;
54            }
55            let interior_winding = interior.winding();
56
57            if exterior_winding == interior_winding {
58                errors.push(
59                    Self::InvalidInteriorWinding {
60                        exterior_winding,
61                        interior_winding,
62                        face: face.clone(),
63                    }
64                    .into(),
65                );
66            }
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use crate::{
74        algorithms::reverse::Reverse,
75        assert_contains_err,
76        objects::{Cycle, Face},
77        operations::{BuildCycle, BuildFace, Insert, UpdateFace},
78        services::Services,
79        validate::{FaceValidationError, Validate, ValidationError},
80    };
81
82    #[test]
83    fn face_invalid_interior_winding() -> anyhow::Result<()> {
84        let mut services = Services::new();
85
86        let valid =
87            Face::unbound(services.objects.surfaces.xy_plane(), &mut services)
88                .update_exterior(|_| {
89                    Cycle::polygon(
90                        [[0., 0.], [3., 0.], [0., 3.]],
91                        &mut services,
92                    )
93                    .insert(&mut services)
94                })
95                .add_interiors([Cycle::polygon(
96                    [[1., 1.], [1., 2.], [2., 1.]],
97                    &mut services,
98                )
99                .insert(&mut services)]);
100        let invalid = {
101            let interiors = valid
102                .interiors()
103                .cloned()
104                .map(|cycle| cycle.reverse(&mut services))
105                .collect::<Vec<_>>();
106
107            Face::new(
108                valid.surface().clone(),
109                valid.exterior().clone(),
110                interiors,
111                valid.color(),
112            )
113        };
114
115        valid.validate_and_return_first_error()?;
116        assert_contains_err!(
117            invalid,
118            ValidationError::Face(
119                FaceValidationError::InvalidInteriorWinding { .. }
120            )
121        );
122
123        services.only_validate(valid);
124
125        Ok(())
126    }
127}