1use super::*;
2
3impl BrepSolid {
4 pub fn validate(&self) -> Vec<ValidationIssue> {
5 self.validate_with_tolerances(&KernelTolerances::for_solid(self, 1e-7))
6 }
7
8 pub(crate) fn validate_memoised(&self) -> Vec<ValidationIssue> {
22 if !validate_memo_on() {
23 return self.validate();
24 }
25 let key_timing = crate::blend::BlendTimer::start("timing.validate.memo_key_ms");
26 let Some(key) = self.validate_memo_key() else {
27 return self.validate();
28 };
29 key_timing.stop();
30 if VALIDATED_CLEAN.with(|clean| clean.borrow().contains(&key)) {
31 crate::blend::record_count("count.validate.memo_hit", 1);
32 return Vec::new();
33 }
34 crate::blend::record_count("count.validate.memo_miss", 1);
35 let issues = self.validate();
36 if issues.is_empty() {
37 VALIDATED_CLEAN.with(|clean| {
38 let mut clean = clean.borrow_mut();
39 if clean.len() >= VALIDATED_CLEAN_CAP {
40 clean.clear();
41 }
42 clean.insert(key);
43 });
44 }
45 issues
46 }
47
48 pub fn validate_with_tolerances(&self, tolerances: &KernelTolerances) -> Vec<ValidationIssue> {
49 self.validate_detailed(tolerances).issues
50 }
51
52 pub fn validate_detailed(&self, tolerances: &KernelTolerances) -> ValidationReport {
53 self.validate_detailed_scoped(tolerances, None)
54 }
55
56 pub fn validate_scoped(&self, geometry_faces: &HashSet<u64>) -> Vec<ValidationIssue> {
65 self.validate_detailed_scoped(&KernelTolerances::for_solid(self, 1e-7), Some(geometry_faces))
66 .issues
67 }
68
69 pub fn validate_detailed_scoped(
73 &self,
74 tolerances: &KernelTolerances,
75 geometry_faces: Option<&HashSet<u64>>,
76 ) -> ValidationReport {
77 let mut issues = Vec::new();
78 let mut wire_warnings = Vec::new();
79 let mut max_pcurve_error = 0.0f64;
80 let mut bbox_lo = crate::Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
86 let mut bbox_hi = crate::Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
87 for vertex in &self.vertices {
88 bbox_lo.x = bbox_lo.x.min(vertex.point.x);
89 bbox_lo.y = bbox_lo.y.min(vertex.point.y);
90 bbox_lo.z = bbox_lo.z.min(vertex.point.z);
91 bbox_hi.x = bbox_hi.x.max(vertex.point.x);
92 bbox_hi.y = bbox_hi.y.max(vertex.point.y);
93 bbox_hi.z = bbox_hi.z.max(vertex.point.z);
94 }
95 let model_diagonal = if self.vertices.is_empty() {
96 0.0
97 } else {
98 bbox_hi.sub(bbox_lo).length()
99 };
100 let pcurve_limit = tolerances.pcurve_acceptance(model_diagonal);
101 let vertex_match = tolerances
129 .heal_band(model_diagonal, 2e-5)
130 .max(crate::VERTEX_MATCH_FLOOR);
131 let vertices: HashMap<u64, &VertexRecord> = self
132 .vertices
133 .iter()
134 .map(|vertex| (vertex.id, vertex))
135 .collect();
136 let edges: HashMap<u64, &EdgeRecord> =
137 self.edges.iter().map(|edge| (edge.id, edge)).collect();
138 if vertices.len() != self.vertices.len() {
139 issues.push(ValidationIssue::error("duplicate vertex id"));
140 }
141 if edges.len() != self.edges.len() {
142 issues.push(ValidationIssue::error("duplicate edge id"));
143 }
144
145 let mut edge_uses: HashMap<u64, Vec<bool>> = HashMap::default();
146 let mut face_ids = HashSet::default();
147 for shell in &self.shells {
148 for face in &shell.faces {
149 if !face_ids.insert(face.id) {
150 issues.push(ValidationIssue::error(format!(
151 "duplicate face id {}",
152 face.id
153 )));
154 }
155 let read_geometry =
156 geometry_faces.is_none_or(|scope| scope.contains(&face.id));
157 let surface = if read_geometry {
158 match NurbsSurface::new(
159 face.surface.degree_u,
160 face.surface.degree_v,
161 face.surface.knots_u.clone(),
162 face.surface.knots_v.clone(),
163 face.surface.control_points.clone(),
164 ) {
165 Ok(surface) => Some(surface),
166 Err(error) => {
167 issues.push(ValidationIssue::error(format!(
168 "face {} has invalid surface: {}",
169 face.id, error
170 )));
171 continue;
172 }
173 }
174 } else {
175 None
176 };
177 for (loop_index, loop_record) in face.loops.iter().enumerate() {
178 if loop_record.coedges.is_empty() {
179 issues.push(ValidationIssue::error(format!(
180 "loop {} of face {} is empty",
181 loop_record.id, face.id
182 )));
183 continue;
184 }
185 for coedge in &loop_record.coedges {
186 edge_uses
187 .entry(coedge.edge_id)
188 .or_default()
189 .push(coedge.forward);
190 }
191 for index in 0..loop_record.coedges.len() {
192 let current = &loop_record.coedges[index];
193 let next = &loop_record.coedges[(index + 1) % loop_record.coedges.len()];
194 let Some(current_edge) = edges.get(¤t.edge_id) else {
195 issues.push(ValidationIssue::error(format!(
196 "coedge {} references missing edge {}",
197 current.id, current.edge_id
198 )));
199 continue;
200 };
201 let Some(next_edge) = edges.get(&next.edge_id) else {
202 issues.push(ValidationIssue::error(format!(
203 "coedge {} references missing edge {}",
204 next.id, next.edge_id
205 )));
206 continue;
207 };
208 let current_end = if current.forward {
209 current_edge.end_vertex_id
210 } else {
211 current_edge.start_vertex_id
212 };
213 let next_start = if next.forward {
214 next_edge.start_vertex_id
215 } else {
216 next_edge.end_vertex_id
217 };
218 if current_end != next_start {
219 issues.push(ValidationIssue::error(format!(
220 "loop {} of face {} is open between coedges {} and {}",
221 loop_record.id, face.id, current.id, next.id
222 )));
223 }
224 let Some(surface) = &surface else {
225 continue;
226 };
227
228 let pcurve = match NurbsCurve::new(
229 current.pcurve.degree,
230 current.pcurve.knots.clone(),
231 current.pcurve.control_points.clone(),
232 ) {
233 Ok(curve) => curve,
234 Err(error) => {
235 issues.push(ValidationIssue::error(format!(
236 "coedge {} has invalid pcurve: {}",
237 current.id, error
238 )));
239 continue;
240 }
241 };
242 let curve = match NurbsCurve::new(
243 current_edge.curve.degree,
244 current_edge.curve.knots.clone(),
245 current_edge.curve.control_points.clone(),
246 ) {
247 Ok(curve) => curve,
248 Err(error) => {
249 issues.push(ValidationIssue::error(format!(
250 "edge {} has invalid curve: {}",
251 current_edge.id, error
252 )));
253 continue;
254 }
255 };
256 match memoized_coedge_error(
257 surface,
258 &pcurve,
259 &curve,
260 current_edge,
261 current.forward,
262 pcurve_limit,
263 ) {
264 Ok(error) => {
265 max_pcurve_error = max_pcurve_error.max(error);
266 if error > pcurve_limit {
267 issues.push(ValidationIssue::error(format!(
268 "coedge {} of face {} pcurve is inconsistent with edge {} \
269 (max deviation {:.6}, limit {:.6})",
270 current.id, face.id, current_edge.id, error, pcurve_limit,
271 )));
272 }
273 }
274 Err(error) => issues.push(ValidationIssue::error(format!(
275 "coedge {} of face {} cannot be evaluated: {}",
276 current.id, face.id, error
277 ))),
278 }
279 }
280
281 let Some(surface) = &surface else {
282 continue;
283 };
284 if let Some(warning) = validate_uv_wire(
285 surface,
286 loop_record,
287 &edges,
288 tolerances,
289 loop_index == 0,
290 face.same_sense,
291 ) {
292 wire_warnings.push(ValidationIssue::warning(format!(
293 "face {} loop {}: {}",
294 face.id, loop_record.id, warning
295 )));
296 }
297 }
298 if let Some(surface) = &surface {
299 if let Some(error) = validate_periodic_boundary_winding(surface, face, tolerances) {
300 issues.push(ValidationIssue::error(format!("face {}: {error}", face.id)));
301 }
302 }
303 }
304 }
305
306 for edge in &self.edges {
307 if !(edge.t0.is_finite() && edge.t1.is_finite() && edge.t0 < edge.t1) {
308 issues.push(ValidationIssue::error(format!(
309 "edge {} has invalid parameter range",
310 edge.id
311 )));
312 }
313 let Some(start) = vertices.get(&edge.start_vertex_id) else {
314 issues.push(ValidationIssue::error(format!(
315 "edge {} references missing start vertex {}",
316 edge.id, edge.start_vertex_id
317 )));
318 continue;
319 };
320 let Some(end) = vertices.get(&edge.end_vertex_id) else {
321 issues.push(ValidationIssue::error(format!(
322 "edge {} references missing end vertex {}",
323 edge.id, edge.end_vertex_id
324 )));
325 continue;
326 };
327 if let Ok(curve) = NurbsCurve::new(
328 edge.curve.degree,
329 edge.curve.knots.clone(),
330 edge.curve.control_points.clone(),
331 ) {
332 if let Ok(point) = curve.evaluate(edge.t0) {
333 let gap = point.sub(start.point).length();
334 if gap > vertex_match {
335 issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: true }, format!(
336 "edge {} curve start does not match vertex {} \
337 (gap={gap:.9}, curve={point:?}, vertex={:?})",
338 edge.id, start.id, start.point
339 )));
340 }
341 }
342 if let Ok(point) = curve.evaluate(edge.t1) {
343 let gap = point.sub(end.point).length();
344 if gap > vertex_match {
345 issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: false }, format!(
346 "edge {} curve end does not match vertex {} \
347 (gap={gap:.9}, curve={point:?}, vertex={:?})",
348 edge.id, end.id, end.point
349 )));
350 }
351 }
352 }
353 let uses = edge_uses.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
354 if edge.degenerate {
355 let valid = uses.len() == 1 || (uses.len() == 2 && uses[0] != uses[1]);
361 if !valid {
362 issues.push(ValidationIssue::error(format!(
363 "degenerate edge {} has invalid incidence {:?} \
364 (expected one use or an opposite-sense pair)",
365 edge.id, uses,
366 )));
367 }
368 } else if self.kind == BodyKind::Sheet && uses.len() == 1 {
372 } else if self.kind == BodyKind::Cellular {
373 if uses.len() < 2 {
376 issues.push(ValidationIssue::error_kind(IssueKind::OpenEdge,
377 format!("Cellular edge {} used {} times (expected at least 2)", edge.id, uses.len())));
378 }
379 } else if uses.len() != 2 {
380 let kind = if uses.len() < 2 { IssueKind::OpenEdge } else { IssueKind::OverUsedEdge };
381 issues.push(ValidationIssue::error_kind(kind, format!(
382 "edge {} used {} times (expected 2)",
383 edge.id,
384 uses.len()
385 )));
386 } else if uses[0] == uses[1] {
387 issues.push(ValidationIssue::error(format!(
388 "edge {} has coedges with the same sense",
389 edge.id
390 )));
391 }
392 }
393
394 for edge_id in edge_uses.keys() {
395 if !edges.contains_key(edge_id) {
396 issues.push(ValidationIssue::error(format!(
397 "topology references missing edge {}",
398 edge_id
399 )));
400 }
401 }
402
403 let non_degenerate_vertex_ids = self
407 .edges
408 .iter()
409 .filter(|edge| !edge.degenerate)
410 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
411 .collect::<HashSet<_>>();
412 let vertex_count = vertices
413 .keys()
414 .filter(|id| non_degenerate_vertex_ids.contains(id))
415 .count() as i64;
416 let edge_count = self.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
417 let face_count = face_ids.len() as i64;
418 let hole_count = self.bounding_hole_count();
419 let shell_count = self.shells.len() as i64;
420 let actual = vertex_count - edge_count + face_count - hole_count;
421 let expected = 2 * (shell_count - self.genus);
422 let closed = self
473 .edges
474 .iter()
475 .filter(|edge| !edge.degenerate)
476 .all(|edge| edge_uses.get(&edge.id).map(Vec::len) == Some(2));
477 if !matches!(self.kind, BodyKind::Sheet | BodyKind::Cellular) && closed && actual != expected {
480 let pairs = self.coincident_parallel_edge_pairs() as i64;
489 if actual + pairs != expected {
490 issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch, format!(
491 "Euler formula: V-E+F-H = {}, expected {} ({} coincident parallel edge pair(s) credited)",
492 actual + pairs,
493 expected,
494 pairs
495 )));
496 }
497 }
498 if self.kind == BodyKind::Cellular {
499 self.validate_cells(&edges, &mut issues);
500 }
501 ValidationReport {
502 issues,
503 wire_warnings,
504 max_pcurve_error,
505 }
506 }
507
508 fn validate_cells(&self, edges: &HashMap<u64, &EdgeRecord>, issues: &mut Vec<ValidationIssue>) {
511 if self.cells.is_empty() {
512 issues.push(ValidationIssue::error("Cellular body has no declared cells"));
513 return;
514 }
515 let faces: HashMap<_, _> = self.shells.iter().flat_map(|s| &s.faces).map(|f| (f.id, f)).collect();
516 let mut cell_ids = HashSet::default();
517 let mut owners: HashMap<u64, Vec<bool>> = HashMap::default();
518 let mut fans: HashMap<u64, Vec<(&FaceRecord, bool, &CoedgeRecord)>> = HashMap::default();
520 for cell in &self.cells {
521 if !cell_ids.insert(cell.id) {
522 issues.push(ValidationIssue::error(format!("duplicate cell id {}", cell.id)));
523 }
524 if cell.faces.is_empty() {
525 issues.push(ValidationIssue::error(format!("cell {} has no faces", cell.id)));
526 }
527 let mut seen = HashSet::default();
528 let mut uses: HashMap<u64, Vec<bool>> = HashMap::default();
529 let mut holes = 0i64;
530 for &(id, forward) in &cell.faces {
531 if !seen.insert(id) {
532 issues.push(ValidationIssue::error(format!("cell {} repeats face {id}", cell.id)));
533 continue;
534 }
535 let Some(&face) = faces.get(&id) else {
536 issues.push(ValidationIssue::error(format!("cell {} references missing face {id}", cell.id)));
537 continue;
538 };
539 owners.entry(id).or_default().push(forward);
540 holes += face.loops.iter().filter(|record| !record.coedges.is_empty()
541 && record.coedges.iter().any(|c| edges.get(&c.edge_id).is_some_and(|e| !e.degenerate)))
542 .count().saturating_sub(1) as i64;
543 for coedge in face.loops.iter().flat_map(|l| &l.coedges) {
544 uses.entry(coedge.edge_id).or_default().push(coedge.forward == forward);
545 fans.entry(coedge.edge_id).or_default().push((face, forward, coedge));
546 }
547 }
548 let mut vertices = HashSet::default();
549 let mut edge_count = 0i64;
550 for (id, senses) in &uses {
551 let Some(&edge) = edges.get(id) else { continue; };
552 let opposite = senses.len() == 2 && senses[0] != senses[1];
553 if !(opposite || (edge.degenerate && senses.len() == 1)) {
554 issues.push(ValidationIssue::error(format!(
555 "cell {} edge {id} is not closed with opposite senses: {senses:?}", cell.id)));
556 }
557 if !edge.degenerate {
558 edge_count += 1;
559 vertices.extend([edge.start_vertex_id, edge.end_vertex_id]);
560 }
561 }
562 let chi = vertices.len() as i64 - edge_count + seen.len() as i64 - holes;
563 if chi % 2 != 0 {
564 issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch,
565 format!("cell {} has odd Euler characteristic {chi}", cell.id)));
566 }
567 }
568 for id in faces.keys() {
569 let senses = owners.get(id).map(Vec::as_slice).unwrap_or(&[]);
570 if senses.is_empty() || senses.len() > 2 || (senses.len() == 2 && senses[0] == senses[1]) {
571 issues.push(ValidationIssue::error(format!(
572 "face {id} has invalid cell ownership senses {senses:?}")));
573 }
574 }
575 for (id, uses) in fans {
576 let Some(&edge) = edges.get(&id) else { continue; };
577 if edge.degenerate || uses.len() <= 2 { continue; }
578 for fraction in [0.25, 0.5, 0.75] {
579 let reading = (|| -> Result<Vec<(f64, bool)>, String> {
580 let t = edge.t0 + (edge.t1-edge.t0)*fraction;
581 let derivatives = edge.curve.derivatives(t, 1)?;
582 let tangent = derivatives[1].scale(if edge.t1 >= edge.t0 { 1.0 } else { -1.0 }).normalized()?;
583 let axis = tangent.perpendicular()?;
584 let other = tangent.cross(axis);
585 let mut radial = Vec::new();
586 for &(face, forward, coedge) in &uses {
587 let uv = crate::project_point_to_surface(&face.surface, derivatives[0])?;
588 let normal = face.surface.normal(uv.u, uv.v)?
589 .scale(if face.same_sense { 1.0 } else { -1.0 });
590 let ray = normal.cross(tangent.scale(if coedge.forward { 1.0 } else { -1.0 }));
595 let (x,y) = (ray.dot(axis), ray.dot(other));
596 if !x.is_finite() || !y.is_finite() || x*x+y*y < 1e-20 {
597 return Err("undefined radial face tangent".into());
598 }
599 radial.push((y.atan2(x), coedge.forward == forward));
600 }
601 radial.sort_by(|a,b| a.0.total_cmp(&b.0).then_with(|| b.1.cmp(&a.1)));
604 Ok(radial)
605 })();
606 match reading {
607 Ok(radial) if radial.iter().enumerate().all(|(i, a)| a.1 != radial[(i+1)%radial.len()].1) => {},
608 Ok(_) => { issues.push(ValidationIssue::error(format!(
609 "Cellular edge {id} radial cell senses do not alternate at {fraction}"))); break; },
610 Err(error) => { issues.push(ValidationIssue::error(format!(
611 "Cellular edge {id} radial cell fan cannot be read: {error}"))); break; },
612 }
613 }
614 }
615 }
616
617 pub(crate) fn bounding_hole_count(&self) -> i64 {
631 let degenerate: HashSet<u64> = self
632 .edges
633 .iter()
634 .filter(|edge| edge.degenerate)
635 .map(|edge| edge.id)
636 .collect();
637 self.shells
638 .iter()
639 .flat_map(|shell| &shell.faces)
640 .map(|face| {
641 face.loops
642 .iter()
643 .filter(|record| {
644 !record.coedges.is_empty()
645 && !record
646 .coedges
647 .iter()
648 .all(|coedge| degenerate.contains(&coedge.edge_id))
649 })
650 .count()
651 .saturating_sub(1) as i64
652 })
653 .sum()
654 }
655
656 pub(crate) fn coincident_parallel_edge_pairs(&self) -> usize {
661 let candidates: Vec<&EdgeRecord> =
662 self.edges.iter().filter(|edge| !edge.degenerate).collect();
663 let mut consumed = vec![false; candidates.len()];
664 let mut pairs = 0usize;
665 for first_index in 0..candidates.len() {
666 if consumed[first_index] {
667 continue;
668 }
669 let first = candidates[first_index];
670 for second_index in first_index + 1..candidates.len() {
671 if consumed[second_index] {
672 continue;
673 }
674 let second = candidates[second_index];
675 let endpoints_match = (first.start_vertex_id == second.start_vertex_id
676 && first.end_vertex_id == second.end_vertex_id)
677 || (first.start_vertex_id == second.end_vertex_id
678 && first.end_vertex_id == second.start_vertex_id);
679 if !endpoints_match {
680 continue;
681 }
682 let coincident = (1..4).all(|sample| {
684 let fraction = sample as f64 / 4.0;
685 let Ok(on_first) = first
686 .curve
687 .evaluate(first.t0 + (first.t1 - first.t0) * fraction)
688 else {
689 return false;
690 };
691 let forward = second.t0 + (second.t1 - second.t0) * fraction;
692 let backward = second.t1 - (second.t1 - second.t0) * fraction;
693 let tolerance = 1e-6 * (1.0 + on_first.length());
694 let matches_forward = second
695 .curve
696 .evaluate(forward)
697 .map(|point| point.sub(on_first).length() <= tolerance)
698 .unwrap_or(false);
699 let matches_backward = second
700 .curve
701 .evaluate(backward)
702 .map(|point| point.sub(on_first).length() <= tolerance)
703 .unwrap_or(false);
704 matches_forward || matches_backward
705 });
706 if coincident {
707 consumed[first_index] = true;
708 consumed[second_index] = true;
709 pairs += 1;
710 break;
711 }
712 }
713 }
714 pairs
715 }
716}
717
718fn coedge_sample(
719 surface: &NurbsSurface,
720 pcurve: &NurbsCurve,
721 curve: &NurbsCurve,
722 edge: &EdgeRecord,
723 forward: bool,
724 fraction: f64,
725) -> Result<(f64, Vec3, Vec3), String> {
726 let [q0, q1] = pcurve.domain()?;
727 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
728 let on_surface = surface.evaluate_extended(uv.x, uv.y)?;
733 let t = if forward {
734 edge.t0 + (edge.t1 - edge.t0) * fraction
735 } else {
736 edge.t1 - (edge.t1 - edge.t0) * fraction
737 };
738 let on_curve = curve.evaluate(t)?;
739 Ok((on_surface.sub(on_curve).length(), on_surface, on_curve))
740}
741
742pub(crate) fn adaptive_coedge_error(
747 surface: &NurbsSurface,
748 pcurve: &NurbsCurve,
749 curve: &NurbsCurve,
750 edge: &EdgeRecord,
751 forward: bool,
752 tolerance: f64,
753) -> Result<f64, String> {
754 fn interval(
755 surface: &NurbsSurface,
756 pcurve: &NurbsCurve,
757 curve: &NurbsCurve,
758 edge: &EdgeRecord,
759 forward: bool,
760 a: f64,
761 b: f64,
762 sample_a: (f64, Vec3, Vec3),
763 sample_b: (f64, Vec3, Vec3),
764 tolerance: f64,
765 depth: usize,
766 ) -> Result<f64, String> {
767 let mid = (a + b) * 0.5;
768 let sample_mid = coedge_sample(surface, pcurve, curve, edge, forward, mid)?;
769 let error_nonlinearity = (sample_mid.0 - (sample_a.0 + sample_b.0) * 0.5).abs();
770 let surface_bend = sample_mid
771 .1
772 .sub(sample_a.1.add(sample_b.1).scale(0.5))
773 .length();
774 let curve_bend = sample_mid
775 .2
776 .sub(sample_a.2.add(sample_b.2).scale(0.5))
777 .length();
778 let local_max = sample_a.0.max(sample_mid.0).max(sample_b.0);
779 if depth >= 3
780 || (error_nonlinearity <= tolerance * 0.05
781 && surface_bend.max(curve_bend) <= tolerance * 0.25)
782 {
783 return Ok(local_max);
784 }
785 Ok(interval(
786 surface,
787 pcurve,
788 curve,
789 edge,
790 forward,
791 a,
792 mid,
793 sample_a,
794 sample_mid,
795 tolerance,
796 depth + 1,
797 )?
798 .max(interval(
799 surface,
800 pcurve,
801 curve,
802 edge,
803 forward,
804 mid,
805 b,
806 sample_mid,
807 sample_b,
808 tolerance,
809 depth + 1,
810 )?))
811 }
812
813 let mut maximum = 0.0f64;
814 let mut previous = coedge_sample(surface, pcurve, curve, edge, forward, 0.0)?;
815 maximum = maximum.max(previous.0);
816 for index in 1..=32 {
817 let a = (index - 1) as f64 / 32.0;
818 let b = index as f64 / 32.0;
819 let next = coedge_sample(surface, pcurve, curve, edge, forward, b)?;
820 maximum = maximum.max(interval(
821 surface, pcurve, curve, edge, forward, a, b, previous, next, tolerance, 0,
822 )?);
823 previous = next;
824 }
825 Ok(maximum)
832}
833
834thread_local! {
835 static COEDGE_MEMO: std::cell::RefCell<Option<HashMap<Vec<u64>, Result<f64, String>>>> =
836 const { std::cell::RefCell::new(None) };
837 static COEDGE_MEMO_COUNTS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
839}
840
841pub(crate) fn coedge_memo_enabled() -> bool {
851 let enabled = !matches!(std::env::var("BREP_VALIDATE_COEDGE_MEMO").as_deref(), Ok("0") | Ok("off"));
852 enabled
853}
854
855
856pub(crate) struct CoedgeMemoScope {
858 opened: bool,
859}
860
861impl CoedgeMemoScope {
862 pub(crate) fn open(enabled: bool) -> Self {
863 let opened = enabled
864 && COEDGE_MEMO.with(|memo| {
865 let mut memo = memo.borrow_mut();
866 if memo.is_some() {
867 return false;
868 }
869 *memo = Some(HashMap::default());
870 COEDGE_MEMO_COUNTS.with(|counts| counts.set((0, 0)));
871 true
872 });
873 CoedgeMemoScope { opened }
874 }
875}
876
877impl Drop for CoedgeMemoScope {
878 fn drop(&mut self) {
879 if self.opened {
880 COEDGE_MEMO.with(|memo| *memo.borrow_mut() = None);
881 let (hits, misses) = COEDGE_MEMO_COUNTS.with(|counts| counts.get());
882 if std::env::var_os("BREP_PROFILE").is_some() {
883 eprintln!("validate.coedge_memo hits={hits} misses={misses}");
884 }
885 }
886 }
887}
888
889fn coedge_memo_key(
890 surface: &NurbsSurface,
891 pcurve: &NurbsCurve,
892 curve: &NurbsCurve,
893 edge: &EdgeRecord,
894 forward: bool,
895 tolerance: f64,
896) -> Vec<u64> {
897 let mut key = Vec::with_capacity(
898 16 + surface.knots_u.len() + surface.knots_v.len() + 4 * surface.control_points.iter().map(Vec::len).sum::<usize>()
899 + pcurve.knots.len() + 4 * pcurve.control_points.len() + curve.knots.len() + 4 * curve.control_points.len(),
900 );
901 let push_point = |key: &mut Vec<u64>, point: &crate::Vec4| key.extend([point.x.to_bits(), point.y.to_bits(), point.z.to_bits(), point.w.to_bits()]);
902 key.extend([edge.t0.to_bits(), edge.t1.to_bits(), forward as u64, tolerance.to_bits()]);
903 key.extend([surface.degree_u as u64, surface.degree_v as u64, surface.knots_u.len() as u64, surface.knots_v.len() as u64, surface.control_points.len() as u64]);
904 key.extend(surface.knots_u.iter().chain(&surface.knots_v).map(|knot| knot.to_bits()));
905 for row in &surface.control_points {
906 key.push(row.len() as u64);
907 for point in row {
908 push_point(&mut key, point);
909 }
910 }
911 for nurbs in [pcurve, curve] {
912 key.extend([nurbs.degree as u64, nurbs.knots.len() as u64, nurbs.control_points.len() as u64]);
913 key.extend(nurbs.knots.iter().map(|knot| knot.to_bits()));
914 for point in &nurbs.control_points {
915 push_point(&mut key, point);
916 }
917 }
918 key
919}
920
921pub(crate) fn exact_lines_enabled() -> bool {
926 let enabled = !matches!(std::env::var("BREP_VALIDATE_EXACT_LINES").as_deref(), Ok("0") | Ok("off"));
927 enabled
928}
929
930
931pub(crate) fn exact_line_on_plane_error(
945 surface: &NurbsSurface,
946 pcurve: &NurbsCurve,
947 curve: &NurbsCurve,
948 edge: &EdgeRecord,
949 forward: bool,
950) -> Option<f64> {
951 let linear = |nurbs: &NurbsCurve| {
952 nurbs.degree == 1
953 && nurbs.control_points.len() == 2
954 && nurbs.knots.len() == 4
955 && nurbs.control_points[0].w == nurbs.control_points[1].w
956 && nurbs.control_points[0].w > 0.0
957 };
958 if !linear(pcurve) || !linear(curve) {
959 return None;
960 }
961 if surface.degree_u != 1
962 || surface.degree_v != 1
963 || surface.knots_u.len() != 4
964 || surface.knots_v.len() != 4
965 || surface.control_points.len() != 2
966 || surface.control_points.iter().any(|row| row.len() != 2)
967 {
968 return None;
969 }
970 let weight = surface.control_points[0][0].w;
971 if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
972 return None;
973 }
974 let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
975 let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
976 let defect = p11.sub(p10).sub(p01.sub(p00));
977 if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
978 return None;
979 }
980 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
981 let [q0, q1] = pcurve.domain().ok()?;
982 for q in [q0, q1] {
983 let uv = pcurve.evaluate(q).ok()?;
984 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
985 return None;
986 }
987 }
988 let [c0, c1] = curve.domain().ok()?;
989 let (low, high) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
990 if !(low >= c0 && high <= c1) {
991 return None;
992 }
993 let start = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
994 let end = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
995 Some(start.max(end))
996}
997
998pub(crate) fn exact_iso_enabled() -> bool {
1002 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1003 *ON.get_or_init(|| !matches!(std::env::var("BREP_VALIDATE_EXACT_ISO").as_deref(), Ok("0") | Ok("off")))
1004}
1005
1006
1007pub(crate) fn exact_iso_line_error(
1025 surface: &NurbsSurface,
1026 pcurve: &NurbsCurve,
1027 curve: &NurbsCurve,
1028 edge: &EdgeRecord,
1029 forward: bool,
1030) -> Option<f64> {
1031 if pcurve.degree != 1
1032 || pcurve.control_points.len() != 2
1033 || pcurve.knots.len() != 4
1034 || pcurve.control_points[0].w != pcurve.control_points[1].w
1035 || !(pcurve.control_points[0].w > 0.0)
1036 {
1037 return None;
1038 }
1039 let [q0, q1] = pcurve.domain().ok()?;
1040 let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
1041 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1042 for uv in [a, b] {
1043 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
1044 return None;
1045 }
1046 }
1047 let (iso, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x {
1049 (surface.iso_curve_v(a.y).ok()?, a.x, b.x)
1050 } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y {
1051 (surface.iso_curve_u(a.x).ok()?, a.y, b.y)
1052 } else {
1053 return None;
1054 };
1055 let (start, end) = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
1057 if start.to_bits() != from.to_bits() || end.to_bits() != to.to_bits() {
1058 return None;
1059 }
1060 if iso.degree != curve.degree
1061 || iso.knots.len() != curve.knots.len()
1062 || iso.knots.iter().zip(&curve.knots).any(|(x, y)| x.to_bits() != y.to_bits())
1063 || iso.control_points.len() != curve.control_points.len()
1064 {
1065 return None;
1066 }
1067 let size = iso
1068 .control_points
1069 .iter()
1070 .filter_map(|c| c.point().ok())
1071 .fold(0f64, |m, q| m.max(q.x.abs()).max(q.y.abs()).max(q.z.abs()));
1072 let mut deviation = 0f64;
1073 for (p, q) in iso.control_points.iter().zip(&curve.control_points) {
1074 if p.w.to_bits() != q.w.to_bits() || !(p.w > 0.0) {
1075 return None;
1076 }
1077 deviation = deviation.max(p.point().ok()?.sub(q.point().ok()?).length());
1078 }
1079 if !(deviation <= 1e-12 * size.max(1.0)) {
1080 return None;
1081 }
1082 let start_gap = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
1083 let end_gap = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
1084 Some(start_gap.max(end_gap).max(deviation))
1085}
1086
1087fn memoized_coedge_error(
1089 surface: &NurbsSurface,
1090 pcurve: &NurbsCurve,
1091 curve: &NurbsCurve,
1092 edge: &EdgeRecord,
1093 forward: bool,
1094 tolerance: f64,
1095) -> Result<f64, String> {
1096 if exact_lines_enabled() {
1097 if let Some(error) = exact_line_on_plane_error(surface, pcurve, curve, edge, forward) {
1098 return Ok(error);
1099 }
1100 }
1101 if exact_iso_enabled() {
1102 if let Some(error) = exact_iso_line_error(surface, pcurve, curve, edge, forward) {
1103 crate::blend::record_count("count.validate.exact_iso", 1);
1104 return Ok(error);
1105 }
1106 }
1107 if COEDGE_MEMO.with(|memo| memo.borrow().is_none()) {
1108 return coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1109 }
1110 let key = coedge_memo_key(surface, pcurve, curve, edge, forward, tolerance);
1111 if let Some(hit) = COEDGE_MEMO.with(|memo| memo.borrow().as_ref().and_then(|map| map.get(&key).cloned())) {
1112 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
1113 return hit;
1114 }
1115 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0, counts.get().1 + 1)));
1116 let read = coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1117 COEDGE_MEMO.with(|memo| {
1118 if let Some(map) = memo.borrow_mut().as_mut() {
1119 map.insert(key, read.clone());
1120 }
1121 });
1122 read
1123}
1124
1125pub(crate) fn coedge_error_floored(
1135 surface: &NurbsSurface,
1136 pcurve: &NurbsCurve,
1137 curve: &NurbsCurve,
1138 edge: &EdgeRecord,
1139 forward: bool,
1140 tolerance: f64,
1141) -> Result<f64, String> {
1142 let steered = adaptive_coedge_error(surface, pcurve, curve, edge, forward, tolerance)?;
1143 Ok(steered.max(knot_span_midpoint_error(surface, pcurve, curve, edge, forward)?))
1144}
1145
1146fn coedge_floor_tracing() -> bool {
1150 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1151 *ON.get_or_init(|| std::env::var_os("BREP_DEBUG_COEDGE_FLOOR").is_some())
1152}
1153
1154fn knot_span_midpoint_error(
1161 surface: &NurbsSurface,
1162 pcurve: &NurbsCurve,
1163 curve: &NurbsCurve,
1164 edge: &EdgeRecord,
1165 forward: bool,
1166) -> Result<f64, String> {
1167 let mut worst = 0.0_f64;
1168 let [q0, q1] = pcurve.domain()?;
1169 if q1 > q0 {
1170 let mut previous = q0;
1171 for &knot in pcurve.knots.iter().chain(std::iter::once(&q1)) {
1172 if knot > previous && knot <= q1 {
1173 let fraction = ((previous + knot) * 0.5 - q0) / (q1 - q0);
1174 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1175 if coedge_floor_tracing() && sample.0 > 1e-3 {
1176 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1177 let ([u0, u1], [v0, v1]) = (surface.domain_u()?, surface.domain_v()?);
1178 eprintln!(
1179 "COEDGE-FLOOR edge {} pcurve-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6}) domain u[{u0},{u1}] v[{v0},{v1}] closed {:?} analytic {:?} degree {} knots {} cps {}",
1180 edge.id, sample.0, uv.x, uv.y, surface.closed_directions()?, surface.analytic().map(|_| "yes"), pcurve.degree, pcurve.knots.len(), pcurve.control_points.len()
1181 );
1182 }
1183 worst = worst.max(sample.0);
1184 previous = knot;
1185 }
1186 }
1187 }
1188 let span = edge.t1 - edge.t0;
1189 if span.is_finite() && span != 0.0 {
1190 let low = edge.t0.min(edge.t1);
1191 let high = edge.t0.max(edge.t1);
1192 let mut previous = low;
1193 for &knot in curve.knots.iter().chain(std::iter::once(&high)) {
1194 if knot > previous && knot <= high {
1195 let midpoint = (previous + knot) * 0.5;
1196 let fraction = if forward {
1197 (midpoint - edge.t0) / span
1198 } else {
1199 (edge.t1 - midpoint) / span
1200 };
1201 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1202 if coedge_floor_tracing() && sample.0 > 1e-3 {
1203 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1204 eprintln!(
1205 "COEDGE-FLOOR edge {} edge-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6})",
1206 edge.id, sample.0, uv.x, uv.y
1207 );
1208 }
1209 worst = worst.max(sample.0);
1210 previous = knot;
1211 }
1212 }
1213 }
1214 Ok(worst)
1215}
1216
1217fn segments_cross(a: Vec2, b: Vec2, c: Vec2, d: Vec2, tolerance: f64) -> bool {
1218 let cross = |first: Vec2, second: Vec2| first.x * second.y - first.y * second.x;
1219 let ab = b.sub(a);
1220 let cd = d.sub(c);
1221 let denominator = cross(ab, cd);
1222 if denominator.abs() <= tolerance {
1223 return false;
1224 }
1225 let ac = c.sub(a);
1226 let t = cross(ac, cd) / denominator;
1227 let u = cross(ac, ab) / denominator;
1228 t > tolerance && t < 1.0 - tolerance && u > tolerance && u < 1.0 - tolerance
1229}
1230
1231fn validate_periodic_boundary_winding(
1238 surface: &NurbsSurface,
1239 face: &FaceRecord,
1240 tolerances: &KernelTolerances,
1241) -> Option<String> {
1242 let Some(crate::AnalyticSurface::RuledRevolution { rho0, rho1, .. }) = surface.analytic() else {
1243 return None;
1244 };
1245 if rho0 * rho1 <= 0.0 || rho0.abs().min(rho1.abs()) <= tolerances.model {
1248 return None;
1249 }
1250 let [u0, u1] = surface.domain_u().ok()?;
1251 let period = u1 - u0;
1252 if period <= 0.0 { return None; }
1253 let mut displacement = 0.0;
1254 for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
1255 let [t0, t1] = coedge.pcurve.domain().ok()?;
1256 let start = coedge.pcurve.evaluate(t0).ok()?;
1257 let end = coedge.pcurve.evaluate(t1).ok()?;
1258 displacement += end.x - start.x;
1259 }
1260 let winding = displacement / period;
1261 (winding.abs() > 0.5).then(|| format!(
1265 "nonzero periodic boundary winding ({winding:.6} turns) on a nonsingular revolution wall"
1266 ))
1267}
1268
1269fn validate_uv_wire(
1270 surface: &NurbsSurface,
1271 loop_record: &LoopRecord,
1272 edges: &HashMap<u64, &EdgeRecord>,
1273 tolerances: &KernelTolerances,
1274 outer: bool,
1275 same_sense: bool,
1276) -> Option<String> {
1277 let u_domain = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)
1278 .ok()?
1279 .domain();
1280 let v_domain = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)
1281 .ok()?
1282 .domain();
1283 let u_span = u_domain[1] - u_domain[0];
1284 let v_span = v_domain[1] - v_domain[0];
1285 let mut points = Vec::<Vec2>::new();
1286 for coedge in &loop_record.coedges {
1287 let edge = edges.get(&coedge.edge_id)?;
1288 if edge.degenerate {
1289 continue;
1290 }
1291 let [q0, q1] = coedge.pcurve.domain().ok()?;
1292 for index in 0..=8 {
1293 if !points.is_empty() && index == 0 {
1294 continue;
1295 }
1296 let fraction = index as f64 / 8.0;
1297 let parameter = q0 + (q1 - q0) * fraction;
1298 let value = coedge.pcurve.evaluate(parameter).ok()?;
1299 let mut point = Vec2 {
1300 x: value.x,
1301 y: value.y,
1302 };
1303 if let Some(previous) = points.last() {
1304 while point.x - previous.x > u_span * 0.5 {
1305 point.x -= u_span;
1306 }
1307 while point.x - previous.x < -u_span * 0.5 {
1308 point.x += u_span;
1309 }
1310 while point.y - previous.y > v_span * 0.5 {
1311 point.y -= v_span;
1312 }
1313 while point.y - previous.y < -v_span * 0.5 {
1314 point.y += v_span;
1315 }
1316 }
1317 points.push(point);
1318 }
1319 }
1320 if points.len() < 4 {
1321 return None;
1322 }
1323 let first = points[0];
1324 let last = *points.last()?;
1325 let uv_tolerance = tolerances.model.max(1e-8);
1326 if last.sub(first).length() > uv_tolerance * 100.0 {
1327 let du = (last.x - first.x) / u_span;
1330 let dv = (last.y - first.y) / v_span;
1331 if (du - du.round()).abs() * u_span > uv_tolerance * 100.0
1332 || (dv - dv.round()).abs() * v_span > uv_tolerance * 100.0
1333 {
1334 return Some(format!(
1335 "wire is open in parameter space (gap {:.3e})",
1336 last.sub(first).length()
1337 ));
1338 }
1339 }
1340 for first_index in 0..points.len() - 1 {
1341 for second_index in first_index + 2..points.len() - 1 {
1342 if first_index == 0 && second_index + 1 == points.len() - 1 {
1343 continue;
1344 }
1345 if segments_cross(
1346 points[first_index],
1347 points[first_index + 1],
1348 points[second_index],
1349 points[second_index + 1],
1350 1e-10,
1351 ) {
1352 return Some(format!(
1353 "wire self-intersects near sampled segments {first_index} and {second_index}"
1354 ));
1355 }
1356 }
1357 }
1358 let area = points
1359 .windows(2)
1360 .map(|pair| pair[0].x * pair[1].y - pair[1].x * pair[0].y)
1361 .sum::<f64>()
1362 * 0.5;
1363 if area.abs() > uv_tolerance * uv_tolerance {
1364 let expected_positive = if outer { same_sense } else { !same_sense };
1365 if (area > 0.0) != expected_positive {
1366 return Some(format!(
1367 "{} wire winding disagrees with face sense (signed UV area {:.6})",
1368 if outer { "outer" } else { "inner" },
1369 area
1370 ));
1371 }
1372 }
1373 None
1374}
1375
1376const VALIDATED_CLEAN_CAP: usize = 4096;
1378
1379thread_local! {
1380 static VALIDATED_CLEAN: std::cell::RefCell<rustc_hash::FxHashSet<(u64, u64)>> =
1381 std::cell::RefCell::new(rustc_hash::FxHashSet::default());
1382}
1383
1384impl BrepSolid {
1385 pub(crate) fn clear_validate_memo() {
1389 VALIDATED_CLEAN.with(|clean| clean.borrow_mut().clear());
1390 }
1391
1392 pub(crate) fn validate_memo_key(&self) -> Option<(u64, u64)> {
1399 use crate::mass_properties::TwoLane;
1400 fn curve(lane: &mut TwoLane, curve: &crate::NurbsCurve) {
1401 lane.usize(curve.degree);
1402 lane.floats(&curve.knots);
1403 lane.usize(curve.control_points.len());
1404 for point in &curve.control_points {
1405 lane.floats(&[point.x, point.y, point.z, point.w]);
1406 }
1407 }
1408 fn name(lane: &mut TwoLane, name: &Option<String>) {
1409 lane.bool(name.is_some());
1410 if let Some(name) = name {
1411 lane.usize(name.len());
1412 for byte in name.bytes() {
1413 lane.u64(u64::from(byte));
1414 }
1415 }
1416 }
1417 let mut lane = TwoLane::new();
1418 lane.u64(self.id);
1419 lane.u64(self.genus as u64);
1420 lane.usize(self.kind as usize);
1421 lane.usize(self.vertices.len());
1422 for vertex in &self.vertices {
1423 lane.u64(vertex.id);
1424 lane.floats(&[vertex.point.x, vertex.point.y, vertex.point.z]);
1425 }
1426 lane.usize(self.edges.len());
1427 for edge in &self.edges {
1428 lane.u64(edge.id);
1429 curve(&mut lane, &edge.curve);
1430 lane.floats(&[edge.t0, edge.t1]);
1431 lane.u64(edge.start_vertex_id);
1432 lane.u64(edge.end_vertex_id);
1433 lane.bool(edge.degenerate);
1434 name(&mut lane, &edge.name);
1435 }
1436 lane.usize(self.shells.len());
1437 for shell in &self.shells {
1438 lane.u64(shell.id);
1439 lane.usize(shell.faces.len());
1440 for face in &shell.faces {
1441 lane.u64(face.id);
1442 crate::mass_properties::hash_surface(&mut lane, &face.surface);
1443 lane.bool(face.same_sense);
1444 name(&mut lane, &face.name);
1445 lane.usize(face.loops.len());
1446 for loop_record in &face.loops {
1447 lane.u64(loop_record.id);
1448 lane.usize(loop_record.coedges.len());
1449 for coedge in &loop_record.coedges {
1450 lane.u64(coedge.id);
1451 lane.u64(coedge.edge_id);
1452 lane.bool(coedge.forward);
1453 curve(&mut lane, &coedge.pcurve);
1454 }
1455 }
1456 }
1457 }
1458 lane.usize(self.cells.len());
1459 for cell in &self.cells {
1460 lane.u64(cell.id);
1461 lane.bool(cell.void);
1462 lane.usize(cell.faces.len());
1463 for (face, sense) in &cell.faces {
1464 lane.u64(*face);
1465 lane.bool(*sense);
1466 }
1467 }
1468 let [a, b] = lane.finish();
1469 Some((a, b))
1470 }
1471
1472}
1473
1474fn validate_memo_on() -> bool {
1475 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1476 *ON.get_or_init(|| !matches!(std::env::var("BREP_VALIDATE_MEMO").as_deref(), Ok("0") | Ok("off")))
1477}
1478
1479