1pub mod checks;
4pub(crate) mod edge;
5pub(crate) mod face;
6pub mod shell;
7pub(crate) mod solid;
8pub(crate) mod vertex;
9pub(crate) mod wire;
10
11pub use checks::{CheckId, EntityRef, Severity, ValidationIssue, ValidationReport};
12
13use std::collections::HashSet;
14
15use brepkit_topology::Topology;
16use brepkit_topology::shell::ShellId;
17use brepkit_topology::solid::SolidId;
18
19use crate::CheckError;
20
21#[derive(Debug, Clone)]
23pub struct ValidateOptions {
24 pub tolerance_scale: f64,
26 pub disabled_checks: HashSet<CheckId>,
28}
29
30impl Default for ValidateOptions {
31 fn default() -> Self {
32 Self {
33 tolerance_scale: 1.0,
34 disabled_checks: HashSet::new(),
35 }
36 }
37}
38
39pub fn validate_solid(
47 topo: &Topology,
48 solid_id: SolidId,
49 options: &ValidateOptions,
50) -> Result<ValidationReport, CheckError> {
51 let mut report = ValidationReport::default();
52 let solid_data = topo.solid(solid_id)?;
53
54 if !options
55 .disabled_checks
56 .contains(&CheckId::SolidEulerCharacteristic)
57 {
58 report.issues.extend(solid::check_euler(topo, solid_id)?);
59 }
60 if !options
61 .disabled_checks
62 .contains(&CheckId::SolidDuplicateFaces)
63 {
64 report
65 .issues
66 .extend(solid::check_duplicate_faces(topo, solid_id)?);
67 }
68
69 let shells: Vec<_> = std::iter::once(solid_data.outer_shell())
70 .chain(solid_data.inner_shells().iter().copied())
71 .collect();
72 for &sid in &shells {
73 report
74 .issues
75 .extend(validate_shell_checks(topo, sid, options)?);
76 }
77
78 Ok(report)
79}
80
81pub fn validate_shell(
89 topo: &Topology,
90 shell_id: ShellId,
91 options: &ValidateOptions,
92) -> Result<ValidationReport, CheckError> {
93 let mut report = ValidationReport::default();
94 report
95 .issues
96 .extend(validate_shell_checks(topo, shell_id, options)?);
97 Ok(report)
98}
99
100fn validate_shell_checks(
102 topo: &Topology,
103 shell_id: ShellId,
104 options: &ValidateOptions,
105) -> Result<Vec<ValidationIssue>, CheckError> {
106 let mut issues = Vec::new();
107
108 if !options.disabled_checks.contains(&CheckId::ShellEmpty) {
109 issues.extend(shell::check_shell_empty(topo, shell_id)?);
110 }
111 if !options.disabled_checks.contains(&CheckId::ShellConnected) {
112 issues.extend(shell::check_shell_connected(topo, shell_id)?);
113 }
114 if !options.disabled_checks.contains(&CheckId::ShellClosed) {
115 issues.extend(shell::check_shell_closed(topo, shell_id)?);
116 }
117 if !options
118 .disabled_checks
119 .contains(&CheckId::ShellOrientationConsistent)
120 {
121 issues.extend(shell::check_shell_orientation(topo, shell_id)?);
122 }
123
124 let shell = topo.shell(shell_id)?;
125 let mut checked_wires = HashSet::new();
126 for &fid in shell.faces() {
127 let face = topo.face(fid)?;
128 let mut wire_ids = vec![face.outer_wire()];
129 wire_ids.extend(face.inner_wires().iter().copied());
130 for wid in wire_ids {
131 if checked_wires.insert(wid) {
132 if !options.disabled_checks.contains(&CheckId::WireEmpty) {
133 issues.extend(wire::check_wire_empty(topo, wid)?);
134 }
135 if !options.disabled_checks.contains(&CheckId::WireNotConnected) {
136 issues.extend(wire::check_wire_connected(topo, wid)?);
137 }
138 if !options.disabled_checks.contains(&CheckId::WireClosure3D) {
139 issues.extend(wire::check_wire_closure(topo, wid)?);
140 }
141 if !options
142 .disabled_checks
143 .contains(&CheckId::WireRedundantEdge)
144 {
145 issues.extend(wire::check_wire_redundant(topo, wid)?);
146 }
147 if !options
148 .disabled_checks
149 .contains(&CheckId::WireSelfIntersection)
150 {
151 issues.extend(wire::check_wire_self_intersection(
152 topo,
153 wid,
154 options.tolerance_scale * 1e-6,
155 )?);
156 }
157 }
158 }
159 }
160
161 let mut checked_faces = HashSet::new();
162 for &fid in shell.faces() {
163 if checked_faces.insert(fid) {
164 if !options.disabled_checks.contains(&CheckId::FaceNoSurface) {
165 issues.extend(face::check_face_has_surface(topo, fid)?);
166 }
167 if !options
168 .disabled_checks
169 .contains(&CheckId::FaceOrientationConsistency)
170 {
171 issues.extend(face::check_face_orientation(topo, fid)?);
172 }
173 }
174 }
175
176 let mut checked_edges = HashSet::new();
177 for &fid in shell.faces() {
178 let face = topo.face(fid)?;
179 let mut wire_ids = vec![face.outer_wire()];
180 wire_ids.extend(face.inner_wires().iter().copied());
181 for wid in wire_ids {
182 let wire_data = topo.wire(wid)?;
183 for oe in wire_data.edges() {
184 let eid = oe.edge();
185 if checked_edges.insert(eid) {
186 if !options.disabled_checks.contains(&CheckId::EdgeRangeValid) {
187 issues.extend(edge::check_edge_range(
188 topo,
189 eid,
190 options.tolerance_scale * 1e-7,
191 )?);
192 }
193 if !options.disabled_checks.contains(&CheckId::EdgeDegenerate) {
194 issues.extend(edge::check_edge_degenerate(
195 topo,
196 eid,
197 options.tolerance_scale * 1e-7,
198 )?);
199 }
200 if !options.disabled_checks.contains(&CheckId::VertexOnCurve) {
201 let edge_data = topo.edge(eid)?;
202 issues.extend(vertex::check_vertex_on_curve(
203 topo,
204 edge_data.start(),
205 eid,
206 options.tolerance_scale * 1e-4,
207 )?);
208 if edge_data.start() != edge_data.end() {
209 issues.extend(vertex::check_vertex_on_curve(
210 topo,
211 edge_data.end(),
212 eid,
213 options.tolerance_scale * 1e-4,
214 )?);
215 }
216 }
217 if !options.disabled_checks.contains(&CheckId::VertexOnSurface) {
218 let edge_data = topo.edge(eid)?;
219 issues.extend(vertex::check_vertex_on_surface(
220 topo,
221 edge_data.start(),
222 fid,
223 options.tolerance_scale * 1e-4,
224 )?);
225 if edge_data.start() != edge_data.end() {
226 issues.extend(vertex::check_vertex_on_surface(
227 topo,
228 edge_data.end(),
229 fid,
230 options.tolerance_scale * 1e-4,
231 )?);
232 }
233 }
234 }
235 }
236 }
237 }
238
239 if !options
241 .disabled_checks
242 .contains(&CheckId::EdgeSameParameter)
243 {
244 let mut sp_checked = HashSet::new();
245 for &fid in shell.faces() {
246 let face = topo.face(fid)?;
247 let mut wire_ids = vec![face.outer_wire()];
248 wire_ids.extend(face.inner_wires().iter().copied());
249 for wid in wire_ids {
250 let wire_data = topo.wire(wid)?;
251 for oe in wire_data.edges() {
252 let eid = oe.edge();
253 if sp_checked.insert((eid, fid)) {
254 issues.extend(edge::check_edge_same_parameter(
255 topo,
256 eid,
257 fid,
258 options.tolerance_scale * 1e-4,
259 )?);
260 }
261 }
262 }
263 }
264 }
265
266 Ok(issues)
267}
268
269#[cfg(test)]
270mod tests {
271 #![allow(clippy::unwrap_used, clippy::expect_used)]
272
273 use brepkit_topology::Topology;
274 use brepkit_topology::test_utils::make_unit_cube_manifold;
275
276 use super::*;
277
278 #[test]
279 fn valid_box_no_issues() {
280 let mut topo = Topology::new();
281 let cube = make_unit_cube_manifold(&mut topo);
282 let opts = ValidateOptions::default();
283 let report = validate_solid(&topo, cube, &opts).unwrap();
284 assert!(
285 report.is_valid(),
286 "unit cube should have no errors, got: {:?}",
287 report.issues
288 );
289 assert_eq!(report.error_count(), 0);
290 }
291
292 #[test]
293 fn edge_range_valid_for_box() {
294 let mut topo = Topology::new();
295 let cube = make_unit_cube_manifold(&mut topo);
296 let opts = ValidateOptions::default();
297 let report = validate_solid(&topo, cube, &opts).unwrap();
298 let range_issues: Vec<_> = report
299 .issues
300 .iter()
301 .filter(|i| i.check == CheckId::EdgeRangeValid)
302 .collect();
303 assert!(
304 range_issues.is_empty(),
305 "box edges should have valid ranges, got: {range_issues:?}"
306 );
307 }
308
309 #[test]
310 fn valid_box_detailed() {
311 let mut topo = Topology::new();
312 let cube = make_unit_cube_manifold(&mut topo);
313 let opts = ValidateOptions::default();
314 let report = validate_solid(&topo, cube, &opts).unwrap();
315 assert_eq!(report.error_count(), 0, "errors: {:?}", report.issues);
316 assert_eq!(report.warning_count(), 0, "warnings: {:?}", report.issues);
317 }
318
319 #[test]
320 fn euler_characteristic_correct() {
321 let mut topo = Topology::new();
322 let cube = make_unit_cube_manifold(&mut topo);
323 let opts = ValidateOptions::default();
324 let report = validate_solid(&topo, cube, &opts).unwrap();
325 let euler_issues: Vec<_> = report
327 .issues
328 .iter()
329 .filter(|i| i.check == CheckId::SolidEulerCharacteristic)
330 .collect();
331 assert!(
332 euler_issues.is_empty(),
333 "cube Euler characteristic should be 2, got issues: {euler_issues:?}"
334 );
335 }
336}