1use std::{collections::BTreeMap, fmt};
2
3use fj_math::{Point, Scalar};
4
5use crate::{
6 geometry::{CurveBoundary, Geometry, SurfaceGeometry},
7 objects::{Curve, HalfEdge, Shell, Vertex},
8 queries::{
9 AllHalfEdgesWithSurface, BoundingVerticesOfHalfEdge, SiblingOfHalfEdge,
10 },
11 storage::{Handle, HandleWrapper},
12};
13
14use super::{Validate, ValidationConfig, ValidationError};
15
16impl Validate for Shell {
17 fn validate(
18 &self,
19 config: &ValidationConfig,
20 errors: &mut Vec<ValidationError>,
21 geometry: &Geometry,
22 ) {
23 ShellValidationError::check_curve_coordinates(
24 self, geometry, config, errors,
25 );
26 ShellValidationError::check_half_edge_pairs(self, errors);
27 ShellValidationError::check_half_edge_coincidence(
28 self, geometry, config, errors,
29 );
30 }
31}
32
33#[derive(Clone, Debug, thiserror::Error)]
35pub enum ShellValidationError {
36 #[error(
38 "Curve coordinate system mismatch ({} errors): {:#?}",
39 .0.len(),
40 .0
41 )]
42 CurveCoordinateSystemMismatch(Vec<CurveCoordinateSystemMismatch>),
43
44 #[error("Half-edge has no sibling: {half_edge:#?}")]
46 HalfEdgeHasNoSibling {
47 half_edge: Handle<HalfEdge>,
49 },
50
51 #[error(
53 "`Shell` contains `HalfEdge`s that are coincident but are not \
54 siblings\n\
55 {boundaries}\
56 {curves}\
57 {vertices}\
58 Half-edge 1: {half_edge_a:#?}\n\
59 Half-edge 2: {half_edge_b:#?}"
60 )]
61 CoincidentHalfEdgesAreNotSiblings {
62 boundaries: Box<CoincidentHalfEdgeBoundaries>,
64
65 curves: Box<CoincidentHalfEdgeCurves>,
67
68 vertices: Box<CoincidentHalfEdgeVertices>,
70
71 half_edge_a: Handle<HalfEdge>,
73
74 half_edge_b: Handle<HalfEdge>,
76 },
77}
78
79impl ShellValidationError {
80 fn check_curve_coordinates(
82 shell: &Shell,
83 geometry: &Geometry,
84 config: &ValidationConfig,
85 errors: &mut Vec<ValidationError>,
86 ) {
87 let mut edges_and_surfaces = Vec::new();
88 shell.all_half_edges_with_surface(&mut edges_and_surfaces);
89
90 for (edge_a, surface_a) in &edges_and_surfaces {
91 for (edge_b, surface_b) in &edges_and_surfaces {
92 if edge_a.curve().id() != edge_b.curve().id() {
94 continue;
95 }
96
97 if edge_a.id() == edge_b.id() {
99 continue;
100 }
101
102 fn compare_curve_coords(
103 edge_a: &Handle<HalfEdge>,
104 surface_a: &SurfaceGeometry,
105 edge_b: &Handle<HalfEdge>,
106 surface_b: &SurfaceGeometry,
107 geometry: &Geometry,
108 config: &ValidationConfig,
109 mismatches: &mut Vec<CurveCoordinateSystemMismatch>,
110 ) {
111 let [a, d] = edge_a.boundary().inner;
116 let b = a + (d - a) * 1. / 3.;
117 let c = a + (d - a) * 2. / 3.;
118
119 for point_curve in [a, b, c, d] {
120 let a_surface = geometry
121 .of_half_edge(edge_a)
122 .path
123 .point_from_path_coords(point_curve);
124 let b_surface = geometry
125 .of_half_edge(edge_b)
126 .path
127 .point_from_path_coords(point_curve);
128
129 let a_global =
130 surface_a.point_from_surface_coords(a_surface);
131 let b_global =
132 surface_b.point_from_surface_coords(b_surface);
133
134 let distance = (a_global - b_global).magnitude();
135
136 if distance > config.identical_max_distance {
137 mismatches.push(CurveCoordinateSystemMismatch {
138 half_edge_a: edge_a.clone(),
139 half_edge_b: edge_b.clone(),
140 point_curve,
141 point_a: a_global,
142 point_b: b_global,
143 distance,
144 });
145 }
146 }
147 }
148
149 let mut mismatches = Vec::new();
150
151 compare_curve_coords(
152 edge_a,
153 &geometry.of_surface(surface_a),
154 edge_b,
155 &geometry.of_surface(surface_b),
156 geometry,
157 config,
158 &mut mismatches,
159 );
160 compare_curve_coords(
161 edge_b,
162 &geometry.of_surface(surface_b),
163 edge_a,
164 &geometry.of_surface(surface_a),
165 geometry,
166 config,
167 &mut mismatches,
168 );
169
170 if !mismatches.is_empty() {
171 errors.push(
172 Self::CurveCoordinateSystemMismatch(mismatches).into(),
173 );
174 }
175 }
176 }
177 }
178
179 fn check_half_edge_pairs(shell: &Shell, errors: &mut Vec<ValidationError>) {
181 let mut unmatched_half_edges = BTreeMap::new();
182
183 for face in shell.faces() {
184 for cycle in face.region().all_cycles() {
185 for half_edge in cycle.half_edges() {
186 let curve = HandleWrapper::from(half_edge.curve().clone());
187 let boundary = half_edge.boundary();
188 let vertices =
189 cycle.bounding_vertices_of_half_edge(half_edge).expect(
190 "`half_edge` came from `cycle`, must exist there",
191 );
192
193 let key = (curve.clone(), boundary, vertices.clone());
194 let key_reversed =
195 (curve, boundary.reverse(), vertices.reverse());
196
197 match unmatched_half_edges.remove(&key_reversed) {
198 Some(sibling) => {
199 assert!(shell.are_siblings(half_edge, sibling));
204 }
205 None => {
206 unmatched_half_edges.insert(key, half_edge);
210 }
211 }
212 }
213 }
214 }
215
216 for half_edge in unmatched_half_edges.into_values().cloned() {
217 errors.push(Self::HalfEdgeHasNoSibling { half_edge }.into());
218 }
219 }
220
221 fn check_half_edge_coincidence(
223 shell: &Shell,
224 geometry: &Geometry,
225 config: &ValidationConfig,
226 errors: &mut Vec<ValidationError>,
227 ) {
228 let mut edges_and_surfaces = Vec::new();
229 shell.all_half_edges_with_surface(&mut edges_and_surfaces);
230
231 for (half_edge_a, surface_a) in &edges_and_surfaces {
235 for (half_edge_b, surface_b) in &edges_and_surfaces {
236 if half_edge_a.id() == half_edge_b.id() {
238 continue;
239 }
240
241 if shell.are_siblings(half_edge_a, half_edge_b) {
242 continue;
246 }
247
248 if distances(
251 half_edge_a.clone(),
252 &geometry.of_surface(surface_a),
253 half_edge_b.clone(),
254 &geometry.of_surface(surface_b),
255 geometry,
256 )
257 .all(|d| d < config.distinct_min_distance)
258 {
259 let boundaries = Box::new(CoincidentHalfEdgeBoundaries {
260 boundaries: [half_edge_a, half_edge_b]
261 .map(|half_edge| half_edge.boundary()),
262 });
263 let curves = Box::new(CoincidentHalfEdgeCurves {
264 curves: [half_edge_a, half_edge_b]
265 .map(|half_edge| half_edge.curve().clone()),
266 });
267 let vertices = Box::new(CoincidentHalfEdgeVertices {
268 vertices: [half_edge_a, half_edge_b].map(|half_edge| {
269 shell
270 .bounding_vertices_of_half_edge(half_edge)
271 .expect(
272 "Expected half-edge to be part of shell",
273 )
274 }),
275 });
276
277 errors.push(
278 Self::CoincidentHalfEdgesAreNotSiblings {
279 boundaries,
280 curves,
281 vertices,
282 half_edge_a: half_edge_a.clone(),
283 half_edge_b: half_edge_b.clone(),
284 }
285 .into(),
286 )
287 }
288 }
289 }
290 }
291}
292
293#[derive(Clone, Debug)]
294pub struct CoincidentHalfEdgeBoundaries {
295 pub boundaries: [CurveBoundary<Point<1>>; 2],
296}
297
298impl fmt::Display for CoincidentHalfEdgeBoundaries {
299 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
300 let [a, b] = &self.boundaries;
301
302 if a != &b.reverse() {
303 writeln!(
304 f,
305 "Boundaries don't match.\n\
306 \tHalf-edge 1 has boundary `{a:?}`\n\
307 \tHalf-edge 2 has boundary `{b:?}`\n\
308 \t(expecting same boundary, but reversed)"
309 )?;
310 }
311
312 Ok(())
313 }
314}
315
316#[derive(Clone, Debug)]
317pub struct CoincidentHalfEdgeCurves {
318 pub curves: [Handle<Curve>; 2],
319}
320
321impl fmt::Display for CoincidentHalfEdgeCurves {
322 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323 let [a, b] = &self.curves;
324
325 if a.id() != b.id() {
326 writeln!(
327 f,
328 "Curves don't match.\n\
329 \tHalf-edge 1 lies on {a:?}\n\
330 \tHalf-edge 2 lies on {b:?}\n\
331 \t(must be the same)"
332 )?;
333 }
334
335 Ok(())
336 }
337}
338
339#[derive(Clone, Debug)]
340pub struct CurveCoordinateSystemMismatch {
341 pub half_edge_a: Handle<HalfEdge>,
342 pub half_edge_b: Handle<HalfEdge>,
343 pub point_curve: Point<1>,
344 pub point_a: Point<3>,
345 pub point_b: Point<3>,
346 pub distance: Scalar,
347}
348
349#[derive(Clone, Debug)]
350pub struct CoincidentHalfEdgeVertices {
351 pub vertices: [CurveBoundary<Vertex>; 2],
352}
353
354impl fmt::Display for CoincidentHalfEdgeVertices {
355 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
356 let [a, b] = &self.vertices;
357
358 if a != &b.clone().reverse() {
359 writeln!(
360 f,
361 "Vertices don't match.\n\
362 \tHalf-edge 1 is bounded by `{a:?}`\n\
363 \tHalf-edge 2 is bounded by `{b:?}`\n\
364 \t(expecting same vertices, but in reverse order)"
365 )?;
366 }
367
368 Ok(())
369 }
370}
371
372fn distances(
376 edge_a: Handle<HalfEdge>,
377 surface_a: &SurfaceGeometry,
378 edge_b: Handle<HalfEdge>,
379 surface_b: &SurfaceGeometry,
380 geometry: &Geometry,
381) -> impl Iterator<Item = Scalar> {
382 fn sample(
383 percent: f64,
384 (edge, surface): (&Handle<HalfEdge>, &SurfaceGeometry),
385 geometry: &Geometry,
386 ) -> Point<3> {
387 let [start, end] = edge.boundary().inner;
388 let path_coords = start + (end - start) * percent;
389 let surface_coords = geometry
390 .of_half_edge(edge)
391 .path
392 .point_from_path_coords(path_coords);
393 surface.point_from_surface_coords(surface_coords)
394 }
395
396 let sample_count = 3;
400 let step = 1.0 / (sample_count as f64 - 1.0);
401
402 let mut distances = Vec::new();
403 for i in 0..sample_count {
404 let percent = i as f64 * step;
405 let sample1 = sample(percent, (&edge_a, surface_a), geometry);
406 let sample2 = sample(1.0 - percent, (&edge_b, surface_b), geometry);
407 distances.push(sample1.distance_to(&sample2))
408 }
409 distances.into_iter()
410}
411
412#[cfg(test)]
413mod tests {
414 use crate::{
415 assert_contains_err,
416 objects::{Curve, Shell},
417 operations::{
418 build::BuildShell,
419 geometry::UpdateHalfEdgeGeometry,
420 insert::Insert,
421 update::{
422 UpdateCycle, UpdateFace, UpdateHalfEdge, UpdateRegion,
423 UpdateShell,
424 },
425 },
426 validate::{shell::ShellValidationError, Validate, ValidationError},
427 Core,
428 };
429
430 #[test]
431 fn curve_coordinate_system_mismatch() -> anyhow::Result<()> {
432 let mut core = Core::new();
433
434 let valid = Shell::tetrahedron(
435 [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
436 &mut core,
437 );
438 let invalid = valid.shell.update_face(
439 &valid.abc.face,
440 |face, core| {
441 [face.update_region(
442 |region, core| {
443 region.update_exterior(
444 |cycle, core| {
445 cycle.update_half_edge(
446 cycle.half_edges().nth_circular(0),
447 |half_edge, core| {
448 [half_edge
477 .update_path(
478 |path| path.reverse(),
479 core,
480 )
481 .update_boundary(
482 |boundary| boundary.reverse(),
483 core,
484 )
485 .set_path(
486 core.layers
487 .geometry
488 .of_half_edge(half_edge)
489 .path
490 .reverse(),
491 &mut core.layers.geometry,
492 )]
493 },
494 core,
495 )
496 },
497 core,
498 )
499 },
500 core,
501 )]
502 },
503 &mut core,
504 );
505
506 valid
507 .shell
508 .validate_and_return_first_error(&core.layers.geometry)?;
509 assert_contains_err!(
510 core,
511 invalid,
512 ValidationError::Shell(
513 ShellValidationError::CurveCoordinateSystemMismatch(..)
514 )
515 );
516
517 Ok(())
518 }
519
520 #[test]
521 fn half_edge_has_no_sibling() -> anyhow::Result<()> {
522 let mut core = Core::new();
523
524 let valid = Shell::tetrahedron(
525 [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
526 &mut core,
527 );
528 let invalid = valid.shell.remove_face(&valid.abc.face);
529
530 valid
531 .shell
532 .validate_and_return_first_error(&core.layers.geometry)?;
533 assert_contains_err!(
534 core,
535 invalid,
536 ValidationError::Shell(
537 ShellValidationError::HalfEdgeHasNoSibling { .. }
538 )
539 );
540
541 Ok(())
542 }
543
544 #[test]
545 fn coincident_half_edges_are_not_siblings() -> anyhow::Result<()> {
546 let mut core = Core::new();
547
548 let valid = Shell::tetrahedron(
549 [[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [0., 0., 1.]],
550 &mut core,
551 );
552 let invalid = valid.shell.update_face(
553 &valid.abc.face,
554 |face, core| {
555 [face.update_region(
556 |region, core| {
557 region.update_exterior(
558 |cycle, core| {
559 cycle.update_half_edge(
560 cycle.half_edges().nth_circular(0),
561 |half_edge, core| {
562 [half_edge
563 .update_curve(
564 |_, _| Curve::new(),
565 core,
566 )
567 .insert(core)
568 .set_path(
569 core.layers
570 .geometry
571 .of_half_edge(half_edge)
572 .path,
573 &mut core.layers.geometry,
574 )]
575 },
576 core,
577 )
578 },
579 core,
580 )
581 },
582 core,
583 )]
584 },
585 &mut core,
586 );
587
588 valid
589 .shell
590 .validate_and_return_first_error(&core.layers.geometry)?;
591 assert_contains_err!(
592 core,
593 invalid,
594 ValidationError::Shell(
595 ShellValidationError::CoincidentHalfEdgesAreNotSiblings { .. }
596 )
597 );
598
599 Ok(())
600 }
601}