fj_core/validate/
shell.rs

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/// [`Shell`] validation failed
34#[derive(Clone, Debug, thiserror::Error)]
35pub enum ShellValidationError {
36    /// [`Shell`] contains curves whose coordinate systems don't match
37    #[error(
38        "Curve coordinate system mismatch ({} errors): {:#?}",
39        .0.len(),
40        .0
41    )]
42    CurveCoordinateSystemMismatch(Vec<CurveCoordinateSystemMismatch>),
43
44    /// [`Shell`] contains a half-edge that is not part of a pair
45    #[error("Half-edge has no sibling: {half_edge:#?}")]
46    HalfEdgeHasNoSibling {
47        /// The half-edge that has no sibling
48        half_edge: Handle<HalfEdge>,
49    },
50
51    /// [`Shell`] contains half-edges that are coincident, but aren't siblings
52    #[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        /// The boundaries of the half-edges
63        boundaries: Box<CoincidentHalfEdgeBoundaries>,
64
65        /// The curves of the half-edges
66        curves: Box<CoincidentHalfEdgeCurves>,
67
68        /// The vertices of the half-edges
69        vertices: Box<CoincidentHalfEdgeVertices>,
70
71        /// The first half-edge
72        half_edge_a: Handle<HalfEdge>,
73
74        /// The second half-edge
75        half_edge_b: Handle<HalfEdge>,
76    },
77}
78
79impl ShellValidationError {
80    /// Check that local curve definitions that refer to the same curve match
81    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                // We only care about edges referring to the same curve.
93                if edge_a.curve().id() != edge_b.curve().id() {
94                    continue;
95                }
96
97                // No need to check an edge against itself.
98                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's check 4 points. Given that the most complex curves
112                    // we have right now are circles, 3 would be enough to check
113                    // for coincidence. But the first and last might be
114                    // identical, so let's add an extra one.
115                    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    /// Check that each half-edge is part of a pair
180    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                            // This must be the sibling of the half-edge we're
200                            // currently looking at. Let's make sure the logic
201                            // we use here to determine that matches the
202                            // "official" definition.
203                            assert!(shell.are_siblings(half_edge, sibling));
204                        }
205                        None => {
206                            // If this half-edge has a sibling, we haven't seen
207                            // it yet. Let's store this half-edge then, in case
208                            // we come across the sibling later.
209                            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    /// Check that non-sibling half-edges are not coincident
222    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        // This is O(N^2) which isn't great, but we can't use a HashMap since we
232        // need to deal with float inaccuracies. Maybe we could use some smarter
233        // data-structure like an octree.
234        for (half_edge_a, surface_a) in &edges_and_surfaces {
235            for (half_edge_b, surface_b) in &edges_and_surfaces {
236                // No need to check a half-edge against itself.
237                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                    // If the half-edges are siblings, they are allowed to be
243                    // coincident. Must be, in fact. There's another validation
244                    // check that takes care of that.
245                    continue;
246                }
247
248                // If all points on distinct curves are within
249                // `distinct_min_distance`, that's a problem.
250                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
372/// Sample two edges at various (currently 3) points in 3D along them.
373///
374/// Returns an [`Iterator`] of the distance at each sample.
375fn 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    // Three samples (start, middle, end), are enough to detect weather lines
397    // and circles match. If we were to add more complicated curves, this might
398    // need to change.
399    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                                        // This is going to be weird:
449                                        //
450                                        // - That first call to `update_path` is
451                                        //   going to reverse a path and insert
452                                        //   a new object with the reversed
453                                        //   path.
454                                        // - The next call to `update_boundary`
455                                        //   relies on that, because it inserts
456                                        //   an object too, and that would cause
457                                        //   a validation failure without the
458                                        //   first call.
459                                        // - But the new object created from
460                                        //   those two operations doesn't
461                                        //   actually have its geometry set in
462                                        //   the geometry layer, because that
463                                        //   happens in `update_path`, for an
464                                        //   earlier version of the object.
465                                        // - So we need a last `set_path`, which
466                                        //   sets the path again.
467                                        //
468                                        // This is very weird, but good new is,
469                                        // it's just an artifact of the
470                                        // transition from a unified object
471                                        // graph to separate topology and
472                                        // geometry layers. This should clear up
473                                        // again, once the separation is
474                                        // finished, and all APIs can adapt to
475                                        // the new reality.
476                                        [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}