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 RETAINED_COEDGE_READINGS: std::cell::RefCell<(HashMap<Vec<u64>, f64>, usize)> =
842 std::cell::RefCell::new((HashMap::default(), 0));
843 static COEDGE_MEMO_COUNTS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
845}
846
847const RETAINED_COEDGE_WORDS: usize = 1 << 20;
848const RETAINED_COEDGE_ENTRIES: usize = 4096;
849
850pub(crate) fn clear_coedge_readings() {
851 RETAINED_COEDGE_READINGS.with(|cache| *cache.borrow_mut() = (HashMap::default(), 0));
852}
853
854fn retain_coedge_reading(key: Vec<u64>, value: f64) {
855 if key.len() > RETAINED_COEDGE_WORDS { return; }
856 RETAINED_COEDGE_READINGS.with(|cache| {
857 let (map, words) = &mut *cache.borrow_mut();
858 if *words + key.len() > RETAINED_COEDGE_WORDS || map.len() >= RETAINED_COEDGE_ENTRIES {
859 map.clear();
860 *words = 0;
861 }
862 *words += key.len();
863 map.insert(key, value);
864 });
865}
866
867pub(crate) fn coedge_memo_enabled() -> bool {
877 switch!(live "BREP_VALIDATE_COEDGE_MEMO", true)
878}
879
880
881pub(crate) struct CoedgeMemoScope {
885 opened: bool,
886}
887
888impl CoedgeMemoScope {
889 pub(crate) fn open(enabled: bool) -> Self {
890 let opened = enabled
891 && COEDGE_MEMO.with(|memo| {
892 let mut memo = memo.borrow_mut();
893 if memo.is_some() {
894 return false;
895 }
896 *memo = Some(HashMap::default());
897 COEDGE_MEMO_COUNTS.with(|counts| counts.set((0, 0)));
898 true
899 });
900 CoedgeMemoScope { opened }
901 }
902}
903
904impl Drop for CoedgeMemoScope {
905 fn drop(&mut self) {
906 if self.opened {
907 COEDGE_MEMO.with(|memo| *memo.borrow_mut() = None);
908 let (hits, misses) = COEDGE_MEMO_COUNTS.with(|counts| counts.get());
909 if crate::profile::profile_enabled() {
910 profile_line!("validate.coedge_memo hits={hits} misses={misses}");
911 }
912 }
913 }
914}
915
916fn coedge_memo_key(
917 surface: &NurbsSurface,
918 pcurve: &NurbsCurve,
919 curve: &NurbsCurve,
920 edge: &EdgeRecord,
921 forward: bool,
922 tolerance: f64,
923) -> Vec<u64> {
924 let mut key = Vec::with_capacity(
925 16 + surface.knots_u.len() + surface.knots_v.len() + 4 * surface.control_points.iter().map(Vec::len).sum::<usize>()
926 + pcurve.knots.len() + 4 * pcurve.control_points.len() + curve.knots.len() + 4 * curve.control_points.len(),
927 );
928 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()]);
929 key.extend([edge.t0.to_bits(), edge.t1.to_bits(), forward as u64, tolerance.to_bits()]);
930 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]);
931 key.extend(surface.knots_u.iter().chain(&surface.knots_v).map(|knot| knot.to_bits()));
932 for row in &surface.control_points {
933 key.push(row.len() as u64);
934 for point in row {
935 push_point(&mut key, point);
936 }
937 }
938 for nurbs in [pcurve, curve] {
939 key.extend([nurbs.degree as u64, nurbs.knots.len() as u64, nurbs.control_points.len() as u64]);
940 key.extend(nurbs.knots.iter().map(|knot| knot.to_bits()));
941 for point in &nurbs.control_points {
942 push_point(&mut key, point);
943 }
944 }
945 key
946}
947
948pub(crate) fn exact_lines_enabled() -> bool {
953 switch!("BREP_VALIDATE_EXACT_LINES", true)
954}
955
956
957pub(crate) fn exact_line_on_plane_error(
971 surface: &NurbsSurface,
972 pcurve: &NurbsCurve,
973 curve: &NurbsCurve,
974 edge: &EdgeRecord,
975 forward: bool,
976) -> Option<f64> {
977 let linear = |nurbs: &NurbsCurve| {
978 nurbs.degree == 1
979 && nurbs.control_points.len() == 2
980 && nurbs.knots.len() == 4
981 && nurbs.control_points[0].w == nurbs.control_points[1].w
982 && nurbs.control_points[0].w > 0.0
983 };
984 if !linear(pcurve) || !linear(curve) {
985 return None;
986 }
987 if surface.degree_u != 1
988 || surface.degree_v != 1
989 || surface.knots_u.len() != 4
990 || surface.knots_v.len() != 4
991 || surface.control_points.len() != 2
992 || surface.control_points.iter().any(|row| row.len() != 2)
993 {
994 return None;
995 }
996 let weight = surface.control_points[0][0].w;
997 if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
998 return None;
999 }
1000 let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
1001 let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
1002 let defect = p11.sub(p10).sub(p01.sub(p00));
1003 if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
1004 return None;
1005 }
1006 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1007 let [q0, q1] = pcurve.domain().ok()?;
1008 for q in [q0, q1] {
1009 let uv = pcurve.evaluate(q).ok()?;
1010 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
1011 return None;
1012 }
1013 }
1014 let [c0, c1] = curve.domain().ok()?;
1015 let (low, high) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
1016 if !(low >= c0 && high <= c1) {
1017 return None;
1018 }
1019 let start = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
1020 let end = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
1021 Some(start.max(end))
1022}
1023
1024pub(crate) fn exact_iso_image_error(
1042 surface: &NurbsSurface,
1043 pcurve: &NurbsCurve,
1044 curve: &NurbsCurve,
1045 edge: &EdgeRecord,
1046 forward: bool,
1047) -> Option<f64> {
1048 let weight = pcurve.control_points.first()?.w;
1049 if pcurve.control_points.len() < 2 || !(weight > 0.0) || pcurve.control_points.iter().any(|control| control.w != weight) {
1050 return None;
1051 }
1052 let points = pcurve.control_points.iter().map(|control| control.point().ok()).collect::<Option<Vec<_>>>()?;
1053 let (a, b) = (points[0], points[points.len() - 1]);
1054 let (along_u, constant, run) = if points.iter().all(|point| point.y == a.y) && a.x != b.x {
1056 (true, a.y, (a.x, b.x))
1057 } else if points.iter().all(|point| point.x == a.x) && a.y != b.y {
1058 (false, a.x, (a.y, b.y))
1059 } else {
1060 return None;
1061 };
1062 let linear = pcurve.degree == 1 && points.len() == 2 && pcurve.knots.len() == 4;
1068 let run = if linear {
1069 run
1070 } else {
1071 let degree = pcurve.degree;
1072 if degree == 0 || pcurve.knots.len() != points.len() + degree + 1 {
1073 return None;
1074 }
1075 for (index, point) in points.iter().enumerate() {
1076 let greville = pcurve.knots[index + 1..=index + degree].iter().sum::<f64>() / degree as f64;
1077 if (if along_u { point.x } else { point.y }) != greville {
1078 return None;
1079 }
1080 }
1081 let [q0, q1] = pcurve.domain().ok()?;
1082 (q0, q1)
1083 };
1084 let (run_knots, run_degree, other_knots, other_degree) = if along_u {
1085 (&surface.knots_u, surface.degree_u, &surface.knots_v, surface.degree_v)
1086 } else {
1087 (&surface.knots_v, surface.degree_v, &surface.knots_u, surface.degree_u)
1088 };
1089 if curve.degree != run_degree || &curve.knots != run_knots {
1090 return None;
1091 }
1092 let expected = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
1093 if run.0 != expected.0 || run.1 != expected.1 {
1094 return None;
1095 }
1096 let run_vector = crate::KnotVector::new(run_knots.clone(), run_degree).ok()?;
1097 let [r0, r1] = run_vector.domain();
1098 let (low, high) = (run.0.min(run.1), run.0.max(run.1));
1099 if !(low >= r0 && high <= r1) {
1100 return None;
1101 }
1102 let other = crate::KnotVector::new(other_knots.clone(), other_degree).ok()?;
1103 let [o0, o1] = other.domain();
1104 if !(constant >= o0 && constant <= o1) {
1105 return None;
1106 }
1107 let span = other.find_span(constant);
1108 let basis = other.basis_functions(span, constant);
1109 let count = curve.control_points.len();
1110 let rows = &surface.control_points;
1111 let net_count = if along_u { rows.len() } else { rows.first()?.len() };
1112 if net_count != count {
1113 return None;
1114 }
1115 let mut controls = Vec::with_capacity(count);
1116 for index in 0..count {
1117 let mut blended = crate::Vec4 { x: 0.0, y: 0.0, z: 0.0, w: 0.0 };
1118 for (offset, weight) in basis.iter().enumerate() {
1119 let other_index = span - other_degree + offset;
1120 let control = if along_u { *rows.get(index)?.get(other_index)? } else { *rows.get(other_index)?.get(index)? };
1121 blended = blended.add(control.scale(*weight));
1122 }
1123 if !(blended.w > 0.0) {
1124 return None;
1125 }
1126 controls.push(blended);
1127 }
1128 let factor = controls[0].w / curve.control_points[0].w;
1129 if !(factor > 0.0) {
1130 return None;
1131 }
1132 let mut weight_defect = 0.0f64;
1133 let mut gap = 0.0f64;
1134 let mut diameter = 0.0f64;
1135 let first = curve.control_points[0].point().ok()?;
1136 for (iso, own) in controls.iter().zip(&curve.control_points) {
1137 if !(own.w > 0.0) {
1138 return None;
1139 }
1140 weight_defect = weight_defect.max((iso.w / factor - own.w).abs() / own.w);
1141 let (iso_point, own_point) = (iso.point().ok()?, own.point().ok()?);
1142 gap = gap.max(iso_point.sub(own_point).length());
1143 diameter = diameter.max(own_point.sub(first).length());
1144 }
1145 if weight_defect > 1e-12 {
1146 return None;
1147 }
1148 Some(gap + 4.0 * weight_defect * diameter)
1149}
1150
1151pub(crate) fn exact_images_enabled() -> bool {
1156 switch!(live "BREP_VALIDATE_EXACT_IMAGES", true)
1157}
1158
1159
1160pub(crate) fn exact_plane_image_error(
1177 surface: &NurbsSurface,
1178 pcurve: &NurbsCurve,
1179 curve: &NurbsCurve,
1180 edge: &EdgeRecord,
1181 forward: bool,
1182) -> Option<f64> {
1183 if pcurve.degree != curve.degree || pcurve.knots != curve.knots || pcurve.control_points.len() != curve.control_points.len() {
1184 return None;
1185 }
1186 if surface.degree_u != 1
1187 || surface.degree_v != 1
1188 || surface.knots_u.len() != 4
1189 || surface.knots_v.len() != 4
1190 || surface.control_points.len() != 2
1191 || surface.control_points.iter().any(|row| row.len() != 2)
1192 {
1193 return None;
1194 }
1195 let weight = surface.control_points[0][0].w;
1196 if !(weight > 0.0) || surface.control_points.iter().flatten().any(|control| control.w != weight) {
1197 return None;
1198 }
1199 let corner = |i: usize, j: usize| surface.control_points[i][j].point().ok();
1200 let (p00, p01, p10, p11) = (corner(0, 0)?, corner(0, 1)?, corner(1, 0)?, corner(1, 1)?);
1201 let defect = p11.sub(p10).sub(p01.sub(p00));
1202 if defect.x != 0.0 || defect.y != 0.0 || defect.z != 0.0 {
1203 return None;
1204 }
1205 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1206 if !(u1 > u0 && v1 > v0) {
1207 return None;
1208 }
1209 let [q0, q1] = pcurve.domain().ok()?;
1210 let [c0, c1] = curve.domain().ok()?;
1211 let (t0, t1) = (edge.t0, edge.t1);
1212 let reference = if forward {
1213 if q0.to_bits() != t0.to_bits() || q1.to_bits() != t1.to_bits() {
1214 return None;
1215 }
1216 curve.clone()
1217 } else {
1218 if q0.to_bits() != (c0 + c1 - t1).to_bits() || q1.to_bits() != (c0 + c1 - t0).to_bits() {
1221 return None;
1222 }
1223 let reversed = curve.reversed().ok()?;
1224 if reversed.knots != pcurve.knots {
1225 return None;
1226 }
1227 reversed
1228 };
1229 let (du, dv) = (p10.sub(p00), p01.sub(p00));
1230 let mut image = Vec::with_capacity(pcurve.control_points.len());
1231 for control in &pcurve.control_points {
1232 if !(control.w > 0.0) {
1233 return None;
1234 }
1235 let (u, v) = (control.x / control.w, control.y / control.w);
1236 let point = p00.add(du.scale((u - u0) / (u1 - u0))).add(dv.scale((v - v0) / (v1 - v0)));
1237 image.push(crate::Vec4 { x: point.x * control.w, y: point.y * control.w, z: point.z * control.w, w: control.w });
1238 }
1239 let bound = homogeneous_net_bound(&reference.control_points, &image)?;
1240 let scale = [p00, p01, p10, p11].iter().fold(0f64, |m, p| m.max(p.length()));
1241 (bound <= 1e-10 * (1.0 + scale)).then_some(bound)
1242}
1243
1244fn homogeneous_net_bound(a: &[crate::Vec4], b: &[crate::Vec4]) -> Option<f64> {
1248 if a.len() != b.len() || a.is_empty() {
1249 return None;
1250 }
1251 let origin = Vec3::new(a[0].x / a[0].w, a[0].y / a[0].w, a[0].z / a[0].w);
1252 let mut floor = f64::INFINITY;
1253 let (mut numerator, mut denominator, mut reach) = (0f64, 0f64, 0f64);
1254 for (p, q) in a.iter().zip(b) {
1255 if !(p.w > 0.0 && q.w > 0.0) {
1256 return None;
1257 }
1258 floor = floor.min(p.w);
1259 let ph = Vec3::new(p.x, p.y, p.z).sub(origin.scale(p.w));
1260 let qh = Vec3::new(q.x, q.y, q.z).sub(origin.scale(q.w));
1261 numerator = numerator.max(ph.sub(qh).length());
1262 denominator = denominator.max((p.w - q.w).abs());
1263 reach = reach.max(qh.length() / q.w);
1264 }
1265 let bound = (numerator + reach * denominator) / floor;
1266 bound.is_finite().then_some(bound)
1267}
1268
1269pub(crate) fn exact_iso_enabled() -> bool {
1273 switch!("BREP_VALIDATE_EXACT_ISO", true)
1274}
1275
1276
1277pub(crate) fn exact_iso_line_error(
1295 surface: &NurbsSurface,
1296 pcurve: &NurbsCurve,
1297 curve: &NurbsCurve,
1298 edge: &EdgeRecord,
1299 forward: bool,
1300) -> Option<f64> {
1301 if pcurve.degree != 1
1302 || pcurve.control_points.len() != 2
1303 || pcurve.knots.len() != 4
1304 || pcurve.control_points[0].w != pcurve.control_points[1].w
1305 || !(pcurve.control_points[0].w > 0.0)
1306 {
1307 return None;
1308 }
1309 let [q0, q1] = pcurve.domain().ok()?;
1310 let (a, b) = (pcurve.evaluate(q0).ok()?, pcurve.evaluate(q1).ok()?);
1311 let ([u0, u1], [v0, v1]) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1312 for uv in [a, b] {
1313 if !(uv.x >= u0 && uv.x <= u1 && uv.y >= v0 && uv.y <= v1) {
1314 return None;
1315 }
1316 }
1317 let (iso, from, to) = if a.y.to_bits() == b.y.to_bits() && a.x != b.x {
1319 (surface.iso_curve_v(a.y).ok()?, a.x, b.x)
1320 } else if a.x.to_bits() == b.x.to_bits() && a.y != b.y {
1321 (surface.iso_curve_u(a.x).ok()?, a.y, b.y)
1322 } else {
1323 return None;
1324 };
1325 let (start, end) = if forward { (edge.t0, edge.t1) } else { (edge.t1, edge.t0) };
1327 if start.to_bits() != from.to_bits() || end.to_bits() != to.to_bits() {
1328 return None;
1329 }
1330 let matches = |iso: &NurbsCurve| {
1331 iso.degree == curve.degree
1332 && iso.knots.len() == curve.knots.len()
1333 && iso.knots.iter().zip(&curve.knots).all(|(x, y)| x.to_bits() == y.to_bits())
1334 && iso.control_points.len() == curve.control_points.len()
1335 };
1336 let iso = if !matches(&iso) && exact_images_enabled() {
1341 let (low, high) = (from.min(to), from.max(to));
1342 let [d0, d1] = iso.domain().ok()?;
1343 let tail = if low == d0 { iso } else { iso.split(low).ok()?.1 };
1344 let piece = if high == d1 { tail } else { tail.split(high).ok()?.0 };
1345 if !matches(&piece) {
1346 return None;
1347 }
1348 crate::blend::record_count("count.validate.exact_iso_piece", 1);
1349 piece
1350 } else {
1351 iso
1352 };
1353 if !matches(&iso) {
1354 return None;
1355 }
1356 let size = iso
1357 .control_points
1358 .iter()
1359 .filter_map(|c| c.point().ok())
1360 .fold(0f64, |m, q| m.max(q.x.abs()).max(q.y.abs()).max(q.z.abs()));
1361 let mut deviation = 0f64;
1362 for (p, q) in iso.control_points.iter().zip(&curve.control_points) {
1363 if p.w.to_bits() != q.w.to_bits() || !(p.w > 0.0) {
1364 return None;
1365 }
1366 deviation = deviation.max(p.point().ok()?.sub(q.point().ok()?).length());
1367 }
1368 if !(deviation <= 1e-12 * size.max(1.0)) {
1369 return None;
1370 }
1371 let start_gap = coedge_sample(surface, pcurve, curve, edge, forward, 0.0).ok()?.0;
1372 let end_gap = coedge_sample(surface, pcurve, curve, edge, forward, 1.0).ok()?.0;
1373 Some(start_gap.max(end_gap).max(deviation))
1374}
1375
1376fn memoized_coedge_error(
1378 surface: &NurbsSurface,
1379 pcurve: &NurbsCurve,
1380 curve: &NurbsCurve,
1381 edge: &EdgeRecord,
1382 forward: bool,
1383 tolerance: f64,
1384) -> Result<f64, String> {
1385 if exact_lines_enabled() {
1386 if let Some(error) = exact_line_on_plane_error(surface, pcurve, curve, edge, forward) {
1387 return Ok(error);
1388 }
1389 }
1390 if exact_iso_enabled() {
1391 if let Some(error) = exact_iso_line_error(surface, pcurve, curve, edge, forward) {
1392 crate::blend::record_count("count.validate.exact_iso", 1);
1393 return Ok(error);
1394 }
1395 }
1396 if exact_images_enabled() {
1397 if let Some(error) = exact_plane_image_error(surface, pcurve, curve, edge, forward) {
1398 crate::blend::record_count("count.validate.exact_image", 1);
1399 return Ok(error);
1400 }
1401 }
1402 if COEDGE_MEMO.with(|memo| memo.borrow().is_none()) {
1403 return coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1404 }
1405 let key = coedge_memo_key(surface, pcurve, curve, edge, forward, tolerance);
1406 if let Some(hit) = COEDGE_MEMO.with(|memo| memo.borrow().as_ref().and_then(|map| map.get(&key).cloned())) {
1407 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
1408 return hit;
1409 }
1410 if let Some(value) = RETAINED_COEDGE_READINGS.with(|cache| cache.borrow().0.get(&key).copied()) {
1411 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0 + 1, counts.get().1)));
1412 COEDGE_MEMO.with(|memo| {
1413 if let Some(map) = memo.borrow_mut().as_mut() { map.insert(key, Ok(value)); }
1414 });
1415 return Ok(value);
1416 }
1417 COEDGE_MEMO_COUNTS.with(|counts| counts.set((counts.get().0, counts.get().1 + 1)));
1418 let read = coedge_error_floored(surface, pcurve, curve, edge, forward, tolerance);
1419 if let Ok(value) = read { retain_coedge_reading(key.clone(), value); }
1420 COEDGE_MEMO.with(|memo| {
1421 if let Some(map) = memo.borrow_mut().as_mut() {
1422 map.insert(key, read.clone());
1423 }
1424 });
1425 read
1426}
1427
1428pub(crate) fn coedge_error_floored(
1438 surface: &NurbsSurface,
1439 pcurve: &NurbsCurve,
1440 curve: &NurbsCurve,
1441 edge: &EdgeRecord,
1442 forward: bool,
1443 tolerance: f64,
1444) -> Result<f64, String> {
1445 let steered = adaptive_coedge_error(surface, pcurve, curve, edge, forward, tolerance)?;
1446 Ok(steered.max(knot_span_midpoint_error(surface, pcurve, curve, edge, forward)?))
1447}
1448
1449fn coedge_floor_tracing() -> bool {
1453 debug_flag!("BREP_DEBUG_COEDGE_FLOOR")
1454}
1455
1456fn knot_span_midpoint_error(
1463 surface: &NurbsSurface,
1464 pcurve: &NurbsCurve,
1465 curve: &NurbsCurve,
1466 edge: &EdgeRecord,
1467 forward: bool,
1468) -> Result<f64, String> {
1469 let mut worst = 0.0_f64;
1470 let [q0, q1] = pcurve.domain()?;
1471 if q1 > q0 {
1472 let mut previous = q0;
1473 for &knot in pcurve.knots.iter().chain(std::iter::once(&q1)) {
1474 if knot > previous && knot <= q1 {
1475 let fraction = ((previous + knot) * 0.5 - q0) / (q1 - q0);
1476 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1477 if coedge_floor_tracing() && sample.0 > 1e-3 {
1478 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1479 let ([u0, u1], [v0, v1]) = (surface.domain_u()?, surface.domain_v()?);
1480 eprintln!(
1481 "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 {}",
1482 edge.id, sample.0, uv.x, uv.y, surface.closed_directions()?, surface.analytic().map(|_| "yes"), pcurve.degree, pcurve.knots.len(), pcurve.control_points.len()
1483 );
1484 }
1485 worst = worst.max(sample.0);
1486 previous = knot;
1487 }
1488 }
1489 }
1490 let span = edge.t1 - edge.t0;
1491 if span.is_finite() && span != 0.0 {
1492 let low = edge.t0.min(edge.t1);
1493 let high = edge.t0.max(edge.t1);
1494 let mut previous = low;
1495 for &knot in curve.knots.iter().chain(std::iter::once(&high)) {
1496 if knot > previous && knot <= high {
1497 let midpoint = (previous + knot) * 0.5;
1498 let fraction = if forward {
1499 (midpoint - edge.t0) / span
1500 } else {
1501 (edge.t1 - midpoint) / span
1502 };
1503 let sample = coedge_sample(surface, pcurve, curve, edge, forward, fraction)?;
1504 if coedge_floor_tracing() && sample.0 > 1e-3 {
1505 let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
1506 eprintln!(
1507 "COEDGE-FLOOR edge {} edge-span midpoint fraction {fraction:.6}: miss {:.4e} at uv ({:.6}, {:.6})",
1508 edge.id, sample.0, uv.x, uv.y
1509 );
1510 }
1511 worst = worst.max(sample.0);
1512 previous = knot;
1513 }
1514 }
1515 }
1516 Ok(worst)
1517}
1518
1519fn segments_cross(a: Vec2, b: Vec2, c: Vec2, d: Vec2, tolerance: f64) -> bool {
1520 let cross = |first: Vec2, second: Vec2| first.x * second.y - first.y * second.x;
1521 let ab = b.sub(a);
1522 let cd = d.sub(c);
1523 let denominator = cross(ab, cd);
1524 if denominator.abs() <= tolerance {
1525 return false;
1526 }
1527 let ac = c.sub(a);
1528 let t = cross(ac, cd) / denominator;
1529 let u = cross(ac, ab) / denominator;
1530 t > tolerance && t < 1.0 - tolerance && u > tolerance && u < 1.0 - tolerance
1531}
1532
1533fn validate_periodic_boundary_winding(
1540 surface: &NurbsSurface,
1541 face: &FaceRecord,
1542 tolerances: &KernelTolerances,
1543) -> Option<String> {
1544 let Some(crate::AnalyticSurface::RuledRevolution { rho0, rho1, .. }) = surface.analytic() else {
1545 return None;
1546 };
1547 if rho0 * rho1 <= 0.0 || rho0.abs().min(rho1.abs()) <= tolerances.model {
1550 return None;
1551 }
1552 let [u0, u1] = surface.domain_u().ok()?;
1553 let period = u1 - u0;
1554 if period <= 0.0 { return None; }
1555 let mut displacement = 0.0;
1556 for coedge in face.loops.iter().flat_map(|lp| &lp.coedges) {
1557 let [t0, t1] = coedge.pcurve.domain().ok()?;
1558 let start = coedge.pcurve.evaluate(t0).ok()?;
1559 let end = coedge.pcurve.evaluate(t1).ok()?;
1560 displacement += end.x - start.x;
1561 }
1562 let winding = displacement / period;
1563 (winding.abs() > 0.5).then(|| format!(
1567 "nonzero periodic boundary winding ({winding:.6} turns) on a nonsingular revolution wall"
1568 ))
1569}
1570
1571fn validate_uv_wire(
1572 surface: &NurbsSurface,
1573 loop_record: &LoopRecord,
1574 edges: &HashMap<u64, &EdgeRecord>,
1575 tolerances: &KernelTolerances,
1576 outer: bool,
1577 same_sense: bool,
1578) -> Option<String> {
1579 let u_domain = crate::KnotVector::new(surface.knots_u.clone(), surface.degree_u)
1580 .ok()?
1581 .domain();
1582 let v_domain = crate::KnotVector::new(surface.knots_v.clone(), surface.degree_v)
1583 .ok()?
1584 .domain();
1585 let u_span = u_domain[1] - u_domain[0];
1586 let v_span = v_domain[1] - v_domain[0];
1587 let mut points = Vec::<Vec2>::new();
1588 for coedge in &loop_record.coedges {
1589 let edge = edges.get(&coedge.edge_id)?;
1590 if edge.degenerate {
1591 continue;
1592 }
1593 let [q0, q1] = coedge.pcurve.domain().ok()?;
1594 for index in 0..=8 {
1595 if !points.is_empty() && index == 0 {
1596 continue;
1597 }
1598 let fraction = index as f64 / 8.0;
1599 let parameter = q0 + (q1 - q0) * fraction;
1600 let value = coedge.pcurve.evaluate(parameter).ok()?;
1601 let mut point = Vec2 {
1602 x: value.x,
1603 y: value.y,
1604 };
1605 if let Some(previous) = points.last() {
1606 while point.x - previous.x > u_span * 0.5 {
1607 point.x -= u_span;
1608 }
1609 while point.x - previous.x < -u_span * 0.5 {
1610 point.x += u_span;
1611 }
1612 while point.y - previous.y > v_span * 0.5 {
1613 point.y -= v_span;
1614 }
1615 while point.y - previous.y < -v_span * 0.5 {
1616 point.y += v_span;
1617 }
1618 }
1619 points.push(point);
1620 }
1621 }
1622 if points.len() < 4 {
1623 return None;
1624 }
1625 let first = points[0];
1626 let last = *points.last()?;
1627 let uv_tolerance = tolerances.model.max(1e-8);
1628 if last.sub(first).length() > uv_tolerance * 100.0 {
1629 let du = (last.x - first.x) / u_span;
1632 let dv = (last.y - first.y) / v_span;
1633 if (du - du.round()).abs() * u_span > uv_tolerance * 100.0
1634 || (dv - dv.round()).abs() * v_span > uv_tolerance * 100.0
1635 {
1636 return Some(format!(
1637 "wire is open in parameter space (gap {:.3e})",
1638 last.sub(first).length()
1639 ));
1640 }
1641 }
1642 for first_index in 0..points.len() - 1 {
1643 for second_index in first_index + 2..points.len() - 1 {
1644 if first_index == 0 && second_index + 1 == points.len() - 1 {
1645 continue;
1646 }
1647 if segments_cross(
1648 points[first_index],
1649 points[first_index + 1],
1650 points[second_index],
1651 points[second_index + 1],
1652 1e-10,
1653 ) {
1654 return Some(format!(
1655 "wire self-intersects near sampled segments {first_index} and {second_index}"
1656 ));
1657 }
1658 }
1659 }
1660 let area = points
1661 .windows(2)
1662 .map(|pair| pair[0].x * pair[1].y - pair[1].x * pair[0].y)
1663 .sum::<f64>()
1664 * 0.5;
1665 if area.abs() > uv_tolerance * uv_tolerance {
1666 let expected_positive = if outer { same_sense } else { !same_sense };
1667 if (area > 0.0) != expected_positive {
1668 return Some(format!(
1669 "{} wire winding disagrees with face sense (signed UV area {:.6})",
1670 if outer { "outer" } else { "inner" },
1671 area
1672 ));
1673 }
1674 }
1675 None
1676}
1677
1678const VALIDATED_CLEAN_CAP: usize = 4096;
1680
1681thread_local! {
1682 static VALIDATED_CLEAN: std::cell::RefCell<rustc_hash::FxHashSet<(u64, u64)>> =
1683 std::cell::RefCell::new(rustc_hash::FxHashSet::default());
1684}
1685
1686impl BrepSolid {
1687 pub(crate) fn clear_validate_memo() {
1691 VALIDATED_CLEAN.with(|clean| clean.borrow_mut().clear());
1692 }
1693
1694 pub(crate) fn validate_memo_key(&self) -> Option<(u64, u64)> {
1701 use crate::mass_properties::TwoLane;
1702 fn curve(lane: &mut TwoLane, curve: &crate::NurbsCurve) {
1703 lane.usize(curve.degree);
1704 lane.floats(&curve.knots);
1705 lane.usize(curve.control_points.len());
1706 for point in &curve.control_points {
1707 lane.floats(&[point.x, point.y, point.z, point.w]);
1708 }
1709 }
1710 fn name(lane: &mut TwoLane, name: &Option<String>) {
1711 lane.bool(name.is_some());
1712 if let Some(name) = name {
1713 lane.usize(name.len());
1714 for byte in name.bytes() {
1715 lane.u64(u64::from(byte));
1716 }
1717 }
1718 }
1719 let mut lane = TwoLane::new();
1720 lane.u64(self.id);
1721 lane.u64(self.genus as u64);
1722 lane.usize(self.kind as usize);
1723 lane.usize(self.vertices.len());
1724 for vertex in &self.vertices {
1725 lane.u64(vertex.id);
1726 lane.floats(&[vertex.point.x, vertex.point.y, vertex.point.z]);
1727 }
1728 lane.usize(self.edges.len());
1729 for edge in &self.edges {
1730 lane.u64(edge.id);
1731 curve(&mut lane, &edge.curve);
1732 lane.floats(&[edge.t0, edge.t1]);
1733 lane.u64(edge.start_vertex_id);
1734 lane.u64(edge.end_vertex_id);
1735 lane.bool(edge.degenerate);
1736 name(&mut lane, &edge.name);
1737 }
1738 lane.usize(self.shells.len());
1739 for shell in &self.shells {
1740 lane.u64(shell.id);
1741 lane.usize(shell.faces.len());
1742 for face in &shell.faces {
1743 lane.u64(face.id);
1744 crate::mass_properties::hash_surface(&mut lane, &face.surface);
1745 lane.bool(face.same_sense);
1746 name(&mut lane, &face.name);
1747 lane.usize(face.loops.len());
1748 for loop_record in &face.loops {
1749 lane.u64(loop_record.id);
1750 lane.usize(loop_record.coedges.len());
1751 for coedge in &loop_record.coedges {
1752 lane.u64(coedge.id);
1753 lane.u64(coedge.edge_id);
1754 lane.bool(coedge.forward);
1755 curve(&mut lane, &coedge.pcurve);
1756 }
1757 }
1758 }
1759 }
1760 lane.usize(self.cells.len());
1761 for cell in &self.cells {
1762 lane.u64(cell.id);
1763 lane.bool(cell.void);
1764 lane.usize(cell.faces.len());
1765 for (face, sense) in &cell.faces {
1766 lane.u64(*face);
1767 lane.bool(*sense);
1768 }
1769 }
1770 let [a, b] = lane.finish();
1771 Some((a, b))
1772 }
1773
1774}
1775
1776fn validate_memo_on() -> bool {
1777 switch!("BREP_VALIDATE_MEMO", true)
1778}
1779
1780
1781