1use brepkit_math::nurbs::surface::NurbsSurface;
9use brepkit_math::vec::{Point3, Vec3};
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum RecognizedSurface {
14 Plane {
16 normal: Vec3,
18 d: f64,
20 },
21 Cylinder {
23 origin: Point3,
25 axis: Vec3,
27 radius: f64,
29 },
30 Sphere {
32 center: Point3,
34 radius: f64,
36 },
37 Cone {
39 apex: Point3,
41 axis: Vec3,
43 half_angle: f64,
46 },
47 Torus {
49 center: Point3,
51 axis: Vec3,
54 major_radius: f64,
56 minor_radius: f64,
58 },
59 NotRecognized,
61}
62
63#[must_use]
71pub fn recognize_surface(surface: &NurbsSurface, tolerance: f64) -> RecognizedSurface {
72 if let Some((normal, d)) = try_recognize_plane(surface, tolerance) {
73 return RecognizedSurface::Plane { normal, d };
74 }
75 if let Some((origin, axis, radius)) = try_recognize_cylinder(surface, tolerance) {
76 return RecognizedSurface::Cylinder {
77 origin,
78 axis,
79 radius,
80 };
81 }
82 if let Some((center, radius)) = try_recognize_sphere(surface, tolerance) {
83 return RecognizedSurface::Sphere { center, radius };
84 }
85 if let Some((apex, axis, half_angle)) = try_recognize_cone(surface, tolerance) {
86 return RecognizedSurface::Cone {
87 apex,
88 axis,
89 half_angle,
90 };
91 }
92 if let Some((center, axis, major_radius, minor_radius)) =
93 try_recognize_torus(surface, tolerance)
94 {
95 return RecognizedSurface::Torus {
96 center,
97 axis,
98 major_radius,
99 minor_radius,
100 };
101 }
102 RecognizedSurface::NotRecognized
103}
104
105fn try_recognize_plane(surface: &NurbsSurface, tolerance: f64) -> Option<(Vec3, f64)> {
111 let cps = surface.control_points();
112 if cps.is_empty() || cps[0].is_empty() {
113 return None;
114 }
115
116 let mut all_pts: Vec<Point3> = Vec::new();
118 for row in cps {
119 for pt in row {
120 all_pts.push(*pt);
121 }
122 }
123
124 if all_pts.len() < 3 {
125 return None;
126 }
127
128 let p0 = all_pts[0];
135 let farthest = |measure: &dyn Fn(Point3) -> f64| {
136 all_pts
137 .iter()
138 .map(|&p| (p, measure(p)))
139 .max_by(|a, b| a.1.total_cmp(&b.1))
140 };
141 let (p1, base) = farthest(&|p| (p - p0).length())?;
142 if base <= tolerance {
143 return None;
144 }
145 let along = (p1 - p0) * (1.0 / base);
146 let (p2, height) = farthest(&|p| {
147 let v = p - p0;
148 (v - along * along.dot(v)).length()
149 })?;
150 if height <= tolerance {
151 return None;
152 }
153 let n = (p1 - p0).cross(p2 - p0).normalize().ok()?;
154 let d = n.dot(Vec3::new(p0.x(), p0.y(), p0.z()));
155
156 for pt in &all_pts {
158 let dist = n.dot(Vec3::new(pt.x(), pt.y(), pt.z())) - d;
159 if dist.abs() > tolerance {
160 return None;
161 }
162 }
163
164 Some((n, d))
165}
166
167#[allow(clippy::items_after_statements)]
178fn try_recognize_cylinder(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64)> {
179 let cps = surface.control_points();
180 if cps.len() < 2 {
181 return None;
182 }
183 for row in cps {
184 if row.len() < 2 {
185 return None;
186 }
187 }
188
189 let mut axis_sum = Vec3::new(0.0, 0.0, 0.0);
191 for row in cps {
192 let v = row[row.len() - 1] - row[0];
193 axis_sum += v;
194 }
195 #[allow(clippy::cast_precision_loss)]
196 let axis_avg = axis_sum * (1.0 / cps.len() as f64);
197 let axis_len = axis_avg.length();
198 if axis_len < tolerance {
199 return None;
200 }
201 let axis = axis_avg.normalize().ok()?;
202
203 let (u0, u1) = surface.domain_u();
208 let (v0, v1) = surface.domain_v();
209 const N: usize = 8;
210
211 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
212 for iu in 0..N {
213 #[allow(clippy::cast_precision_loss)]
214 let u = u0 + (u1 - u0) * (iu as f64) / ((N - 1) as f64);
215 for iv in 0..N {
216 #[allow(clippy::cast_precision_loss)]
217 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
218 samples.push(surface.evaluate(u, v));
219 }
220 }
221
222 let ref_pt = samples[0];
228
229 let perp1 = {
231 let trial = if axis.x().abs() < 0.9 {
232 Vec3::new(1.0, 0.0, 0.0)
233 } else {
234 Vec3::new(0.0, 1.0, 0.0)
235 };
236 let p = trial - axis * axis.dot(trial);
237 p.normalize().unwrap_or(Vec3::new(1.0, 0.0, 0.0))
238 };
239 let perp2 = axis.cross(perp1);
240
241 let pts_2d: Vec<(f64, f64)> = samples
243 .iter()
244 .map(|pt| {
245 let v = *pt - ref_pt;
246 (perp1.dot(v), perp2.dot(v))
247 })
248 .collect();
249
250 let mut ata = [[0.0_f64; 3]; 3];
253 let mut atb = [0.0_f64; 3];
254 for &(x, y) in &pts_2d {
255 let rhs = x * x + y * y;
256 let row = [2.0 * x, 2.0 * y, 1.0];
257 for i in 0..3 {
258 for j in 0..3 {
259 ata[i][j] += row[i] * row[j];
260 }
261 atb[i] += row[i] * rhs;
262 }
263 }
264
265 let sol = solve_3x3(ata, atb)?;
266 let cx = sol[0];
267 let cy = sol[1];
268 let origin = ref_pt + perp1 * cx + perp2 * cy;
270
271 let mut radii: Vec<f64> = Vec::with_capacity(samples.len());
272 for pt in &samples {
273 let to_pt = *pt - origin;
274 let along = axis.dot(to_pt);
275 let radial = to_pt - axis * along;
276 radii.push(radial.length());
277 }
278
279 if radii.is_empty() {
280 return None;
281 }
282
283 let sum: f64 = radii.iter().sum();
284 #[allow(clippy::cast_precision_loss)]
285 let mean_radius = sum / radii.len() as f64;
286
287 if mean_radius < tolerance {
288 return None; }
290
291 let max_dev = radii
292 .iter()
293 .map(|r| (r - mean_radius).abs())
294 .fold(0.0_f64, f64::max);
295 if max_dev > tolerance {
296 return None;
297 }
298
299 Some((origin, axis, mean_radius))
300}
301
302#[allow(clippy::items_after_statements)]
309fn try_recognize_sphere(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, f64)> {
310 let (u0, u1) = surface.domain_u();
311 let (v0, v1) = surface.domain_v();
312 const N: usize = 8;
313
314 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
315
316 for iu in 0..N {
317 #[allow(clippy::cast_precision_loss)]
318 let u = u0 + (u1 - u0) * (iu as f64) / ((N - 1) as f64);
319 for iv in 0..N {
320 #[allow(clippy::cast_precision_loss)]
321 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
322 samples.push(surface.evaluate(u, v));
323 }
324 }
325
326 if samples.len() < 4 {
327 return None;
328 }
329
330 let sq = |p: Point3| p.x() * p.x() + p.y() * p.y() + p.z() * p.z();
334
335 let n = samples.len();
336 let mut ata = [[0.0_f64; 3]; 3];
337 let mut atb = [0.0_f64; 3];
338
339 let p0 = samples[0];
340 let sq0 = sq(p0);
341
342 for i in 1..n {
343 let pi = samples[i];
344 let a_row = [
345 2.0 * (pi.x() - p0.x()),
346 2.0 * (pi.y() - p0.y()),
347 2.0 * (pi.z() - p0.z()),
348 ];
349 let bi = sq(pi) - sq0;
350
351 for r in 0..3 {
352 for c in 0..3 {
353 ata[r][c] += a_row[r] * a_row[c];
354 }
355 atb[r] += a_row[r] * bi;
356 }
357 }
358
359 let center = solve_3x3(ata, atb)?;
360 let center_pt = Point3::new(center[0], center[1], center[2]);
361
362 let mut distances: Vec<f64> = Vec::with_capacity(n);
363 for pt in &samples {
364 let d = Vec3::new(
365 pt.x() - center_pt.x(),
366 pt.y() - center_pt.y(),
367 pt.z() - center_pt.z(),
368 )
369 .length();
370 distances.push(d);
371 }
372
373 let sum: f64 = distances.iter().sum();
374 #[allow(clippy::cast_precision_loss)]
375 let mean_radius = sum / distances.len() as f64;
376
377 if mean_radius < tolerance {
378 return None;
379 }
380
381 let max_dev = distances
382 .iter()
383 .map(|d| (d - mean_radius).abs())
384 .fold(0.0_f64, f64::max);
385
386 if max_dev > tolerance {
387 return None;
388 }
389
390 Some((center_pt, mean_radius))
391}
392
393fn try_recognize_cone(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64)> {
410 const N: usize = 8;
411 let cps = surface.control_points();
412 if cps.len() < 2 {
413 return None;
414 }
415 for row in cps {
416 if row.len() < 2 {
417 return None;
418 }
419 }
420
421 let n_rows = cps.len();
430 let row_count = if n_rows >= 3 && (cps[0][0] - cps[n_rows - 1][0]).length() < tolerance {
431 n_rows - 1
432 } else {
433 n_rows
434 };
435 let mut axis_sum = Vec3::new(0.0, 0.0, 0.0);
436 for row in cps.iter().take(row_count) {
437 let v = row[row.len() - 1] - row[0];
438 axis_sum += v;
439 }
440 #[allow(clippy::cast_precision_loss)]
441 let axis_avg = axis_sum * (1.0 / row_count as f64);
442 if axis_avg.length() < tolerance {
443 return None;
444 }
445 let axis = axis_avg.normalize().ok()?;
446
447 let (u0, u1) = surface.domain_u();
453 let (v0, v1) = surface.domain_v();
454 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
455 for iu in 0..N {
456 #[allow(clippy::cast_precision_loss)]
457 let u = u0 + (u1 - u0) * (iu as f64 + 0.5) / (N as f64);
458 for iv in 0..N {
459 #[allow(clippy::cast_precision_loss)]
460 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
461 samples.push(surface.evaluate(u, v));
462 }
463 }
464
465 #[allow(clippy::cast_precision_loss)]
472 let inv_n = 1.0 / samples.len() as f64;
473 let mut anchor_x = 0.0_f64;
474 let mut anchor_y = 0.0_f64;
475 let mut anchor_z = 0.0_f64;
476 for p in &samples {
477 anchor_x += p.x();
478 anchor_y += p.y();
479 anchor_z += p.z();
480 }
481 let anchor = Point3::new(anchor_x * inv_n, anchor_y * inv_n, anchor_z * inv_n);
482
483 let mut axials: Vec<f64> = Vec::with_capacity(samples.len());
489 let mut radials: Vec<f64> = Vec::with_capacity(samples.len());
490 for p in &samples {
491 let to_p = *p - anchor;
492 let along = axis.dot(to_p);
493 let radial_vec = to_p - axis * along;
494 axials.push(along);
495 radials.push(radial_vec.length());
496 }
497
498 let max_r = radials.iter().fold(0.0_f64, |m, &r| m.max(r));
501 let min_r = radials.iter().fold(f64::INFINITY, |m, &r| m.min(r));
502 if max_r - min_r < tolerance {
503 return None; }
505
506 let n_f = samples.len() as f64;
517 let sum_a: f64 = axials.iter().sum();
518 let sum_r: f64 = radials.iter().sum();
519 let mean_a = sum_a / n_f;
520 let mean_r = sum_r / n_f;
521 let mut s_aa = 0.0_f64;
522 let mut s_ar = 0.0_f64;
523 for i in 0..samples.len() {
524 let da = axials[i] - mean_a;
525 let dr = radials[i] - mean_r;
526 s_aa += da * da;
527 s_ar += da * dr;
528 }
529 if s_aa < 1e-30 {
530 return None;
531 }
532 let slope = s_ar / s_aa;
533 let intercept = mean_r - slope * mean_a;
534 if slope.abs() < tolerance {
535 return None; }
537 let axial_apex = -intercept / slope;
539
540 for i in 0..samples.len() {
542 let pred = slope * axials[i] + intercept;
543 if (radials[i] - pred).abs() > tolerance {
544 return None;
545 }
546 }
547
548 let half_angle = (1.0 / slope.abs()).atan();
557 if !(0.0 < half_angle && half_angle < std::f64::consts::FRAC_PI_2) {
558 return None;
559 }
560
561 let apex_offset = axis * axial_apex;
567 let apex = anchor + apex_offset;
568 let cone_axis = if slope > 0.0 { axis } else { -axis };
569
570 Some((apex, cone_axis, half_angle))
571}
572
573#[allow(clippy::items_after_statements)]
593fn try_recognize_torus(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64, f64)> {
594 const N: usize = 8;
595 let cps = surface.control_points();
596 if cps.len() < 2 {
597 return None;
598 }
599 for row in cps {
600 if row.len() < 2 {
601 return None;
602 }
603 }
604
605 let n_rows = cps.len();
615 if n_rows < 3 {
616 return None;
617 }
618 let p0 = cps[0][0];
619 let mut axis: Option<Vec3> = None;
620 'outer: for i in 1..n_rows {
621 let v1 = cps[i][0] - p0;
622 for j in (i + 1)..n_rows {
623 let v2 = cps[j][0] - p0;
624 let cross = v1.cross(v2);
625 if cross.length() > tolerance
626 && let Ok(normalized) = cross.normalize()
627 {
628 axis = Some(normalized);
629 break 'outer;
630 }
631 }
632 }
633 let axis = axis?;
634
635 let (u0, u1) = surface.domain_u();
637 let (v0, v1) = surface.domain_v();
638 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
639 for iu in 0..N {
640 #[allow(clippy::cast_precision_loss)]
641 let u = u0 + (u1 - u0) * (iu as f64 + 0.5) / (N as f64);
642 for iv in 0..N {
643 #[allow(clippy::cast_precision_loss)]
644 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
645 samples.push(surface.evaluate(u, v));
646 }
647 }
648
649 #[allow(clippy::cast_precision_loss)]
651 let inv_n = 1.0 / samples.len() as f64;
652 let mut ax = 0.0_f64;
653 let mut ay = 0.0_f64;
654 let mut az = 0.0_f64;
655 for p in &samples {
656 ax += p.x();
657 ay += p.y();
658 az += p.z();
659 }
660 let anchor = Point3::new(ax * inv_n, ay * inv_n, az * inv_n);
661
662 let mut axials: Vec<f64> = Vec::with_capacity(samples.len());
664 let mut radials: Vec<f64> = Vec::with_capacity(samples.len());
665 for p in &samples {
666 let to_p = *p - anchor;
667 let along = axis.dot(to_p);
668 let radial = (to_p - axis * along).length();
669 axials.push(along);
670 radials.push(radial);
671 }
672
673 let mut ata = [[0.0_f64; 3]; 3];
677 let mut atb = [0.0_f64; 3];
678 for i in 0..samples.len() {
679 let x = axials[i];
680 let y = radials[i];
681 let row = [2.0 * x, 2.0 * y, 1.0];
682 let rhs = x * x + y * y;
683 for r in 0..3 {
684 for c in 0..3 {
685 ata[r][c] += row[r] * row[c];
686 }
687 atb[r] += row[r] * rhs;
688 }
689 }
690 let sol = solve_3x3(ata, atb)?;
691 let center_axial = sol[0];
692 let major_radius = sol[1];
693 let k = sol[2]; let r_sq = k + center_axial * center_axial + major_radius * major_radius;
695 if r_sq <= 0.0 || major_radius <= 0.0 {
696 return None;
697 }
698 let minor_radius = r_sq.sqrt();
699
700 if minor_radius >= major_radius - tolerance {
702 return None;
703 }
704
705 for i in 0..samples.len() {
707 let dx = axials[i] - center_axial;
708 let dy = radials[i] - major_radius;
709 let dist = (dx * dx + dy * dy).sqrt();
710 if (dist - minor_radius).abs() > tolerance {
711 return None;
712 }
713 }
714
715 let center = anchor + axis * center_axial;
716 Some((center, axis, major_radius, minor_radius))
717}
718
719pub(super) fn solve_3x3(a: [[f64; 3]; 3], b: [f64; 3]) -> Option<[f64; 3]> {
727 let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
728 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
729 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
730
731 if det.abs() < 1e-30 {
732 return None;
733 }
734
735 let inv = 1.0 / det;
736
737 let x0 = (b[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
738 - a[0][1] * (b[1] * a[2][2] - a[1][2] * b[2])
739 + a[0][2] * (b[1] * a[2][1] - a[1][1] * b[2]))
740 * inv;
741
742 let x1 = (a[0][0] * (b[1] * a[2][2] - a[1][2] * b[2])
743 - b[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
744 + a[0][2] * (a[1][0] * b[2] - b[1] * a[2][0]))
745 * inv;
746
747 let x2 = (a[0][0] * (a[1][1] * b[2] - b[1] * a[2][1])
748 - a[0][1] * (a[1][0] * b[2] - b[1] * a[2][0])
749 + b[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]))
750 * inv;
751
752 Some([x0, x1, x2])
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub enum DetectedSurfaceKind {
761 Plane,
763 Sphere,
765 Cylinder,
767 BSpline,
769}
770
771impl DetectedSurfaceKind {
772 #[must_use]
774 pub const fn as_str(self) -> &'static str {
775 match self {
776 Self::Plane => "plane",
777 Self::Sphere => "sphere",
778 Self::Cylinder => "cylinder",
779 Self::BSpline => "bspline",
780 }
781 }
782}
783
784#[must_use]
792#[allow(clippy::cast_precision_loss)]
793pub fn detect_surface_kind(surface: &NurbsSurface) -> DetectedSurfaceKind {
794 let (u_min, u_max) = surface.domain_u();
795 let (v_min, v_max) = surface.domain_v();
796 let n = 8; let mut points = Vec::with_capacity(n * n);
799 for i in 0..n {
800 for j in 0..n {
801 let u = u_min + (u_max - u_min) * (i as f64) / ((n - 1) as f64);
802 let v = v_min + (v_max - v_min) * (j as f64) / ((n - 1) as f64);
803 points.push(surface.evaluate(u, v));
804 }
805 }
806
807 let mut cx = 0.0_f64;
809 let mut cy = 0.0_f64;
810 let mut cz = 0.0_f64;
811 for p in &points {
812 cx += p.x();
813 cy += p.y();
814 cz += p.z();
815 }
816 let np = points.len() as f64;
817 let center = Point3::new(cx / np, cy / np, cz / np);
818
819 let mut plane_normal = None;
822 for i in 1..points.len() {
823 for j in (i + 1)..points.len() {
824 let v0 = points[i] - center;
825 let v1 = points[j] - center;
826 let n = v0.cross(v1);
827 if let Ok(normalized) = n.normalize() {
828 plane_normal = Some(normalized);
829 break;
830 }
831 }
832 if plane_normal.is_some() {
833 break;
834 }
835 }
836 if let Some(normal) = plane_normal {
837 let is_plane = points
838 .iter()
839 .all(|p| (*p - center).dot(normal).abs() < 1e-6);
840 if is_plane {
841 return DetectedSurfaceKind::Plane;
842 }
843 }
844
845 let distances: Vec<f64> = points.iter().map(|p| (*p - center).length()).collect();
847 let avg_dist = distances.iter().sum::<f64>() / np;
848
849 if avg_dist < 1e-10 {
850 return DetectedSurfaceKind::BSpline;
851 }
852
853 let tol = avg_dist * 1e-3; let is_sphere = distances.iter().all(|d| (d - avg_dist).abs() < tol);
855
856 if is_sphere {
857 return DetectedSurfaceKind::Sphere;
858 }
859
860 if let Some(axis_dir) = estimate_cylinder_axis(&points, center) {
862 let projected_distances: Vec<f64> = points
863 .iter()
864 .map(|p| {
865 let v = *p - center;
866 let along_axis = v.dot(axis_dir);
867 let radial = Vec3::new(
868 v.x() - axis_dir.x() * along_axis,
869 v.y() - axis_dir.y() * along_axis,
870 v.z() - axis_dir.z() * along_axis,
871 );
872 radial.length()
873 })
874 .collect();
875
876 let avg_r = projected_distances.iter().sum::<f64>() / np;
877 if avg_r > 1e-10 {
878 let r_tol = avg_r * 1e-3;
879 let is_cylinder = projected_distances
880 .iter()
881 .all(|d| (d - avg_r).abs() < r_tol);
882 if is_cylinder {
883 return DetectedSurfaceKind::Cylinder;
884 }
885 }
886 }
887
888 DetectedSurfaceKind::BSpline
889}
890
891fn estimate_cylinder_axis(points: &[Point3], center: Point3) -> Option<Vec3> {
894 let mut cxx = 0.0_f64;
896 let mut cxy = 0.0_f64;
897 let mut cxz = 0.0_f64;
898 let mut cyy = 0.0_f64;
899 let mut cyz = 0.0_f64;
900 let mut czz = 0.0_f64;
901
902 for p in points {
903 let dx = p.x() - center.x();
904 let dy = p.y() - center.y();
905 let dz = p.z() - center.z();
906 cxx += dx * dx;
907 cxy += dx * dy;
908 cxz += dx * dz;
909 cyy += dy * dy;
910 cyz += dy * dz;
911 czz += dz * dz;
912 }
913
914 let mut v = Vec3::new(1.0, 0.0, 0.0);
916 for _ in 0..20 {
917 let new_v = Vec3::new(
918 v.x().mul_add(cxx, v.y().mul_add(cxy, v.z() * cxz)),
919 v.x().mul_add(cxy, v.y().mul_add(cyy, v.z() * cyz)),
920 v.x().mul_add(cxz, v.y().mul_add(cyz, v.z() * czz)),
921 );
922 let len = new_v.length();
923 if len < 1e-15 {
924 return None;
925 }
926 v = Vec3::new(new_v.x() / len, new_v.y() / len, new_v.z() / len);
927 }
928 Some(v)
929}
930
931#[cfg(test)]
934mod tests {
935 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
936
937 use brepkit_math::surfaces::{
938 ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface,
939 };
940 use brepkit_math::vec::{Point3, Vec3};
941
942 use super::*;
943 use crate::convert::surface_to_nurbs::{
944 cone_to_nurbs, cylinder_to_nurbs, sphere_to_nurbs, torus_to_nurbs,
945 };
946
947 fn origin() -> Point3 {
948 Point3::new(0.0, 0.0, 0.0)
949 }
950
951 fn z_axis() -> Vec3 {
952 Vec3::new(0.0, 0.0, 1.0)
953 }
954
955 #[test]
956 fn recognize_cylinder_round_trip() {
957 let cyl = CylindricalSurface::new(origin(), z_axis(), 3.0).unwrap();
958 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
959
960 let result = recognize_surface(&nurbs, 1e-4);
961 match result {
962 RecognizedSurface::Cylinder { radius, .. } => {
963 assert!((radius - 3.0).abs() < 0.01, "radius {radius} != 3.0");
964 }
965 other => panic!("expected Cylinder, got {other:?}"),
966 }
967 }
968
969 #[test]
970 fn recognize_sphere_round_trip() {
971 let sphere = SphericalSurface::new(origin(), 5.0).unwrap();
972 let nurbs = sphere_to_nurbs(&sphere).unwrap();
973
974 let result = recognize_surface(&nurbs, 0.1);
975 match result {
976 RecognizedSurface::Sphere { center, radius } => {
977 let dist = Vec3::new(center.x(), center.y(), center.z()).length();
978 assert!(dist < 0.5, "center too far from origin: {dist}");
979 assert!((radius - 5.0).abs() < 0.5, "radius {radius} != 5.0");
980 }
981 other => panic!("expected Sphere, got {other:?}"),
982 }
983 }
984
985 #[test]
986 fn recognize_cone_round_trip() {
987 let half_angle = std::f64::consts::PI / 6.0;
991 let cone = ConicalSurface::new(origin(), z_axis(), half_angle).unwrap();
992 let nurbs = cone_to_nurbs(&cone, (1.0, 4.0)).unwrap();
993
994 match recognize_surface(&nurbs, 0.05) {
995 RecognizedSurface::Cone {
996 apex,
997 axis,
998 half_angle: ha,
999 } => {
1000 assert!(
1002 Vec3::new(apex.x(), apex.y(), apex.z()).length() < 0.05,
1003 "apex {apex:?}"
1004 );
1005 assert!(
1007 axis.dot(z_axis()).abs() > 1.0 - 1e-3,
1008 "axis {axis:?} not aligned with z"
1009 );
1010 assert!(
1011 (ha - half_angle).abs() < 1e-3,
1012 "half_angle {ha} vs {half_angle}"
1013 );
1014 }
1015 other => panic!("expected Cone, got {other:?}"),
1016 }
1017 }
1018
1019 #[test]
1020 fn cylinder_is_recognized_as_cylinder_not_cone() {
1021 let cyl = CylindricalSurface::new(origin(), z_axis(), 2.0).unwrap();
1024 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
1025 assert!(matches!(
1026 recognize_surface(&nurbs, 1e-4),
1027 RecognizedSurface::Cylinder { .. }
1028 ));
1029 }
1030
1031 #[test]
1032 fn recognize_torus_round_trip() {
1033 let torus = ToroidalSurface::new(origin(), 3.0, 0.5).unwrap();
1034 let nurbs = torus_to_nurbs(&torus).unwrap();
1035
1036 match recognize_surface(&nurbs, 0.05) {
1037 RecognizedSurface::Torus {
1038 center,
1039 axis,
1040 major_radius,
1041 minor_radius,
1042 } => {
1043 assert!(
1044 Vec3::new(center.x(), center.y(), center.z()).length() < 0.05,
1045 "center {center:?} not at origin"
1046 );
1047 assert!(
1048 axis.dot(z_axis()).abs() > 1.0 - 1e-3,
1049 "axis {axis:?} not aligned with z"
1050 );
1051 assert!(
1052 (major_radius - 3.0).abs() < 0.05,
1053 "major_radius {major_radius} vs 3.0"
1054 );
1055 assert!(
1056 (minor_radius - 0.5).abs() < 0.05,
1057 "minor_radius {minor_radius} vs 0.5"
1058 );
1059 }
1060 other => panic!("expected Torus, got {other:?}"),
1061 }
1062 }
1063
1064 #[test]
1068 fn recognize_a_tiny_plane() {
1069 use brepkit_math::nurbs::surface::NurbsSurface;
1070 let side = 1e-4;
1071 let base = Point3::new(1.0, 0.0, 0.0);
1072 let u = Vec3::new(-1.0, 1.0, 0.0).normalize().unwrap() * side;
1073 let v = Vec3::new(-1.0, -1.0, 2.0).normalize().unwrap() * side;
1074 let nurbs = NurbsSurface::new(
1075 1,
1076 1,
1077 vec![0.0, 0.0, 1.0, 1.0],
1078 vec![0.0, 0.0, 1.0, 1.0],
1079 vec![vec![base, base + v], vec![base + u, base + u + v]],
1080 vec![vec![1.0; 2]; 2],
1081 )
1082 .unwrap();
1083 match recognize_surface(&nurbs, 1e-7) {
1084 RecognizedSurface::Plane { normal, d } => {
1085 let expected = Vec3::new(1.0, 1.0, 1.0).normalize().unwrap();
1086 let sign = normal.dot(expected).signum();
1087 assert!(
1088 (normal * sign - expected).length() < 1e-9,
1089 "normal {normal:?}"
1090 );
1091 assert!((d * sign - expected.x()).abs() < 1e-12, "d {d}");
1092 }
1093 other => panic!("expected Plane, got {other:?}"),
1094 }
1095 }
1096
1097 #[test]
1103 fn recognize_a_plane_whose_first_points_crowd_together() {
1104 use brepkit_math::nurbs::surface::NurbsSurface;
1105 let (xs, ys) = ([0.0, 1e-6, 1.0], [0.0, 1e-4, 1.0]);
1106 let net: Vec<Vec<Point3>> = (0..3)
1107 .map(|i| {
1108 (0..3)
1109 .map(|j| {
1110 let z = if (i, j) == (1, 0) { 5e-8 } else { 0.0 };
1111 Point3::new(xs[i], ys[j], z)
1112 })
1113 .collect()
1114 })
1115 .collect();
1116 let nurbs = NurbsSurface::new(
1117 2,
1118 2,
1119 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
1120 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
1121 net,
1122 vec![vec![1.0; 3]; 3],
1123 )
1124 .unwrap();
1125 match recognize_surface(&nurbs, 1e-7) {
1126 RecognizedSurface::Plane { normal, d } => {
1127 assert!(normal.z().abs() > 1.0 - 1e-12, "normal {normal:?}");
1128 assert!(d.abs() < 1e-7, "d {d}");
1129 }
1130 other => panic!("expected Plane, got {other:?}"),
1131 }
1132 }
1133
1134 #[test]
1135 fn cylinder_is_not_recognized_as_torus() {
1136 let cyl = CylindricalSurface::new(origin(), z_axis(), 2.0).unwrap();
1139 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
1140 assert!(matches!(
1141 recognize_surface(&nurbs, 1e-4),
1142 RecognizedSurface::Cylinder { .. }
1143 ));
1144 }
1145}