geop_core_topology/validation/
manifold_check.rs1use super::{ValidationParameters, validate};
2use geop_core_math::{geop_error::GeopError, plain, scalars::Scalar, vector::Vector3};
3
4use crate::{
5 Model, Sense,
6 contains::{
7 rng::Rng,
8 shell::{PointClassification, ShellBoundary, cast_ray},
9 },
10};
11
12fn check_every_edge_has_paired_coedges(errors: &mut Vec<GeopError>, model: &Model) {
27 let wire_edges: std::collections::HashSet<crate::EdgeId> = model
28 .wires
29 .values()
30 .flat_map(|w| w.edges.iter().copied())
31 .collect();
32 for &edge_id in model.edges.keys().filter(|e| !wire_edges.contains(e)) {
33 let coedge_ids = model.coedges_of_edge(edge_id);
34 let n = coedge_ids.len();
35 let on_sheet = |coedge: &crate::CoedgeId| {
36 model
37 .body_of_face(model.coedges[coedge].face)
38 .is_ok_and(|body| matches!(body, crate::Body::Sheet(_)))
39 };
40 if n == 1 && on_sheet(&coedge_ids[0]) {
41 continue;
42 }
43 if n == 0 || !n.is_multiple_of(2) {
44 errors.push(GeopError::new(format!(
45 "edge {} has {} coedge(s) (expected an even number, two where two faces meet)",
46 edge_id.0, n
47 )));
48 continue;
49 }
50 let forward = coedge_ids
51 .iter()
52 .filter(|&id| model.coedges[id].sense == Sense::Forward)
53 .count();
54 if 2 * forward != n {
55 errors.push(GeopError::new(format!(
56 "edge {}'s {n} coedges {coedge_ids:?} run it Forward {forward} time(s) (expected as many Forward as Reversed)",
57 edge_id.0
58 )));
59 }
60 }
61}
62
63fn check_ray_direction_consistency(
76 params: &ValidationParameters,
77 errors: &mut Vec<GeopError>,
78 model: &Model,
79) {
80 for (&shell_id, _) in model.shells.iter().filter(|(_, s)| s.solid.is_some()) {
82 let boundary = ShellBoundary::of(model, shell_id);
83 let points = (boundary.vertices.iter()).map(|v| model.vertices[v].point.to_f64_array());
86 let Some((lo, hi)) = plain::bounds(points) else {
87 continue;
88 };
89 let margin: Vec<f64> = (0..3)
92 .map(|axis| (hi[axis] - lo[axis]).max(1.0) * 0.5)
93 .collect();
94
95 let mut rng = Rng::new(params.manifold_seed ^ shell_id.0);
96
97 for point_sample in 0..params.manifold_ray_sample_count {
98 let point = Vector3::from_array([
99 Scalar::from_f64(rng.next_range(lo[0] - margin[0], hi[0] + margin[0])),
100 Scalar::from_f64(rng.next_range(lo[1] - margin[1], hi[1] + margin[1])),
101 Scalar::from_f64(rng.next_range(lo[2] - margin[2], hi[2] + margin[2])),
102 ]);
103
104 let Ok(ray_length) = boundary.ray_length(&point) else {
105 continue;
106 };
107
108 let mut classifications = Vec::new();
109 for dir_sample in 0..params.manifold_ray_sample_count {
110 let direction = rng.next_direction3();
111 let seed = params.manifold_seed
112 ^ shell_id.0
113 ^ (point_sample as u64).wrapping_mul(0x9E3779B97F4A7C15)
114 ^ (dir_sample as u64).wrapping_mul(0x2545_F491_4F6C_DD1D);
115 match cast_ray(
116 &boundary,
117 point,
118 direction,
119 ray_length,
120 None,
121 params.max_nodes,
122 params.min_subdivision_size,
123 seed,
124 ) {
125 Ok(Ok(ray)) => classifications.push(ray.classification()),
126 Ok(Err(_)) => {}
129 Err(e) => errors.push(e.with_context(format!(
130 "shell {}, point {:?}, direction sample {}",
131 shell_id.0,
132 (point[0].to_f64(), point[1].to_f64(), point[2].to_f64()),
133 dir_sample
134 ))),
135 }
136 }
137
138 if classifications.len() >= 2 {
139 let inside_count = classifications
140 .iter()
141 .filter(|&&c| c == PointClassification::Inside)
142 .count();
143 let outside_count = classifications.len() - inside_count;
144 let minority = inside_count.min(outside_count);
145 if minority >= 2 {
149 errors.push(GeopError::new(format!(
150 "shell {}, point {:?}: ray directions disagree on inside/outside ({:?})",
151 shell_id.0,
152 (point[0].to_f64(), point[1].to_f64(), point[2].to_f64()),
153 classifications
154 )));
155 }
156 }
157 }
158 }
159}
160
161pub fn validate_manifold(
167 params: &ValidationParameters,
168 model: &Model,
169) -> Result<(), Vec<GeopError>> {
170 let mut errors = match validate(params, model) {
171 Ok(()) => Vec::new(),
172 Err(e) => e,
173 };
174
175 check_every_edge_has_paired_coedges(&mut errors, model);
176 check_ray_direction_consistency(params, &mut errors, model);
177
178 if errors.is_empty() {
179 Ok(())
180 } else {
181 Err(errors)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::validate_manifold;
188 use crate::test_fixtures::test_cube_solid;
189 use crate::{CoedgeGeometry, Model, validation::ValidationParameters};
190
191 #[test]
192 fn welded_cube_passes() {
193 let mut model = Model::new();
194 test_cube_solid(&mut model);
195 validate_manifold(&ValidationParameters::default(), &model).unwrap();
196 }
197
198 #[test]
199 fn missing_edge_coedge_fails() {
200 let mut model = Model::new();
201 test_cube_solid(&mut model);
202 let coedge_id = *model.coedges.keys().next().unwrap();
205 let orphan_edge = {
206 let template = model.edges[&model.coedges[&coedge_id].edge().unwrap()].clone();
207 model.insert_edge(template)
208 };
209 model.coedges.get_mut(&coedge_id).unwrap().geometry = CoedgeGeometry::Edge(orphan_edge);
210
211 let errors = validate_manifold(&ValidationParameters::default(), &model).unwrap_err();
212 assert!(
213 errors
214 .iter()
215 .any(|e| format!("{e:?}").contains("expected an even number"))
216 );
217 }
218}