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#[derive(Clone, Debug, thiserror::Error)]
30pub enum FaceValidationError {
31 #[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 exterior_winding: Winding,
41
42 interior_winding: Winding,
44
45 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 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 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}