Skip to main content

geop_core_topology/validation/
mod.rs

1//! Whole-model consistency checks — as opposed to
2//! [`super::argument_validation`], which checks a single euler operator
3//! call's own arguments. Each check lives in its own file, one check per
4//! file. Every check function takes `(params, errors, model)` and pushes
5//! its own violations onto `errors` rather than returning on the first one,
6//! so [`validate`] always runs every check and reports everything wrong in
7//! one pass.
8
9mod check_curves_and_surfaces_vertices;
10mod curve_and_surface_sampling_check;
11mod disjointness_check;
12mod face_face_numerical_intersection;
13mod face_orientation;
14mod faces_have_interior;
15mod holes_inside_outer;
16mod manifold_check;
17pub mod numerical_accuracy;
18mod parameters;
19mod pcurve_loop_continuity;
20mod pointer_check;
21mod two_way_references;
22
23pub use manifold_check::validate_manifold;
24pub use parameters::ValidationParameters;
25
26use crate::Model;
27use geop_core_math::{geop_error::GeopError, scalars::Scalar};
28
29pub fn validate<S: Scalar>(
30    params: &ValidationParameters<S>,
31    model: &Model<S>,
32) -> Result<(), Vec<GeopError>> {
33    let mut errors = Vec::new();
34
35    // First, because it explains the rest — see `check_numerical_accuracy`.
36    numerical_accuracy::check_numerical_accuracy(params, &mut errors, model);
37    pointer_check::check_pointers(params, &mut errors, model);
38    two_way_references::check_two_way_references(params, &mut errors, model);
39    check_curves_and_surfaces_vertices::check_curves_and_surfaces_vertices(
40        params,
41        &mut errors,
42        model,
43    );
44    curve_and_surface_sampling_check::check_curve_and_surface_sampling(params, &mut errors, model);
45    pcurve_loop_continuity::check_pcurve_loop_continuity(params, &mut errors, model);
46    disjointness_check::check_vertices_disjoint(params, &mut errors, model);
47    disjointness_check::check_edges_disjoint(params, &mut errors, model);
48    disjointness_check::check_edges_and_faces_consistent(params, &mut errors, model);
49    face_face_numerical_intersection::check_face_face_numerical_intersection(
50        params,
51        &mut errors,
52        model,
53    );
54    holes_inside_outer::check_holes_inside_outer(params, &mut errors, model);
55    face_orientation::check_loop_winding(params, &mut errors, model);
56    face_orientation::check_normals_point_outward(params, &mut errors, model);
57
58    if errors.is_empty() {
59        Ok(())
60    } else {
61        Err(errors)
62    }
63}
64
65/// A cheap subset of [`validate`]: only the checks that are purely
66/// structural/combinatorial (pointer validity, two-way `next`/`prev`/backref
67/// consistency, pcurve loop continuity) or a single pass over already-stored
68/// geometry (vertices vs. their edges'/faces' curves/surfaces) — none of the
69/// `O(n^2)` pairwise numerical-intersection searches (`disjointness_check`,
70/// `face_face_numerical_intersection`) or the sampling-based
71/// `curve_and_surface_sampling_check`. Meant for call sites that want a
72/// quick "is this model still well-formed" check after every mutating step
73/// (e.g. a test sweeping many scenes) without paying for the searches that
74/// dominate `validate`'s cost.
75pub fn validate_fast<S: Scalar>(
76    params: &ValidationParameters<S>,
77    model: &Model<S>,
78) -> Result<(), Vec<GeopError>> {
79    let mut errors = Vec::new();
80
81    // First, because it explains the rest: an entity carrying more
82    // uncertainty than the searches assume fails somewhere else entirely.
83    numerical_accuracy::check_numerical_accuracy(params, &mut errors, model);
84    pointer_check::check_pointers(params, &mut errors, model);
85    two_way_references::check_two_way_references(params, &mut errors, model);
86    check_curves_and_surfaces_vertices::check_curves_and_surfaces_vertices(
87        params,
88        &mut errors,
89        model,
90    );
91    pcurve_loop_continuity::check_pcurve_loop_continuity(params, &mut errors, model);
92    faces_have_interior::check_faces_have_interior(params, &mut errors, model);
93
94    if errors.is_empty() {
95        Ok(())
96    } else {
97        Err(errors)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::{ValidationParameters, validate};
104    use crate::Model;
105    use crate::test_fixtures::test_cube_solid;
106    use geop_core_math::{for_all_scalars, scalars::Scalar, vector::Vector3};
107
108    fn check_cube_passes_full_validate<S: Scalar>() {
109        let mut model = Model::<S>::new();
110        test_cube_solid(&mut model);
111        validate(&ValidationParameters::default(), &model).unwrap();
112    }
113    #[test]
114    fn cube_passes_full_validate() {
115        for_all_scalars!(check_cube_passes_full_validate);
116    }
117
118    fn check_independent_failures_are_all_reported<S: Scalar>() {
119        let mut model = Model::<S>::new();
120        test_cube_solid(&mut model);
121
122        // Break two independent checks at once: a shell/face backref
123        // mismatch (`check_two_way_references`) and a moved vertex that no
124        // longer matches its edges' curves (`check_curves_and_surfaces_vertices`).
125        let face_id = *model.faces.keys().next().unwrap();
126        let shell_id = model.faces[&face_id].shell;
127        model
128            .shells
129            .get_mut(&shell_id)
130            .unwrap()
131            .faces
132            .retain(|&f| f != face_id);
133
134        let vertex_id = *model.vertices.keys().next().unwrap();
135        model.vertices.get_mut(&vertex_id).unwrap().point =
136            Vector3::from_array([S::from_f64(42.0); 3]);
137
138        // Both the shell backref break and the moved vertex (which affects
139        // every edge/coedge touching it) show up; the exact count isn't the
140        // point, only that more than one independent failure got reported.
141        let errors = validate(&ValidationParameters::default(), &model).unwrap_err();
142        assert!(errors.len() >= 2);
143    }
144    #[test]
145    fn independent_failures_are_all_reported() {
146        for_all_scalars!(check_independent_failures_are_all_reported);
147    }
148}