1use std::{collections::HashMap, iter::repeat};
2
3use fj_math::{Point, Scalar};
4
5use crate::{
6 geometry::surface::SurfaceGeometry,
7 objects::{HalfEdge, Shell, Surface},
8 storage::{Handle, ObjectId},
9};
10
11use super::{Validate, ValidationConfig, ValidationError};
12
13impl Validate for Shell {
14 fn validate_with_config(
15 &self,
16 config: &ValidationConfig,
17 errors: &mut Vec<ValidationError>,
18 ) {
19 ShellValidationError::validate_edges_coincident(self, config, errors);
20 ShellValidationError::validate_watertight(self, config, errors);
21 }
22}
23
24#[derive(Clone, Debug, thiserror::Error)]
26pub enum ShellValidationError {
27 #[error("Shell is not watertight")]
29 NotWatertight,
30
31 #[error(
33 "`Shell` contains `HalfEdge`s that are coincident but refer to \
34 different `GlobalEdge`s\n\
35 Edge 1: {0:#?}\n\
36 Edge 2: {1:#?}"
37 )]
38 CoincidentEdgesNotIdentical(Handle<HalfEdge>, Handle<HalfEdge>),
39
40 #[error(
42 "Shell contains HalfEdges that are identical but do not coincide\n\
43 Edge 1: {edge_1:#?}\n\
44 Surface for edge 1: {surface_1:#?}\n\
45 Edge 2: {edge_2:#?}\n\
46 Surface for edge 2: {surface_2:#?}"
47 )]
48 IdenticalEdgesNotCoincident {
49 edge_1: Handle<HalfEdge>,
51
52 surface_1: Handle<Surface>,
54
55 edge_2: Handle<HalfEdge>,
57
58 surface_2: Handle<Surface>,
60 },
61}
62
63fn distances(
67 config: &ValidationConfig,
68 (edge1, surface1): (Handle<HalfEdge>, Handle<Surface>),
69 (edge2, surface2): (Handle<HalfEdge>, Handle<Surface>),
70) -> impl Iterator<Item = Scalar> {
71 fn sample(
72 percent: f64,
73 (edge, surface): (&Handle<HalfEdge>, SurfaceGeometry),
74 ) -> Point<3> {
75 let boundary = edge.boundary();
76 let path_coords = boundary[0] + (boundary[1] - boundary[0]) * percent;
77 let surface_coords = edge.curve().point_from_path_coords(path_coords);
78 surface.point_from_surface_coords(surface_coords)
79 }
80
81 let flip = sample(0.0, (&edge1, surface1.geometry()))
83 .distance_to(&sample(0.0, (&edge2, surface2.geometry())))
84 > config.identical_max_distance;
85
86 let sample_count = 3;
90 let step = 1.0 / (sample_count as f64 - 1.0);
91
92 let mut distances = Vec::new();
93 for i in 0..sample_count {
94 let percent = i as f64 * step;
95 let sample1 = sample(percent, (&edge1, surface1.geometry()));
96 let sample2 = sample(
97 if flip { 1.0 - percent } else { percent },
98 (&edge2, surface2.geometry()),
99 );
100 distances.push(sample1.distance_to(&sample2))
101 }
102 distances.into_iter()
103}
104
105impl ShellValidationError {
106 fn validate_edges_coincident(
107 shell: &Shell,
108 config: &ValidationConfig,
109 errors: &mut Vec<ValidationError>,
110 ) {
111 let edges_and_surfaces: Vec<_> = shell
112 .faces()
113 .into_iter()
114 .flat_map(|face| {
115 face.all_cycles()
116 .flat_map(|cycle| cycle.half_edges().cloned())
117 .zip(repeat(face.surface().clone()))
118 })
119 .collect();
120
121 for edge in &edges_and_surfaces {
125 for other_edge in &edges_and_surfaces {
126 let id = edge.0.global_form().id();
127 let other_id = other_edge.0.global_form().id();
128 let identical = id == other_id;
129 match identical {
130 true => {
131 if distances(config, edge.clone(), other_edge.clone())
135 .any(|d| d > config.identical_max_distance)
136 {
137 errors.push(
138 Self::IdenticalEdgesNotCoincident {
139 edge_1: edge.0.clone(),
140 surface_1: edge.1.clone(),
141 edge_2: other_edge.0.clone(),
142 surface_2: other_edge.1.clone(),
143 }
144 .into(),
145 )
146 }
147 }
148 false => {
149 if distances(config, edge.clone(), other_edge.clone())
152 .all(|d| d < config.distinct_min_distance)
153 {
154 errors.push(
155 Self::CoincidentEdgesNotIdentical(
156 edge.0.clone(),
157 other_edge.0.clone(),
158 )
159 .into(),
160 )
161 }
162 }
163 }
164 }
165 }
166 }
167
168 fn validate_watertight(
169 shell: &Shell,
170 _: &ValidationConfig,
171 errors: &mut Vec<ValidationError>,
172 ) {
173 let faces = shell.faces();
174 let mut half_edge_to_faces: HashMap<ObjectId, usize> = HashMap::new();
175 for face in faces {
176 for cycle in face.all_cycles() {
177 for half_edge in cycle.half_edges() {
178 let id = half_edge.global_form().id();
179 let entry = half_edge_to_faces.entry(id);
180 *entry.or_insert(0) += 1;
181 }
182 }
183 }
184
185 if half_edge_to_faces.iter().any(|(_, c)| *c != 2) {
187 errors.push(Self::NotWatertight.into())
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use crate::{
195 assert_contains_err,
196 objects::{GlobalEdge, Shell},
197 operations::{
198 BuildShell, Insert, UpdateCycle, UpdateFace, UpdateHalfEdge,
199 UpdateShell,
200 },
201 services::Services,
202 validate::{shell::ShellValidationError, Validate, ValidationError},
203 };
204
205 #[test]
206 fn coincident_not_identical() -> anyhow::Result<()> {
207 let mut services = Services::new();
208
209 let valid = Shell::tetrahedron(
210 [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
211 &mut services,
212 );
213 let invalid = valid.shell.replace_face(
214 &valid.abc.face,
215 valid
216 .abc
217 .face
218 .update_exterior(|cycle| {
219 cycle
220 .update_nth_half_edge(0, |half_edge| {
221 let global_form =
222 GlobalEdge::new().insert(&mut services);
223 half_edge
224 .replace_global_form(global_form)
225 .insert(&mut services)
226 })
227 .insert(&mut services)
228 })
229 .insert(&mut services),
230 );
231
232 valid.shell.validate_and_return_first_error()?;
233 assert_contains_err!(
234 invalid,
235 ValidationError::Shell(
236 ShellValidationError::CoincidentEdgesNotIdentical(..)
237 )
238 );
239
240 Ok(())
241 }
242
243 #[test]
244 fn shell_not_watertight() -> anyhow::Result<()> {
245 let mut services = Services::new();
246
247 let valid = Shell::tetrahedron(
248 [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
249 &mut services,
250 );
251 let invalid = valid.shell.remove_face(&valid.abc.face);
252
253 valid.shell.validate_and_return_first_error()?;
254 assert_contains_err!(
255 invalid,
256 ValidationError::Shell(ShellValidationError::NotWatertight)
257 );
258
259 Ok(())
260 }
261}