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 }
299 }
300
301 for edge in &self.edges {
302 if !(edge.t0.is_finite() && edge.t1.is_finite() && edge.t0 < edge.t1) {
303 issues.push(ValidationIssue::error(format!(
304 "edge {} has invalid parameter range",
305 edge.id
306 )));
307 }
308 let Some(start) = vertices.get(&edge.start_vertex_id) else {
309 issues.push(ValidationIssue::error(format!(
310 "edge {} references missing start vertex {}",
311 edge.id, edge.start_vertex_id
312 )));
313 continue;
314 };
315 let Some(end) = vertices.get(&edge.end_vertex_id) else {
316 issues.push(ValidationIssue::error(format!(
317 "edge {} references missing end vertex {}",
318 edge.id, edge.end_vertex_id
319 )));
320 continue;
321 };
322 if let Ok(curve) = NurbsCurve::new(
323 edge.curve.degree,
324 edge.curve.knots.clone(),
325 edge.curve.control_points.clone(),
326 ) {
327 if let Ok(point) = curve.evaluate(edge.t0) {
328 let gap = point.sub(start.point).length();
329 if gap > vertex_match {
330 issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: true }, format!(
331 "edge {} curve start does not match vertex {} \
332 (gap={gap:.9}, curve={point:?}, vertex={:?})",
333 edge.id, start.id, start.point
334 )));
335 }
336 }
337 if let Ok(point) = curve.evaluate(edge.t1) {
338 let gap = point.sub(end.point).length();
339 if gap > vertex_match {
340 issues.push(ValidationIssue::error_kind(IssueKind::CurveVertexGap { at_start: false }, format!(
341 "edge {} curve end does not match vertex {} \
342 (gap={gap:.9}, curve={point:?}, vertex={:?})",
343 edge.id, end.id, end.point
344 )));
345 }
346 }
347 }
348 let uses = edge_uses.get(&edge.id).map(Vec::as_slice).unwrap_or(&[]);
349 if edge.degenerate {
350 let valid = uses.len() == 1 || (uses.len() == 2 && uses[0] != uses[1]);
356 if !valid {
357 issues.push(ValidationIssue::error(format!(
358 "degenerate edge {} has invalid incidence {:?} \
359 (expected one use or an opposite-sense pair)",
360 edge.id, uses,
361 )));
362 }
363 } else if self.kind == BodyKind::Sheet && uses.len() == 1 {
367 } else if self.kind == BodyKind::Cellular {
368 if uses.len() < 2 {
371 issues.push(ValidationIssue::error_kind(IssueKind::OpenEdge,
372 format!("Cellular edge {} used {} times (expected at least 2)", edge.id, uses.len())));
373 }
374 } else if uses.len() != 2 {
375 let kind = if uses.len() < 2 { IssueKind::OpenEdge } else { IssueKind::OverUsedEdge };
376 issues.push(ValidationIssue::error_kind(kind, format!(
377 "edge {} used {} times (expected 2)",
378 edge.id,
379 uses.len()
380 )));
381 } else if uses[0] == uses[1] {
382 issues.push(ValidationIssue::error(format!(
383 "edge {} has coedges with the same sense",
384 edge.id
385 )));
386 }
387 }
388
389 for edge_id in edge_uses.keys() {
390 if !edges.contains_key(edge_id) {
391 issues.push(ValidationIssue::error(format!(
392 "topology references missing edge {}",
393 edge_id
394 )));
395 }
396 }
397
398 let non_degenerate_vertex_ids = self
402 .edges
403 .iter()
404 .filter(|edge| !edge.degenerate)
405 .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
406 .collect::<HashSet<_>>();
407 let vertex_count = vertices
408 .keys()
409 .filter(|id| non_degenerate_vertex_ids.contains(id))
410 .count() as i64;
411 let edge_count = self.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
412 let face_count = face_ids.len() as i64;
413 let hole_count = self.bounding_hole_count();
414 let shell_count = self.shells.len() as i64;
415 let actual = vertex_count - edge_count + face_count - hole_count;
416 let expected = 2 * (shell_count - self.genus);
417 let closed = self
468 .edges
469 .iter()
470 .filter(|edge| !edge.degenerate)
471 .all(|edge| edge_uses.get(&edge.id).map(Vec::len) == Some(2));
472 if !matches!(self.kind, BodyKind::Sheet | BodyKind::Cellular) && closed && actual != expected {
475 let pairs = self.coincident_parallel_edge_pairs() as i64;
484 if actual + pairs != expected {
485 issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch, format!(
486 "Euler formula: V-E+F-H = {}, expected {} ({} coincident parallel edge pair(s) credited)",
487 actual + pairs,
488 expected,
489 pairs
490 )));
491 }
492 }
493 if self.kind == BodyKind::Cellular {
494 self.validate_cells(&edges, &mut issues);
495 }
496 ValidationReport {
497 issues,
498 wire_warnings,
499 max_pcurve_error,
500 }
501 }
502
503 fn validate_cells(&self, edges: &HashMap<u64, &EdgeRecord>, issues: &mut Vec<ValidationIssue>) {
506 if self.cells.is_empty() {
507 issues.push(ValidationIssue::error("Cellular body has no declared cells"));
508 return;
509 }
510 let faces: HashMap<_, _> = self.shells.iter().flat_map(|s| &s.faces).map(|f| (f.id, f)).collect();
511 let mut cell_ids = HashSet::default();
512 let mut owners: HashMap<u64, Vec<bool>> = HashMap::default();
513 let mut fans: HashMap<u64, Vec<(&FaceRecord, bool, &CoedgeRecord)>> = HashMap::default();
515 for cell in &self.cells {
516 if !cell_ids.insert(cell.id) {
517 issues.push(ValidationIssue::error(format!("duplicate cell id {}", cell.id)));
518 }
519 if cell.faces.is_empty() {
520 issues.push(ValidationIssue::error(format!("cell {} has no faces", cell.id)));
521 }
522 let mut seen = HashSet::default();
523 let mut uses: HashMap<u64, Vec<bool>> = HashMap::default();
524 let mut holes = 0i64;
525 for &(id, forward) in &cell.faces {
526 if !seen.insert(id) {
527 issues.push(ValidationIssue::error(format!("cell {} repeats face {id}", cell.id)));
528 continue;
529 }
530 let Some(&face) = faces.get(&id) else {
531 issues.push(ValidationIssue::error(format!("cell {} references missing face {id}", cell.id)));
532 continue;
533 };
534 owners.entry(id).or_default().push(forward);
535 holes += face.loops.iter().filter(|record| !record.coedges.is_empty()
536 && record.coedges.iter().any(|c| edges.get(&c.edge_id).is_some_and(|e| !e.degenerate)))
537 .count().saturating_sub(1) as i64;
538 for coedge in face.loops.iter().flat_map(|l| &l.coedges) {
539 uses.entry(coedge.edge_id).or_default().push(coedge.forward == forward);
540 fans.entry(coedge.edge_id).or_default().push((face, forward, coedge));
541 }
542 }
543 let mut vertices = HashSet::default();
544 let mut edge_count = 0i64;
545 for (id, senses) in &uses {
546 let Some(&edge) = edges.get(id) else { continue; };
547 let opposite = senses.len() == 2 && senses[0] != senses[1];
548 if !(opposite || (edge.degenerate && senses.len() == 1)) {
549 issues.push(ValidationIssue::error(format!(
550 "cell {} edge {id} is not closed with opposite senses: {senses:?}", cell.id)));
551 }
552 if !edge.degenerate {
553 edge_count += 1;
554 vertices.extend([edge.start_vertex_id, edge.end_vertex_id]);
555 }
556 }
557 let chi = vertices.len() as i64 - edge_count + seen.len() as i64 - holes;
558 if chi % 2 != 0 {
559 issues.push(ValidationIssue::error_kind(IssueKind::GenusMismatch,
560 format!("cell {} has odd Euler characteristic {chi}", cell.id)));
561 }
562 }
563 for id in faces.keys() {
564 let senses = owners.get(id).map(Vec::as_slice).unwrap_or(&[]);
565 if senses.is_empty() || senses.len() > 2 || (senses.len() == 2 && senses[0] == senses[1]) {
566 issues.push(ValidationIssue::error(format!(
567 "face {id} has invalid cell ownership senses {senses:?}")));
568 }
569 }
570 for (id, uses) in fans {
571 let Some(&edge) = edges.get(&id) else { continue; };
572 if edge.degenerate || uses.len() <= 2 { continue; }
573 for fraction in [0.25, 0.5, 0.75] {
574 let reading = (|| -> Result<Vec<(f64, bool)>, String> {
575 let t = edge.t0 + (edge.t1-edge.t0)*fraction;
576 let derivatives = edge.curve.derivatives(t, 1)?;
577 let tangent = derivatives[1].scale(if edge.t1 >= edge.t0 { 1.0 } else { -1.0 }).normalized()?;
578 let axis = tangent.perpendicular()?;
579 let other = tangent.cross(axis);
580 let mut radial = Vec::new();
581 for &(face, forward, coedge) in &uses {
582 let uv = crate::project_point_to_surface(&face.surface, derivatives[0])?;
583 let normal = face.surface.normal(uv.u, uv.v)?
584 .scale(if face.same_sense { 1.0 } else { -1.0 });
585 let ray = normal.cross(tangent.scale(if coedge.forward { 1.0 } else { -1.0 }));
590 let (x,y) = (ray.dot(axis), ray.dot(other));
591 if !x.is_finite() || !y.is_finite() || x*x+y*y < 1e-20 {
592 return Err("undefined radial face tangent".into());
593 }
594 radial.push((y.atan2(x), coedge.forward == forward));
595 }
596 radial.sort_by(|a,b| a.0.total_cmp(&b.0).then_with(|| b.1.cmp(&a.1)));
599 Ok(radial)
600 })();
601 match reading {
602 Ok(radial) if radial.iter().enumerate().all(|(i, a)| a.1 != radial[(i+1)%radial.len()].1) => {},
603 Ok(_) => { issues.push(ValidationIssue::error(format!(
604 "Cellular edge {id} radial cell senses do not alternate at {fraction}"))); break; },
605 Err(error) => { issues.push(ValidationIssue::error(format!(
606 "Cellular edge {id} radial cell fan cannot be read: {error}"))); break; },
607 }
608 }
609 }
610 }
611
612 pub(crate) fn bounding_hole_count(&self) -> i64 {
626 let degenerate: HashSet<u64> = self
627 .edges
628 .iter()
629 .filter(|edge| edge.degenerate)
630 .map(|edge| edge.id)
631 .collect();
632 self.shells
633 .iter()
634 .flat_map(|shell| &shell.faces)
635 .map(|face| {
636 face.loops
637 .iter()
638 .filter(|record| {
639 !record.coedges.is_empty()
640 && !record
641 .coedges
642 .iter()
643 .all(|coedge| degenerate.contains(&coedge.edge_id))
644 })
645 .count()
646 .saturating_sub(1) as i64
647 })
648 .sum()
649 }
650
651 pub(crate) fn coincident_parallel_edge_pairs(&self) -> usize {
656 let candidates: Vec<&EdgeRecord> =
657 self.edges.iter().filter(|edge| !edge.degenerate).collect();
658 let mut consumed = vec![false; candidates.len()];
659 let mut pairs = 0usize;
660 for first_index in 0..candidates.len() {
661 if consumed[first_index] {
662 continue;
663 }
664 let first = candidates[first_index];
665 for second_index in first_index + 1..candidates.len() {
666 if consumed[second_index] {
667 continue;
668 }
669 let second = candidates[second_index];
670 let endpoints_match = (first.start_vertex_id == second.start_vertex_id
671 && first.end_vertex_id == second.end_vertex_id)
672 || (first.start_vertex_id == second.end_vertex_id
673 && first.end_vertex_id == second.start_vertex_id);
674 if !endpoints_match {
675 continue;
676 }
677 let coincident = (1..4).all(|sample| {
679 let fraction = sample as f64 / 4.0;
680 let Ok(on_first) = first
681 .curve
682 .evaluate(first.t0 + (first.t1 - first.t0) * fraction)
683 else {
684 return false;
685 };
686 let forward = second.t0 + (second.t1 - second.t0) * fraction;
687 let backward = second.t1 - (second.t1 - second.t0) * fraction;
688 let tolerance = 1e-6 * (1.0 + on_first.length());
689 let matches_forward = second
690 .curve
691 .evaluate(forward)
692 .map(|point| point.sub(on_first).length() <= tolerance)
693 .unwrap_or(false);
694 let matches_backward = second
695 .curve
696 .evaluate(backward)
697 .map(|point| point.sub(on_first).length() <= tolerance)
698 .unwrap_or(false);
699 matches_forward || matches_backward
700 });
701 if coincident {
702 consumed[first_index] = true;
703 consumed[second_index] = true;
704 pairs += 1;
705 break;
706 }
707 }
708 }
709 pairs
710 }
711}
712
713fn coedge_sample(
714 surface: &NurbsSurface,
715 pcurve: &NurbsCurve,
716 curve: &NurbsCurve,
717 edge: &EdgeRecord,
718 forward: bool,
719 fraction: f64,
720) -> Result<(f64, Vec3, Vec3), String> {
721 let [q0, q1] = pcurve.domain()?;
722 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
723 let on_surface = surface.evaluate_extended(uv.x, uv.y)?;
728 let t = if forward {
729 edge.t0 + (edge.t1 - edge.t0) * fraction
730 } else {
731 edge.t1 - (edge.t1 - edge.t0) * fraction
732 };
733 let on_curve = curve.evaluate(t)?;
734 Ok((on_surface.sub(on_curve).length(), on_surface, on_curve))
735}
736
737pub(crate) fn adaptive_coedge_error(
742 surface: &NurbsSurface,
743 pcurve: &NurbsCurve,
744 curve: &NurbsCurve,
745 edge: &EdgeRecord,
746 forward: bool,
747 tolerance: f64,
748) -> Result<f64, String> {
749 fn interval(
750 surface: &NurbsSurface,
751 pcurve: &NurbsCurve,
752 curve: &NurbsCurve,
753 edge: &EdgeRecord,
754 forward: bool,
755 a: f64,
756 b: f64,
757 sample_a: (f64, Vec3, Vec3),
758 sample_b: (f64, Vec3, Vec3),
759 tolerance: f64,
760 depth: usize,
761 ) -> Result<f64, String> {
762 let mid = (a + b) * 0.5;
763 let sample_mid = coedge_sample(surface, pcurve, curve, edge, forward, mid)?;
764 let error_nonlinearity = (sample_mid.0 - (sample_a.0 + sample_b.0) * 0.5).abs();
765 let surface_bend = sample_mid
766 .1
767 .sub(sample_a.1.add(sample_b.1).scale(0.5))
768 .length();
769 let curve_bend = sample_mid
770 .2
771 .sub(sample_a.2.add(sample_b.2).scale(0.5))
772 .length();
773 let local_max = sample_a.0.max(sample_mid.0).max(sample_b.0);
774 if depth >= 3
775 || (error_nonlinearity <= tolerance * 0.05
776 && surface_bend.max(curve_bend) <= tolerance * 0.25)
777 {
778 return Ok(local_max);
779 }
780 Ok(interval(
781 surface,
782 pcurve,
783 curve,
784 edge,
785 forward,
786 a,
787 mid,
788 sample_a,
789 sample_mid,
790 tolerance,
791 depth + 1,
792 )?
793 .max(interval(
794 surface,
795 pcurve,
796 curve,
797 edge,
798 forward,
799 mid,
800 b,
801 sample_mid,
802 sample_b,
803 tolerance,
804 depth + 1,
805 )?))
806 }
807
808 let mut maximum = 0.0f64;
809 let mut previous = coedge_sample(surface, pcurve, curve, edge, forward, 0.0)?;
810 maximum = maximum.max(previous.0);
811 for index in 1..=32 {
812 let a = (index - 1) as f64 / 32.0;
813 let b = index as f64 / 32.0;
814 let next = coedge_sample(surface, pcurve, curve, edge, forward, b)?;
815 maximum = maximum.max(interval(
816 surface, pcurve, curve, edge, forward, a, b, previous, next, tolerance, 0,
817 )?);
818 previous = next;
819 }
820 Ok(maximum)
827}
828
829thread_local! {
839 static COEDGE_MEMO: std::cell::RefCell<Option<HashMap<Vec<u64>, Result<f64, String>>>> =
840 const { std::cell::RefCell::new(None) };
841 static COEDGE_MEMO_COUNTS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
843}
844
845pub(crate) fn coedge_memo_enabled() -> bool {
855 let enabled = !matches!(std::env::var("BREP_VALIDATE_COEDGE_MEMO").as_deref(), Ok("0") | Ok("off"));
856 enabled
857}
858
859
860pub(crate) struct CoedgeMemoScope {
862 opened: bool,
863}
864
865impl CoedgeMemoScope {
866 pub(crate) fn open(enabled: bool) -> Self {
867 let opened = enabled
868 && COEDGE_MEMO.with(|memo| {
869 let mut memo = memo.borrow_mut();
870 if memo.is_some() {
871 return false;
872 }
873 *memo = Some(HashMap::default());
874 COEDGE_MEMO_COUNTS.with(|counts| counts.set((0, 0)));
875 true
876 });
877 CoedgeMemoScope { opened }
878 }
879}
880
881impl Drop for CoedgeMemoScope {
882 fn drop(&mut self) {
883 if self.opened {
884 COEDGE_MEMO.with(|memo| *memo.borrow_mut() = None);
885 let (hits, misses) = COEDGE_MEMO_COUNTS.with(|counts| counts.get());
886 if std::env::var_os("BREP_PROFILE").is_some() {
887 eprintln!("validate.coedge_memo hits={hits} misses={misses}");
888 }
889 }
890 }
891}
892
893fn coedge_memo_key(
894 surface: &NurbsSurface,
895 pcurve: &NurbsCurve,
896 curve: &NurbsCurve,
897 edge: &EdgeRecord,
898 forward: bool,
899 tolerance: f64,
900) -> Vec<u64> {
901 let mut key = Vec::with_capacity(
902 16 + surface.knots_u.len() + surface.knots_v.len() + 4 * surface.control_points.iter().map(Vec::len).sum::<usize>()
903 + pcurve.knots.len() + 4 * pcurve.control_points.len() + curve.knots.len() + 4 * curve.control_points.len(),
904 );
905 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()]);
906 key.extend([edge.t0.to_bits(), edge.t1.to_bits(), forward as u64, tolerance.to_bits()]);
907 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]);
908 key.extend(surface.knots_u.iter().chain(&surface.knots_v).map(|knot| knot.to_bits()));
909 for row in &surface.control_points {
910 key.push(row.len() as u64);
911 for point in row {
912 push_point(&mut key, point);
913 }
914 }
915 for nurbs in [pcurve, curve] {
916 key.extend([nurbs.degree as u64, nurbs.knots.len() as u64, nurbs.control_points.len() as u64]);
917 key.extend(nurbs.knots.iter().map(|knot| knot.to_bits()));
918 for point in &nurbs.control_points {
919 push_point(&mut key, point);
920 }
921 }
922 key
923}
924
925pub(crate) fn exact_lines_enabled() -> bool {
930 let enabled = !matches!(std::env::var("BREP_VALIDATE_EXACT_LINES").as_deref(), Ok("0") | Ok("off"));
931 enabled
932}
933
934
935pub(crate) fn exact_line_on_plane_error(
949 surface: &NurbsSurface,
950 pcurve: &NurbsCurve,
951 curve: &NurbsCurve,
952 edge: &EdgeRecord,
953 forward: bool,
954) -> Option<f64> {
955 let linear = |nurbs: &NurbsCurve| {
956 nurbs.degree == 1
957 && nurbs.control_points.len() == 2
958 && nurbs.knots.len() == 4
959 && nurbs.control_points[0].w == nurbs.control_points[1].w
960 && nurbs.control_points[0].w > 0.0
961 };
962 if !linear(pcurve) || !linear(curve) {
963 return None;
964 }
965 if surface.degree_u != 1
966 || surface.degree_v != 1
967 || surface.knots_u.len() != 4
968 || surface.knots_v.len() != 4
969 || surface.control_points.len() != 2
970 || surface.control_points.iter().any(|row| row.len() != 2)
971 {
972 return None;
973 }
974 let weight = surface.control_points[0][0].w;
975 if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
976 return None;
977 }
978 let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
979 let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
980 let defect = p11.sub(p10).sub(p01.sub(p00));
981 if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
982 return None;
983 }
984 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
985 let [q0, q1] = pcurve.domain().ok()?;
986 for q in [q0, q1] {
987 let uv = pcurve.evaluate(q).ok()?;
988 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
989 return None;
990 }
991 }
992 let [c0, c1] = curve.domain().ok()?;
993 let (low, high) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
994 if !(low >= c0 && high <= c1) {
995 return None;
996 }
997 let start = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
998 let end = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
999 Some(start.max(end))
1000}
1001
1002pub(crate) fn exact_iso_enabled() -> bool {
1006 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1007 *ON.get_or_init(|| !matches!(std::env::var("BREP_VALIDATE_EXACT_ISO").as_deref(), Ok("0") | Ok("off")))
1008}
1009
1010
1011pub(crate) fn exact_iso_line_error(
1029 surface: &NurbsSurface,
1030 pcurve: &NurbsCurve,
1031 curve: &NurbsCurve,
1032 edge: &EdgeRecord,
1033 forward: bool,
1034) -> Option<f64> {
1035 if pcurve.degree != 1
1036 || pcurve.control_points.len() != 2
1037 || pcurve.knots.len() != 4
1038 || pcurve.control_points[0].w != pcurve.control_points[1].w
1039 || !(pcurve.control_points[0].w > 0.0)
1040 {
1041 return None;
1042 }
1043 let [q0, q1] = pcurve.domain().ok()?;
1044 let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
1045 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1046 for uv in [a, b] {
1047 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
1048 return None;
1049 }
1050 }
1051 let (iso, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x {
1053 (surface.iso_curve_v(a.y).ok()?, a.x, b.x)
1054 } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y {
1055 (surface.iso_curve_u(a.x).ok()?, a.y, b.y)
1056 } else {
1057 return None;
1058 };
1059 let (start, end) = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
1061 if start.to_bits() != from.to_bits() || end.to_bits() != to.to_bits() {
1062 return None;
1063 }
1064 if iso.degree != curve.degree
1065 || iso.knots.len() != curve.knots.len()
1066 || iso.knots.iter().zip(&curve.knots).any(|(x, y)| x.to_bits() != y.to_bits())
1067 || iso.control_points.len() != curve.control_points.len()
1068 {
1069 return None;
1070 }
1071 let size = iso
1072 .control_points
1073 .iter()
1074 .filter_map(|c| c.point().ok())
1075 .fold(0f64, |m, q| m.max(q.x.abs()).max(q.y.abs()).max(q.z.abs()));
1076 let mut deviation = 0f64;
1077 for (p, q) in iso.control_points.iter().zip(&curve.control_points) {
1078 if p.w.to_bits() != q.w.to_bits() || !(p.w > 0.0) {
1079 return None;
1080 }
1081 deviation = deviation.max(p.point().ok()?.sub(q.point().ok()?).length());
1082 }
1083 if !(deviation <= 1e-12 * size.max(1.0)) {
1084 return None;
1085 }
1086 let start_gap = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
1087 let end_gap = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
1088 Some(start_gap.max(end_gap).max(deviation))
1089}
1090
1091fn memoized_coedge_error(
1093 surface: &NurbsSurface,
1094 pcurve: &NurbsCurve,
1095 curve: &NurbsCurve,
1096 edge: &EdgeRecord,
1097 forward: bool,
1098 tolerance: f64,
1099) -> Result<f64, String> {
1100 if exact_lines_enabled() {
1101 if let Some(error) = exact_line_on_plane_error(surface, pcurve, curve, edge, forward) {
1102 return Ok(error);
1103 }
1104 }
1105 if exact_iso_enabled() {
1106 if let Some(error) = exact_iso_line_error(surface, pcurve, curve, edge, forward) {
1107 crate::blend::record_count("count.validate.exact_iso", 1);
1108 return Ok(error);
1109 }
1110 }
1111 if COEDGE_MEMO.with(|memo| memo.borrow().is_none()) {
1112 return coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1113 }
1114 let key = coedge_memo_key(surface, pcurve, curve, edge, forward, tolerance);
1115 if let Some(hit) = COEDGE_MEMO.with(|memo| memo.borrow().as_ref().and_then(|map| map.get(&key).cloned())) {
1116 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
1117 return hit;
1118 }
1119 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0, counts.get().1 + 1)));
1120 let read = coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1121 COEDGE_MEMO.with(|memo| {
1122 if let Some(map) = memo.borrow_mut().as_mut() {
1123 map.insert(key, read.clone());
1124 }
1125 });
1126 read
1127}
1128
1129pub(crate) fn coedge_error_floored(
1130 surface: &NurbsSurface,
1131 pcurve: &NurbsCurve,
1132 curve: &NurbsCurve,
1133 edge: &EdgeRecord,
1134 forward: bool,
1135 tolerance: f64,
1136) -> Result<f64, String> {
1137 let steered = adaptive_coedge_error(surface, pcurve, curve, edge, forward, tolerance)?;
1138 Ok(steered.max(knot_span_midpoint_error(surface, pcurve, curve, edge, forward)?))
1139}
1140
1141fn coedge_floor_tracing() -> bool {
1145 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1146 *ON.get_or_init(|| std::env::var_os("BREP_DEBUG_COEDGE_FLOOR").is_some())
1147}
1148
1149fn knot_span_midpoint_error(
1156 surface: &NurbsSurface,
1157 pcurve: &NurbsCurve,
1158 curve: &NurbsCurve,
1159 edge: &EdgeRecord,
1160 forward: bool,
1161) -> Result<f64, String> {
1162 let mut worst = 0.0_f64;
1163 let [q0, q1] = pcurve.domain()?;
1164 if q1 > q0 {
1165 let mut previous = q0;
1166 for &knot in pcurve.knots.iter().chain(std::iter::once(&q1)) {
1167 if knot > previous && knot <= q1 {
1168 let fraction = ((previous + knot) * 0.5 - q0) / (q1 - q0);
1169 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1170 if coedge_floor_tracing() && sample.0 > 1e-3 {
1171 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1172 let ([u0, u1], [v0, v1]) = (surface.domain_u()?, surface.domain_v()?);
1173 eprintln!(
1174 "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 {}",
1175 edge.id, sample.0, uv.x, uv.y, surface.closed_directions()?, surface.analytic().map(|_| "yes"), pcurve.degree, pcurve.knots.len(), pcurve.control_points.len()
1176 );
1177 }
1178 worst = worst.max(sample.0);
1179 previous = knot;
1180 }
1181 }
1182 }
1183 let span = edge.t1 - edge.t0;
1184 if span.is_finite() && span != 0.0 {
1185 let low = edge.t0.min(edge.t1);
1186 let high = edge.t0.max(edge.t1);
1187 let mut previous = low;
1188 for &knot in curve.knots.iter().chain(std::iter::once(&high)) {
1189 if knot > previous && knot <= high {
1190 let midpoint = (previous + knot) * 0.5;
1191 let fraction = if forward {
1192 (midpoint - edge.t0) / span
1193 } else {
1194 (edge.t1 - midpoint) / span
1195 };
1196 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1197 if coedge_floor_tracing() && sample.0 > 1e-3 {
1198 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1199 eprintln!(
1200 "COEDGE-FLOOR edge {} edge-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6})",
1201 edge.id, sample.0, uv.x, uv.y
1202 );
1203 }
1204 worst = worst.max(sample.0);
1205 previous = knot;
1206 }
1207 }
1208 }
1209 Ok(worst)
1210}
1211
1212fn segments_cross(a: Vec2, b: Vec2, c: Vec2, d: Vec2, tolerance: f64) -> bool {
1213 let cross = |first: Vec2, second: Vec2| first.x * second.y - first.y * second.x;
1214 let ab = b.sub(a);
1215 let cd = d.sub(c);
1216 let denominator = cross(ab, cd);
1217 if denominator.abs() <= tolerance {
1218 return false;
1219 }
1220 let ac = c.sub(a);
1221 let t = cross(ac, cd) / denominator;
1222 let u = cross(ac, ab) / denominator;
1223 t > tolerance && t < 1.0 - tolerance && u > tolerance && u < 1.0 - tolerance
1224}
1225
1226fn validate_uv_wire(
1227 surface: &NurbsSurface,
1228 loop_record: &LoopRecord,
1229 edges: &HashMap<u64, &EdgeRecord>,
1230 tolerances: &KernelTolerances,
1231 outer: bool,
1232 same_sense: bool,
1233) -> Option<String> {
1234 let u_domain = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)
1235 .ok()?
1236 .domain();
1237 let v_domain = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)
1238 .ok()?
1239 .domain();
1240 let u_span = u_domain[1] - u_domain[0];
1241 let v_span = v_domain[1] - v_domain[0];
1242 let mut points = Vec::<Vec2>::new();
1243 for coedge in &loop_record.coedges {
1244 let edge = edges.get(&coedge.edge_id)?;
1245 if edge.degenerate {
1246 continue;
1247 }
1248 let [q0, q1] = coedge.pcurve.domain().ok()?;
1249 for index in 0..=8 {
1250 if !points.is_empty() && index == 0 {
1251 continue;
1252 }
1253 let fraction = index as f64 / 8.0;
1254 let parameter = q0 + (q1 - q0) * fraction;
1255 let value = coedge.pcurve.evaluate(parameter).ok()?;
1256 let mut point = Vec2 {
1257 x: value.x,
1258 y: value.y,
1259 };
1260 if let Some(previous) = points.last() {
1261 while point.x - previous.x > u_span * 0.5 {
1262 point.x -= u_span;
1263 }
1264 while point.x - previous.x < -u_span * 0.5 {
1265 point.x += u_span;
1266 }
1267 while point.y - previous.y > v_span * 0.5 {
1268 point.y -= v_span;
1269 }
1270 while point.y - previous.y < -v_span * 0.5 {
1271 point.y += v_span;
1272 }
1273 }
1274 points.push(point);
1275 }
1276 }
1277 if points.len() < 4 {
1278 return None;
1279 }
1280 let first = points[0];
1281 let last = *points.last()?;
1282 let uv_tolerance = tolerances.model.max(1e-8);
1283 if last.sub(first).length() > uv_tolerance * 100.0 {
1284 let du = (last.x - first.x) / u_span;
1287 let dv = (last.y - first.y) / v_span;
1288 if (du - du.round()).abs() * u_span > uv_tolerance * 100.0
1289 || (dv - dv.round()).abs() * v_span > uv_tolerance * 100.0
1290 {
1291 return Some(format!(
1292 "wire is open in parameter space (gap {:.3e})",
1293 last.sub(first).length()
1294 ));
1295 }
1296 }
1297 for first_index in 0..points.len() - 1 {
1298 for second_index in first_index + 2..points.len() - 1 {
1299 if first_index == 0 && second_index + 1 == points.len() - 1 {
1300 continue;
1301 }
1302 if segments_cross(
1303 points[first_index],
1304 points[first_index + 1],
1305 points[second_index],
1306 points[second_index + 1],
1307 1e-10,
1308 ) {
1309 return Some(format!(
1310 "wire self-intersects near sampled segments {first_index} and {second_index}"
1311 ));
1312 }
1313 }
1314 }
1315 let area = points
1316 .windows(2)
1317 .map(|pair| pair[0].x * pair[1].y - pair[1].x * pair[0].y)
1318 .sum::<f64>()
1319 * 0.5;
1320 if area.abs() > uv_tolerance * uv_tolerance {
1321 let expected_positive = if outer { same_sense } else { !same_sense };
1322 if (area > 0.0) != expected_positive {
1323 return Some(format!(
1324 "{} wire winding disagrees with face sense (signed UV area {:.6})",
1325 if outer { "outer" } else { "inner" },
1326 area
1327 ));
1328 }
1329 }
1330 None
1331}
1332
1333const VALIDATED_CLEAN_CAP: usize = 4096;
1335
1336thread_local! {
1337 static VALIDATED_CLEAN: std::cell::RefCell<rustc_hash::FxHashSet<(u64, u64)>> =
1338 std::cell::RefCell::new(rustc_hash::FxHashSet::default());
1339}
1340
1341impl BrepSolid {
1342 pub(crate) fn clear_validate_memo() {
1346 VALIDATED_CLEAN.with(|clean| clean.borrow_mut().clear());
1347 }
1348
1349 pub(crate) fn validate_memo_key(&self) -> Option<(u64, u64)> {
1356 use crate::mass_properties::TwoLane;
1357 fn curve(lane: &mut TwoLane, curve: &crate::NurbsCurve) {
1358 lane.usize(curve.degree);
1359 lane.floats(&curve.knots);
1360 lane.usize(curve.control_points.len());
1361 for point in &curve.control_points {
1362 lane.floats(&[point.x, point.y, point.z, point.w]);
1363 }
1364 }
1365 fn name(lane: &mut TwoLane, name: &Option<String>) {
1366 lane.bool(name.is_some());
1367 if let Some(name) = name {
1368 lane.usize(name.len());
1369 for byte in name.bytes() {
1370 lane.u64(u64::from(byte));
1371 }
1372 }
1373 }
1374 let mut lane = TwoLane::new();
1375 lane.u64(self.id);
1376 lane.u64(self.genus as u64);
1377 lane.usize(self.kind as usize);
1378 lane.usize(self.vertices.len());
1379 for vertex in &self.vertices {
1380 lane.u64(vertex.id);
1381 lane.floats(&[vertex.point.x, vertex.point.y, vertex.point.z]);
1382 }
1383 lane.usize(self.edges.len());
1384 for edge in &self.edges {
1385 lane.u64(edge.id);
1386 curve(&mut lane, &edge.curve);
1387 lane.floats(&[edge.t0, edge.t1]);
1388 lane.u64(edge.start_vertex_id);
1389 lane.u64(edge.end_vertex_id);
1390 lane.bool(edge.degenerate);
1391 name(&mut lane, &edge.name);
1392 }
1393 lane.usize(self.shells.len());
1394 for shell in &self.shells {
1395 lane.u64(shell.id);
1396 lane.usize(shell.faces.len());
1397 for face in &shell.faces {
1398 lane.u64(face.id);
1399 crate::mass_properties::hash_surface(&mut lane, &face.surface);
1400 lane.bool(face.same_sense);
1401 name(&mut lane, &face.name);
1402 lane.usize(face.loops.len());
1403 for loop_record in &face.loops {
1404 lane.u64(loop_record.id);
1405 lane.usize(loop_record.coedges.len());
1406 for coedge in &loop_record.coedges {
1407 lane.u64(coedge.id);
1408 lane.u64(coedge.edge_id);
1409 lane.bool(coedge.forward);
1410 curve(&mut lane, &coedge.pcurve);
1411 }
1412 }
1413 }
1414 }
1415 lane.usize(self.cells.len());
1416 for cell in &self.cells {
1417 lane.u64(cell.id);
1418 lane.bool(cell.void);
1419 lane.usize(cell.faces.len());
1420 for (face, sense) in &cell.faces {
1421 lane.u64(*face);
1422 lane.bool(*sense);
1423 }
1424 }
1425 let [a, b] = lane.finish();
1426 Some((a, b))
1427 }
1428
1429}
1430
1431fn validate_memo_on() -> bool {
1432 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1433 *ON.get_or_init(|| !matches!(std::env::var("BREP_VALIDATE_MEMO").as_deref(), Ok("0") | Ok("off")))
1434}
1435
1436