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];
130 let mut normal: Option<Vec3> = None;
131 'outer: for i in 1..all_pts.len() {
132 let v1 = all_pts[i] - p0;
133 for pt in all_pts.iter().skip(i + 1) {
134 let v2 = *pt - p0;
135 let n = v1.cross(v2);
136 if n.length() > tolerance
137 && let Ok(normalized) = n.normalize()
138 {
139 normal = Some(normalized);
140 break 'outer;
141 }
142 }
143 }
144
145 let n = normal?;
146 let d = n.dot(Vec3::new(p0.x(), p0.y(), p0.z()));
147
148 for pt in &all_pts {
150 let dist = n.dot(Vec3::new(pt.x(), pt.y(), pt.z())) - d;
151 if dist.abs() > tolerance {
152 return None;
153 }
154 }
155
156 Some((n, d))
157}
158
159#[allow(clippy::items_after_statements)]
170fn try_recognize_cylinder(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64)> {
171 let cps = surface.control_points();
172 if cps.len() < 2 {
173 return None;
174 }
175 for row in cps {
176 if row.len() < 2 {
177 return None;
178 }
179 }
180
181 let mut axis_sum = Vec3::new(0.0, 0.0, 0.0);
183 for row in cps {
184 let v = row[row.len() - 1] - row[0];
185 axis_sum += v;
186 }
187 #[allow(clippy::cast_precision_loss)]
188 let axis_avg = axis_sum * (1.0 / cps.len() as f64);
189 let axis_len = axis_avg.length();
190 if axis_len < tolerance {
191 return None;
192 }
193 let axis = axis_avg.normalize().ok()?;
194
195 let (u0, u1) = surface.domain_u();
200 let (v0, v1) = surface.domain_v();
201 const N: usize = 8;
202
203 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
204 for iu in 0..N {
205 #[allow(clippy::cast_precision_loss)]
206 let u = u0 + (u1 - u0) * (iu as f64) / ((N - 1) as f64);
207 for iv in 0..N {
208 #[allow(clippy::cast_precision_loss)]
209 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
210 samples.push(surface.evaluate(u, v));
211 }
212 }
213
214 let ref_pt = samples[0];
220
221 let perp1 = {
223 let trial = if axis.x().abs() < 0.9 {
224 Vec3::new(1.0, 0.0, 0.0)
225 } else {
226 Vec3::new(0.0, 1.0, 0.0)
227 };
228 let p = trial - axis * axis.dot(trial);
229 p.normalize().unwrap_or(Vec3::new(1.0, 0.0, 0.0))
230 };
231 let perp2 = axis.cross(perp1);
232
233 let pts_2d: Vec<(f64, f64)> = samples
235 .iter()
236 .map(|pt| {
237 let v = *pt - ref_pt;
238 (perp1.dot(v), perp2.dot(v))
239 })
240 .collect();
241
242 let mut ata = [[0.0_f64; 3]; 3];
245 let mut atb = [0.0_f64; 3];
246 for &(x, y) in &pts_2d {
247 let rhs = x * x + y * y;
248 let row = [2.0 * x, 2.0 * y, 1.0];
249 for i in 0..3 {
250 for j in 0..3 {
251 ata[i][j] += row[i] * row[j];
252 }
253 atb[i] += row[i] * rhs;
254 }
255 }
256
257 let sol = solve_3x3(ata, atb)?;
258 let cx = sol[0];
259 let cy = sol[1];
260 let origin = ref_pt + perp1 * cx + perp2 * cy;
262
263 let mut radii: Vec<f64> = Vec::with_capacity(samples.len());
264 for pt in &samples {
265 let to_pt = *pt - origin;
266 let along = axis.dot(to_pt);
267 let radial = to_pt - axis * along;
268 radii.push(radial.length());
269 }
270
271 if radii.is_empty() {
272 return None;
273 }
274
275 let sum: f64 = radii.iter().sum();
276 #[allow(clippy::cast_precision_loss)]
277 let mean_radius = sum / radii.len() as f64;
278
279 if mean_radius < tolerance {
280 return None; }
282
283 let max_dev = radii
284 .iter()
285 .map(|r| (r - mean_radius).abs())
286 .fold(0.0_f64, f64::max);
287 if max_dev > tolerance {
288 return None;
289 }
290
291 Some((origin, axis, mean_radius))
292}
293
294#[allow(clippy::items_after_statements)]
301fn try_recognize_sphere(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, f64)> {
302 let (u0, u1) = surface.domain_u();
303 let (v0, v1) = surface.domain_v();
304 const N: usize = 8;
305
306 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
307
308 for iu in 0..N {
309 #[allow(clippy::cast_precision_loss)]
310 let u = u0 + (u1 - u0) * (iu as f64) / ((N - 1) as f64);
311 for iv in 0..N {
312 #[allow(clippy::cast_precision_loss)]
313 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
314 samples.push(surface.evaluate(u, v));
315 }
316 }
317
318 if samples.len() < 4 {
319 return None;
320 }
321
322 let sq = |p: Point3| p.x() * p.x() + p.y() * p.y() + p.z() * p.z();
326
327 let n = samples.len();
328 let mut ata = [[0.0_f64; 3]; 3];
329 let mut atb = [0.0_f64; 3];
330
331 let p0 = samples[0];
332 let sq0 = sq(p0);
333
334 for i in 1..n {
335 let pi = samples[i];
336 let a_row = [
337 2.0 * (pi.x() - p0.x()),
338 2.0 * (pi.y() - p0.y()),
339 2.0 * (pi.z() - p0.z()),
340 ];
341 let bi = sq(pi) - sq0;
342
343 for r in 0..3 {
344 for c in 0..3 {
345 ata[r][c] += a_row[r] * a_row[c];
346 }
347 atb[r] += a_row[r] * bi;
348 }
349 }
350
351 let center = solve_3x3(ata, atb)?;
352 let center_pt = Point3::new(center[0], center[1], center[2]);
353
354 let mut distances: Vec<f64> = Vec::with_capacity(n);
355 for pt in &samples {
356 let d = Vec3::new(
357 pt.x() - center_pt.x(),
358 pt.y() - center_pt.y(),
359 pt.z() - center_pt.z(),
360 )
361 .length();
362 distances.push(d);
363 }
364
365 let sum: f64 = distances.iter().sum();
366 #[allow(clippy::cast_precision_loss)]
367 let mean_radius = sum / distances.len() as f64;
368
369 if mean_radius < tolerance {
370 return None;
371 }
372
373 let max_dev = distances
374 .iter()
375 .map(|d| (d - mean_radius).abs())
376 .fold(0.0_f64, f64::max);
377
378 if max_dev > tolerance {
379 return None;
380 }
381
382 Some((center_pt, mean_radius))
383}
384
385fn try_recognize_cone(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64)> {
402 const N: usize = 8;
403 let cps = surface.control_points();
404 if cps.len() < 2 {
405 return None;
406 }
407 for row in cps {
408 if row.len() < 2 {
409 return None;
410 }
411 }
412
413 let n_rows = cps.len();
422 let row_count = if n_rows >= 3 && (cps[0][0] - cps[n_rows - 1][0]).length() < tolerance {
423 n_rows - 1
424 } else {
425 n_rows
426 };
427 let mut axis_sum = Vec3::new(0.0, 0.0, 0.0);
428 for row in cps.iter().take(row_count) {
429 let v = row[row.len() - 1] - row[0];
430 axis_sum += v;
431 }
432 #[allow(clippy::cast_precision_loss)]
433 let axis_avg = axis_sum * (1.0 / row_count as f64);
434 if axis_avg.length() < tolerance {
435 return None;
436 }
437 let axis = axis_avg.normalize().ok()?;
438
439 let (u0, u1) = surface.domain_u();
445 let (v0, v1) = surface.domain_v();
446 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
447 for iu in 0..N {
448 #[allow(clippy::cast_precision_loss)]
449 let u = u0 + (u1 - u0) * (iu as f64 + 0.5) / (N as f64);
450 for iv in 0..N {
451 #[allow(clippy::cast_precision_loss)]
452 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
453 samples.push(surface.evaluate(u, v));
454 }
455 }
456
457 #[allow(clippy::cast_precision_loss)]
464 let inv_n = 1.0 / samples.len() as f64;
465 let mut anchor_x = 0.0_f64;
466 let mut anchor_y = 0.0_f64;
467 let mut anchor_z = 0.0_f64;
468 for p in &samples {
469 anchor_x += p.x();
470 anchor_y += p.y();
471 anchor_z += p.z();
472 }
473 let anchor = Point3::new(anchor_x * inv_n, anchor_y * inv_n, anchor_z * inv_n);
474
475 let mut axials: Vec<f64> = Vec::with_capacity(samples.len());
481 let mut radials: Vec<f64> = Vec::with_capacity(samples.len());
482 for p in &samples {
483 let to_p = *p - anchor;
484 let along = axis.dot(to_p);
485 let radial_vec = to_p - axis * along;
486 axials.push(along);
487 radials.push(radial_vec.length());
488 }
489
490 let max_r = radials.iter().fold(0.0_f64, |m, &r| m.max(r));
493 let min_r = radials.iter().fold(f64::INFINITY, |m, &r| m.min(r));
494 if max_r - min_r < tolerance {
495 return None; }
497
498 let n_f = samples.len() as f64;
509 let sum_a: f64 = axials.iter().sum();
510 let sum_r: f64 = radials.iter().sum();
511 let mean_a = sum_a / n_f;
512 let mean_r = sum_r / n_f;
513 let mut s_aa = 0.0_f64;
514 let mut s_ar = 0.0_f64;
515 for i in 0..samples.len() {
516 let da = axials[i] - mean_a;
517 let dr = radials[i] - mean_r;
518 s_aa += da * da;
519 s_ar += da * dr;
520 }
521 if s_aa < 1e-30 {
522 return None;
523 }
524 let slope = s_ar / s_aa;
525 let intercept = mean_r - slope * mean_a;
526 if slope.abs() < tolerance {
527 return None; }
529 let axial_apex = -intercept / slope;
531
532 for i in 0..samples.len() {
534 let pred = slope * axials[i] + intercept;
535 if (radials[i] - pred).abs() > tolerance {
536 return None;
537 }
538 }
539
540 let half_angle = (1.0 / slope.abs()).atan();
549 if !(0.0 < half_angle && half_angle < std::f64::consts::FRAC_PI_2) {
550 return None;
551 }
552
553 let apex_offset = axis * axial_apex;
559 let apex = anchor + apex_offset;
560 let cone_axis = if slope > 0.0 { axis } else { -axis };
561
562 Some((apex, cone_axis, half_angle))
563}
564
565#[allow(clippy::items_after_statements)]
585fn try_recognize_torus(surface: &NurbsSurface, tolerance: f64) -> Option<(Point3, Vec3, f64, f64)> {
586 const N: usize = 8;
587 let cps = surface.control_points();
588 if cps.len() < 2 {
589 return None;
590 }
591 for row in cps {
592 if row.len() < 2 {
593 return None;
594 }
595 }
596
597 let n_rows = cps.len();
607 if n_rows < 3 {
608 return None;
609 }
610 let p0 = cps[0][0];
611 let mut axis: Option<Vec3> = None;
612 'outer: for i in 1..n_rows {
613 let v1 = cps[i][0] - p0;
614 for j in (i + 1)..n_rows {
615 let v2 = cps[j][0] - p0;
616 let cross = v1.cross(v2);
617 if cross.length() > tolerance
618 && let Ok(normalized) = cross.normalize()
619 {
620 axis = Some(normalized);
621 break 'outer;
622 }
623 }
624 }
625 let axis = axis?;
626
627 let (u0, u1) = surface.domain_u();
629 let (v0, v1) = surface.domain_v();
630 let mut samples: Vec<Point3> = Vec::with_capacity(N * N);
631 for iu in 0..N {
632 #[allow(clippy::cast_precision_loss)]
633 let u = u0 + (u1 - u0) * (iu as f64 + 0.5) / (N as f64);
634 for iv in 0..N {
635 #[allow(clippy::cast_precision_loss)]
636 let v = v0 + (v1 - v0) * (iv as f64) / ((N - 1) as f64);
637 samples.push(surface.evaluate(u, v));
638 }
639 }
640
641 #[allow(clippy::cast_precision_loss)]
643 let inv_n = 1.0 / samples.len() as f64;
644 let mut ax = 0.0_f64;
645 let mut ay = 0.0_f64;
646 let mut az = 0.0_f64;
647 for p in &samples {
648 ax += p.x();
649 ay += p.y();
650 az += p.z();
651 }
652 let anchor = Point3::new(ax * inv_n, ay * inv_n, az * inv_n);
653
654 let mut axials: Vec<f64> = Vec::with_capacity(samples.len());
656 let mut radials: Vec<f64> = Vec::with_capacity(samples.len());
657 for p in &samples {
658 let to_p = *p - anchor;
659 let along = axis.dot(to_p);
660 let radial = (to_p - axis * along).length();
661 axials.push(along);
662 radials.push(radial);
663 }
664
665 let mut ata = [[0.0_f64; 3]; 3];
669 let mut atb = [0.0_f64; 3];
670 for i in 0..samples.len() {
671 let x = axials[i];
672 let y = radials[i];
673 let row = [2.0 * x, 2.0 * y, 1.0];
674 let rhs = x * x + y * y;
675 for r in 0..3 {
676 for c in 0..3 {
677 ata[r][c] += row[r] * row[c];
678 }
679 atb[r] += row[r] * rhs;
680 }
681 }
682 let sol = solve_3x3(ata, atb)?;
683 let center_axial = sol[0];
684 let major_radius = sol[1];
685 let k = sol[2]; let r_sq = k + center_axial * center_axial + major_radius * major_radius;
687 if r_sq <= 0.0 || major_radius <= 0.0 {
688 return None;
689 }
690 let minor_radius = r_sq.sqrt();
691
692 if minor_radius >= major_radius - tolerance {
694 return None;
695 }
696
697 for i in 0..samples.len() {
699 let dx = axials[i] - center_axial;
700 let dy = radials[i] - major_radius;
701 let dist = (dx * dx + dy * dy).sqrt();
702 if (dist - minor_radius).abs() > tolerance {
703 return None;
704 }
705 }
706
707 let center = anchor + axis * center_axial;
708 Some((center, axis, major_radius, minor_radius))
709}
710
711pub(super) fn solve_3x3(a: [[f64; 3]; 3], b: [f64; 3]) -> Option<[f64; 3]> {
719 let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
720 - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
721 + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
722
723 if det.abs() < 1e-30 {
724 return None;
725 }
726
727 let inv = 1.0 / det;
728
729 let x0 = (b[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
730 - a[0][1] * (b[1] * a[2][2] - a[1][2] * b[2])
731 + a[0][2] * (b[1] * a[2][1] - a[1][1] * b[2]))
732 * inv;
733
734 let x1 = (a[0][0] * (b[1] * a[2][2] - a[1][2] * b[2])
735 - b[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
736 + a[0][2] * (a[1][0] * b[2] - b[1] * a[2][0]))
737 * inv;
738
739 let x2 = (a[0][0] * (a[1][1] * b[2] - b[1] * a[2][1])
740 - a[0][1] * (a[1][0] * b[2] - b[1] * a[2][0])
741 + b[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]))
742 * inv;
743
744 Some([x0, x1, x2])
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
752pub enum DetectedSurfaceKind {
753 Plane,
755 Sphere,
757 Cylinder,
759 BSpline,
761}
762
763impl DetectedSurfaceKind {
764 #[must_use]
766 pub const fn as_str(self) -> &'static str {
767 match self {
768 Self::Plane => "plane",
769 Self::Sphere => "sphere",
770 Self::Cylinder => "cylinder",
771 Self::BSpline => "bspline",
772 }
773 }
774}
775
776#[must_use]
784#[allow(clippy::cast_precision_loss)]
785pub fn detect_surface_kind(surface: &NurbsSurface) -> DetectedSurfaceKind {
786 let (u_min, u_max) = surface.domain_u();
787 let (v_min, v_max) = surface.domain_v();
788 let n = 8; let mut points = Vec::with_capacity(n * n);
791 for i in 0..n {
792 for j in 0..n {
793 let u = u_min + (u_max - u_min) * (i as f64) / ((n - 1) as f64);
794 let v = v_min + (v_max - v_min) * (j as f64) / ((n - 1) as f64);
795 points.push(surface.evaluate(u, v));
796 }
797 }
798
799 let mut cx = 0.0_f64;
801 let mut cy = 0.0_f64;
802 let mut cz = 0.0_f64;
803 for p in &points {
804 cx += p.x();
805 cy += p.y();
806 cz += p.z();
807 }
808 let np = points.len() as f64;
809 let center = Point3::new(cx / np, cy / np, cz / np);
810
811 let mut plane_normal = None;
814 for i in 1..points.len() {
815 for j in (i + 1)..points.len() {
816 let v0 = points[i] - center;
817 let v1 = points[j] - center;
818 let n = v0.cross(v1);
819 if let Ok(normalized) = n.normalize() {
820 plane_normal = Some(normalized);
821 break;
822 }
823 }
824 if plane_normal.is_some() {
825 break;
826 }
827 }
828 if let Some(normal) = plane_normal {
829 let is_plane = points
830 .iter()
831 .all(|p| (*p - center).dot(normal).abs() < 1e-6);
832 if is_plane {
833 return DetectedSurfaceKind::Plane;
834 }
835 }
836
837 let distances: Vec<f64> = points.iter().map(|p| (*p - center).length()).collect();
839 let avg_dist = distances.iter().sum::<f64>() / np;
840
841 if avg_dist < 1e-10 {
842 return DetectedSurfaceKind::BSpline;
843 }
844
845 let tol = avg_dist * 1e-3; let is_sphere = distances.iter().all(|d| (d - avg_dist).abs() < tol);
847
848 if is_sphere {
849 return DetectedSurfaceKind::Sphere;
850 }
851
852 if let Some(axis_dir) = estimate_cylinder_axis(&points, center) {
854 let projected_distances: Vec<f64> = points
855 .iter()
856 .map(|p| {
857 let v = *p - center;
858 let along_axis = v.dot(axis_dir);
859 let radial = Vec3::new(
860 v.x() - axis_dir.x() * along_axis,
861 v.y() - axis_dir.y() * along_axis,
862 v.z() - axis_dir.z() * along_axis,
863 );
864 radial.length()
865 })
866 .collect();
867
868 let avg_r = projected_distances.iter().sum::<f64>() / np;
869 if avg_r > 1e-10 {
870 let r_tol = avg_r * 1e-3;
871 let is_cylinder = projected_distances
872 .iter()
873 .all(|d| (d - avg_r).abs() < r_tol);
874 if is_cylinder {
875 return DetectedSurfaceKind::Cylinder;
876 }
877 }
878 }
879
880 DetectedSurfaceKind::BSpline
881}
882
883fn estimate_cylinder_axis(points: &[Point3], center: Point3) -> Option<Vec3> {
886 let mut cxx = 0.0_f64;
888 let mut cxy = 0.0_f64;
889 let mut cxz = 0.0_f64;
890 let mut cyy = 0.0_f64;
891 let mut cyz = 0.0_f64;
892 let mut czz = 0.0_f64;
893
894 for p in points {
895 let dx = p.x() - center.x();
896 let dy = p.y() - center.y();
897 let dz = p.z() - center.z();
898 cxx += dx * dx;
899 cxy += dx * dy;
900 cxz += dx * dz;
901 cyy += dy * dy;
902 cyz += dy * dz;
903 czz += dz * dz;
904 }
905
906 let mut v = Vec3::new(1.0, 0.0, 0.0);
908 for _ in 0..20 {
909 let new_v = Vec3::new(
910 v.x().mul_add(cxx, v.y().mul_add(cxy, v.z() * cxz)),
911 v.x().mul_add(cxy, v.y().mul_add(cyy, v.z() * cyz)),
912 v.x().mul_add(cxz, v.y().mul_add(cyz, v.z() * czz)),
913 );
914 let len = new_v.length();
915 if len < 1e-15 {
916 return None;
917 }
918 v = Vec3::new(new_v.x() / len, new_v.y() / len, new_v.z() / len);
919 }
920 Some(v)
921}
922
923#[cfg(test)]
926mod tests {
927 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
928
929 use brepkit_math::surfaces::{
930 ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface,
931 };
932 use brepkit_math::vec::{Point3, Vec3};
933
934 use super::*;
935 use crate::convert::surface_to_nurbs::{
936 cone_to_nurbs, cylinder_to_nurbs, sphere_to_nurbs, torus_to_nurbs,
937 };
938
939 fn origin() -> Point3 {
940 Point3::new(0.0, 0.0, 0.0)
941 }
942
943 fn z_axis() -> Vec3 {
944 Vec3::new(0.0, 0.0, 1.0)
945 }
946
947 #[test]
948 fn recognize_cylinder_round_trip() {
949 let cyl = CylindricalSurface::new(origin(), z_axis(), 3.0).unwrap();
950 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
951
952 let result = recognize_surface(&nurbs, 1e-4);
953 match result {
954 RecognizedSurface::Cylinder { radius, .. } => {
955 assert!((radius - 3.0).abs() < 0.01, "radius {radius} != 3.0");
956 }
957 other => panic!("expected Cylinder, got {other:?}"),
958 }
959 }
960
961 #[test]
962 fn recognize_sphere_round_trip() {
963 let sphere = SphericalSurface::new(origin(), 5.0).unwrap();
964 let nurbs = sphere_to_nurbs(&sphere).unwrap();
965
966 let result = recognize_surface(&nurbs, 0.1);
967 match result {
968 RecognizedSurface::Sphere { center, radius } => {
969 let dist = Vec3::new(center.x(), center.y(), center.z()).length();
970 assert!(dist < 0.5, "center too far from origin: {dist}");
971 assert!((radius - 5.0).abs() < 0.5, "radius {radius} != 5.0");
972 }
973 other => panic!("expected Sphere, got {other:?}"),
974 }
975 }
976
977 #[test]
978 fn recognize_cone_round_trip() {
979 let half_angle = std::f64::consts::PI / 6.0;
983 let cone = ConicalSurface::new(origin(), z_axis(), half_angle).unwrap();
984 let nurbs = cone_to_nurbs(&cone, (1.0, 4.0)).unwrap();
985
986 match recognize_surface(&nurbs, 0.05) {
987 RecognizedSurface::Cone {
988 apex,
989 axis,
990 half_angle: ha,
991 } => {
992 assert!(
994 Vec3::new(apex.x(), apex.y(), apex.z()).length() < 0.05,
995 "apex {apex:?}"
996 );
997 assert!(
999 axis.dot(z_axis()).abs() > 1.0 - 1e-3,
1000 "axis {axis:?} not aligned with z"
1001 );
1002 assert!(
1003 (ha - half_angle).abs() < 1e-3,
1004 "half_angle {ha} vs {half_angle}"
1005 );
1006 }
1007 other => panic!("expected Cone, got {other:?}"),
1008 }
1009 }
1010
1011 #[test]
1012 fn cylinder_is_recognized_as_cylinder_not_cone() {
1013 let cyl = CylindricalSurface::new(origin(), z_axis(), 2.0).unwrap();
1016 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
1017 assert!(matches!(
1018 recognize_surface(&nurbs, 1e-4),
1019 RecognizedSurface::Cylinder { .. }
1020 ));
1021 }
1022
1023 #[test]
1024 fn recognize_torus_round_trip() {
1025 let torus = ToroidalSurface::new(origin(), 3.0, 0.5).unwrap();
1026 let nurbs = torus_to_nurbs(&torus).unwrap();
1027
1028 match recognize_surface(&nurbs, 0.05) {
1029 RecognizedSurface::Torus {
1030 center,
1031 axis,
1032 major_radius,
1033 minor_radius,
1034 } => {
1035 assert!(
1036 Vec3::new(center.x(), center.y(), center.z()).length() < 0.05,
1037 "center {center:?} not at origin"
1038 );
1039 assert!(
1040 axis.dot(z_axis()).abs() > 1.0 - 1e-3,
1041 "axis {axis:?} not aligned with z"
1042 );
1043 assert!(
1044 (major_radius - 3.0).abs() < 0.05,
1045 "major_radius {major_radius} vs 3.0"
1046 );
1047 assert!(
1048 (minor_radius - 0.5).abs() < 0.05,
1049 "minor_radius {minor_radius} vs 0.5"
1050 );
1051 }
1052 other => panic!("expected Torus, got {other:?}"),
1053 }
1054 }
1055
1056 #[test]
1057 fn cylinder_is_not_recognized_as_torus() {
1058 let cyl = CylindricalSurface::new(origin(), z_axis(), 2.0).unwrap();
1061 let nurbs = cylinder_to_nurbs(&cyl, (0.0, 5.0)).unwrap();
1062 assert!(matches!(
1063 recognize_surface(&nurbs, 1e-4),
1064 RecognizedSurface::Cylinder { .. }
1065 ));
1066 }
1067}