Skip to main content

geop_core_topology/validation/
mod.rs

1//! Whole-model consistency checks. Each check lives in its own file, one
2//! check per file. Every check function takes `(params, errors, model)` and pushes
3//! its own violations onto `errors` rather than returning on the first one,
4//! so [`validate`] always runs every check and reports everything wrong in
5//! one pass.
6
7mod check_curves_and_surfaces_vertices;
8mod curve_and_surface_sampling_check;
9mod disjointness_check;
10mod face_face_numerical_intersection;
11mod face_orientation;
12mod faces_have_interior;
13mod holes_inside_outer;
14mod manifold_check;
15pub mod numerical_accuracy;
16mod parameters;
17mod pcurve_loop_continuity;
18mod pointer_check;
19mod two_way_references;
20
21pub use disjointness_check::{body_crossing, edge_crosses_face};
22pub use face_orientation::{Inward, Outward, normal_points_outward};
23pub use manifold_check::validate_manifold;
24pub use parameters::ValidationParameters;
25
26use std::collections::{BTreeMap, HashMap};
27
28use crate::{Body, CoedgeGeometry, EdgeId, FaceId, Model, VertexId};
29use geop_core_math::geop_error::GeopError;
30
31/// The vertices, edges and faces of one body (see [`body_groups`]).
32#[derive(Default)]
33pub(crate) struct BodyGroup {
34    pub vertices: Vec<VertexId>,
35    pub edges: Vec<EdgeId>,
36    pub faces: Vec<FaceId>,
37}
38
39/// The model's entities, grouped by the body they belong to — what must not
40/// overlap, or cross without sharing a vertex, is the entities of one body:
41/// separate bodies may touch, or overlap, as they please (the two halves of
42/// a split, a new body extruded into another). A wire is a group of its own
43/// too. An entity of no body — of a face whose shell is not there, as in a
44/// model still being put together — is in a group of its own with all
45/// others like it.
46pub(crate) fn body_groups(model: &Model) -> Vec<BodyGroup> {
47    let body_of_face = |face: FaceId| model.body_of_face(face).ok();
48    let body_of_coedge = |c: &crate::Coedge| body_of_face(c.face);
49    let mut edge_body: HashMap<EdgeId, Option<Body>> = HashMap::new();
50    let mut vertex_body: HashMap<VertexId, Option<Body>> = HashMap::new();
51    for coedge in model.coedges.values() {
52        let body = body_of_coedge(coedge);
53        match coedge.geometry {
54            CoedgeGeometry::Edge(e) => {
55                edge_body.entry(e).or_insert(body);
56            }
57            CoedgeGeometry::Vertex(v) => {
58                vertex_body.entry(v).or_insert(body);
59            }
60        }
61    }
62    for (&id, edge) in &model.edges {
63        let body = edge_body.get(&id).copied().flatten();
64        for v in [edge.start_vertex, edge.end_vertex] {
65            vertex_body.entry(v).or_insert(body);
66        }
67    }
68    let key = |body: Option<Body>| match body {
69        None => (0, 0),
70        Some(Body::Solid(s)) => (1, s.0),
71        Some(Body::Sheet(s)) => (2, s.0),
72    };
73    let mut vertex_wire = HashMap::new();
74    let mut edge_wire = HashMap::new();
75    for (&id, wire) in &model.wires {
76        vertex_wire.extend(wire.vertices.iter().map(|&v| (v, (3, id.0))));
77        edge_wire.extend(wire.edges.iter().map(|&e| (e, (3, id.0))));
78    }
79    let mut groups: BTreeMap<(u8, u64), BodyGroup> = BTreeMap::new();
80    for &id in model.vertices.keys() {
81        let group = vertex_wire
82            .get(&id)
83            .copied()
84            .unwrap_or_else(|| key(vertex_body.get(&id).copied().flatten()));
85        groups.entry(group).or_default().vertices.push(id);
86    }
87    for &id in model.edges.keys() {
88        let group = edge_wire
89            .get(&id)
90            .copied()
91            .unwrap_or_else(|| key(edge_body.get(&id).copied().flatten()));
92        groups.entry(group).or_default().edges.push(id);
93    }
94    for &id in model.faces.keys() {
95        groups
96            .entry(key(body_of_face(id)))
97            .or_default()
98            .faces
99            .push(id);
100    }
101    groups.into_values().collect()
102}
103
104pub fn validate(params: &ValidationParameters, model: &Model) -> Result<(), Vec<GeopError>> {
105    let mut errors = Vec::new();
106
107    // First, because it explains the rest — see `check_numerical_accuracy`.
108    numerical_accuracy::check_numerical_accuracy(params, &mut errors, model);
109    pointer_check::check_pointers(params, &mut errors, model);
110    two_way_references::check_two_way_references(params, &mut errors, model);
111    check_curves_and_surfaces_vertices::check_curves_and_surfaces_vertices(
112        params,
113        &mut errors,
114        model,
115    );
116    curve_and_surface_sampling_check::check_curve_and_surface_sampling(params, &mut errors, model);
117    pcurve_loop_continuity::check_pcurve_loop_continuity(params, &mut errors, model);
118    disjointness_check::check_vertices_disjoint(params, &mut errors, model);
119    disjointness_check::check_edges_disjoint(params, &mut errors, model);
120    disjointness_check::check_edges_and_faces_consistent(params, &mut errors, model);
121    face_face_numerical_intersection::check_face_face_numerical_intersection(
122        params,
123        &mut errors,
124        model,
125    );
126    holes_inside_outer::check_holes_inside_outer(params, &mut errors, model);
127    face_orientation::check_loop_winding(params, &mut errors, model);
128    face_orientation::check_normals_point_outward(params, &mut errors, model);
129
130    if errors.is_empty() {
131        Ok(())
132    } else {
133        Err(errors)
134    }
135}
136
137/// A cheap subset of [`validate`]: only the checks that are purely
138/// structural/combinatorial (pointer validity, two-way `next`/`prev`/backref
139/// consistency, pcurve loop continuity) or a single pass over already-stored
140/// geometry (vertices vs. their edges'/faces' curves/surfaces) — none of the
141/// `O(n^2)` pairwise numerical-intersection searches (`disjointness_check`,
142/// `face_face_numerical_intersection`) or the sampling-based
143/// `curve_and_surface_sampling_check`. Meant for call sites that want a
144/// quick "is this model still well-formed" check after every mutating step
145/// (e.g. a test sweeping many scenes) without paying for the searches that
146/// dominate `validate`'s cost.
147pub fn validate_fast(params: &ValidationParameters, model: &Model) -> Result<(), Vec<GeopError>> {
148    let mut errors = Vec::new();
149
150    // First, because it explains the rest: an entity carrying more
151    // uncertainty than the searches assume fails somewhere else entirely.
152    numerical_accuracy::check_numerical_accuracy(params, &mut errors, model);
153    pointer_check::check_pointers(params, &mut errors, model);
154    two_way_references::check_two_way_references(params, &mut errors, model);
155    check_curves_and_surfaces_vertices::check_curves_and_surfaces_vertices(
156        params,
157        &mut errors,
158        model,
159    );
160    pcurve_loop_continuity::check_pcurve_loop_continuity(params, &mut errors, model);
161    faces_have_interior::check_faces_have_interior(params, &mut errors, model);
162
163    if errors.is_empty() {
164        Ok(())
165    } else {
166        Err(errors)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{ValidationParameters, validate};
173    use crate::Model;
174    use crate::test_fixtures::test_cube_solid;
175    use geop_core_math::{scalars::Scalar, vector::Vector3};
176
177    #[test]
178    fn cube_passes_full_validate() {
179        let mut model = Model::new();
180        test_cube_solid(&mut model);
181        validate(&ValidationParameters::default(), &model).unwrap();
182    }
183
184    #[test]
185    fn independent_failures_are_all_reported() {
186        let mut model = Model::new();
187        test_cube_solid(&mut model);
188
189        // Break two independent checks at once: a shell/face backref
190        // mismatch (`check_two_way_references`) and a moved vertex that no
191        // longer matches its edges' curves (`check_curves_and_surfaces_vertices`).
192        let face_id = *model.faces.keys().next().unwrap();
193        let shell_id = model.faces[&face_id].shell;
194        model
195            .shells
196            .get_mut(&shell_id)
197            .unwrap()
198            .faces
199            .retain(|&f| f != face_id);
200
201        let vertex_id = *model.vertices.keys().next().unwrap();
202        model.vertices.get_mut(&vertex_id).unwrap().point =
203            Vector3::from_array([Scalar::from_f64(42.0); 3]);
204
205        // Both the shell backref break and the moved vertex (which affects
206        // every edge/coedge touching it) show up; the exact count isn't the
207        // point, only that more than one independent failure got reported.
208        let errors = validate(&ValidationParameters::default(), &model).unwrap_err();
209        assert!(errors.len() >= 2);
210    }
211}