1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::curves::{Circle3D, Ellipse3D};
11use crate::frame::Frame3;
12use crate::nurbs::fitting::interpolate;
13use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
14use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
15use crate::tolerance::Tolerance;
16use crate::vec::{Point3, Vec3};
17
18#[derive(Debug, Clone)]
20pub enum ExactIntersectionCurve {
21 Circle(Circle3D),
23 Ellipse(Ellipse3D),
25 Points(Vec<Point3>),
27}
28
29pub fn exact_plane_analytic(
40 surface: AnalyticSurface<'_>,
41 plane_normal: Vec3,
42 plane_d: f64,
43) -> Result<Vec<ExactIntersectionCurve>, MathError> {
44 match surface {
45 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
46 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
47 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d),
48 AnalyticSurface::Torus(torus) => {
49 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
51 Ok(chains
52 .into_iter()
53 .map(ExactIntersectionCurve::Points)
54 .collect())
55 }
56 }
57}
58
59fn exact_plane_cylinder(
65 cyl: &CylindricalSurface,
66 normal: Vec3,
67 d: f64,
68) -> Result<Vec<ExactIntersectionCurve>, MathError> {
69 let axis = cyl.axis();
70 let cos_theta = normal.dot(axis).abs();
71 let r = cyl.radius();
72
73 if cos_theta < 1e-10 {
74 let chains = sample_plane_cylinder(cyl, normal, d)?;
77 return Ok(chains
78 .into_iter()
79 .map(ExactIntersectionCurve::Points)
80 .collect());
81 }
82
83 let n_dot_axis = normal.dot(axis);
86 let n_dot_origin = dot_np(normal, cyl.origin());
87 let t = (d - n_dot_origin) / n_dot_axis;
88 let center_on_axis = Point3::new(
89 cyl.origin().x() + t * axis.x(),
90 cyl.origin().y() + t * axis.y(),
91 cyl.origin().z() + t * axis.z(),
92 );
93
94 if cos_theta > 1.0 - 1e-10 {
95 let circle = Circle3D::new(center_on_axis, normal, r)?;
97 Ok(vec![ExactIntersectionCurve::Circle(circle)])
98 } else {
99 let semi_minor = r;
103 let semi_major = r / cos_theta;
104
105 let axis_proj = Vec3::new(
109 axis.x() - n_dot_axis * normal.x(),
110 axis.y() - n_dot_axis * normal.y(),
111 axis.z() - n_dot_axis * normal.z(),
112 );
113 let u_axis = axis_proj.normalize()?;
114 let v_axis = normal.cross(u_axis);
115
116 let ellipse = Ellipse3D::with_axes(
117 center_on_axis,
118 normal,
119 semi_major,
120 semi_minor,
121 u_axis,
122 v_axis,
123 )?;
124 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
125 }
126}
127
128fn exact_plane_sphere(
132 sphere: &SphericalSurface,
133 normal: Vec3,
134 d: f64,
135) -> Result<Vec<ExactIntersectionCurve>, MathError> {
136 let h = dot_np(normal, sphere.center()) - d;
137 let r = sphere.radius();
138
139 if h.abs() > r - 1e-10 {
140 return Ok(vec![]);
141 }
142
143 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
144 let circle_center = Point3::new(
145 h.mul_add(-normal.x(), sphere.center().x()),
146 h.mul_add(-normal.y(), sphere.center().y()),
147 h.mul_add(-normal.z(), sphere.center().z()),
148 );
149
150 let circle = Circle3D::new(circle_center, normal, circle_r)?;
151 Ok(vec![ExactIntersectionCurve::Circle(circle)])
152}
153
154fn exact_plane_cone(
163 cone: &ConicalSurface,
164 normal: Vec3,
165 d: f64,
166) -> Result<Vec<ExactIntersectionCurve>, MathError> {
167 let axis = cone.axis();
168 let cos_theta = normal.dot(axis).abs();
169 let half_angle = cone.half_angle();
170
171 if cos_theta > 1.0 - 1e-10 {
172 let n_dot_axis = normal.dot(axis);
175 let n_dot_apex = dot_np(normal, cone.apex());
176 let t = (d - n_dot_apex) / n_dot_axis;
177
178 if t.abs() < 1e-10 {
183 return Ok(vec![]);
184 }
185
186 let center = Point3::new(
187 cone.apex().x() + t * axis.x(),
188 cone.apex().y() + t * axis.y(),
189 cone.apex().z() + t * axis.z(),
190 );
191 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
195 if circle_r < 1e-15 {
196 return Ok(vec![]);
197 }
198
199 let circle = Circle3D::new(center, normal, circle_r)?;
200 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
201 }
202
203 let c = normal.dot(axis);
215 let p2 = (1.0 - c * c).max(0.0);
216 let p = p2.sqrt();
217 let k = half_angle.sin().powi(2);
218 let a_coeff = p2 - k;
219
220 let m = Vec3::new(
222 axis.x() - c * normal.x(),
223 axis.y() - c * normal.y(),
224 axis.z() - c * normal.z(),
225 );
226 let m_len = m.length();
227 if m_len < 1e-12 {
228 let chains = sample_plane_cone(cone, normal, d)?;
231 return Ok(chains
232 .into_iter()
233 .map(ExactIntersectionCurve::Points)
234 .collect());
235 }
236 let e1 = m * (1.0 / m_len);
237 let e2 = normal.cross(e1);
238 let apex = cone.apex();
239 let e = d - dot_np(normal, apex);
240
241 if a_coeff < -1e-9 {
244 let abs_a = -a_coeff; if e * c < 0.0 {
251 return Ok(vec![]);
252 }
253 let s_c = e * c * p / abs_a;
256 let rhs = e * e * k * (1.0 - k) / abs_a;
257 if rhs <= 0.0 {
258 return Ok(vec![]);
259 }
260 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
263 return Ok(vec![]);
264 }
265 let center = apex + normal * e + e1 * s_c;
266 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
267 (semi_s, semi_t, e1, e2)
268 } else {
269 (semi_t, semi_s, e2, e1)
270 };
271 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
272 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
273 }
274
275 let chains = sample_plane_cone(cone, normal, d)?;
278 Ok(chains
279 .into_iter()
280 .map(ExactIntersectionCurve::Points)
281 .collect())
282}
283
284#[derive(Clone, Copy)]
286pub enum AnalyticSurface<'a> {
287 Cylinder(&'a CylindricalSurface),
289 Cone(&'a ConicalSurface),
291 Sphere(&'a SphericalSurface),
293 Torus(&'a ToroidalSurface),
295}
296
297fn dot_np(n: Vec3, p: Point3) -> f64 {
299 n.dot(Vec3::new(p.x(), p.y(), p.z()))
300}
301
302pub fn intersect_plane_analytic(
310 surface: AnalyticSurface<'_>,
311 normal: Vec3,
312 d: f64,
313) -> Result<Vec<IntersectionCurve>, MathError> {
314 match surface {
315 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
316 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
317 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
318 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
319 }
320}
321
322pub fn sample_plane_analytic(
333 surface: AnalyticSurface<'_>,
334 normal: Vec3,
335 d: f64,
336) -> Result<Vec<Vec<Point3>>, MathError> {
337 match surface {
338 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
339 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d),
340 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
341 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
342 }
343}
344
345#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
347fn sample_plane_cylinder(
348 cyl: &CylindricalSurface,
349 normal: Vec3,
350 d: f64,
351) -> Result<Vec<Vec<Point3>>, MathError> {
352 let n_samples = 64_usize;
353 let mut points = Vec::with_capacity(n_samples + 1);
354
355 for i in 0..=n_samples {
356 let u = TAU * (i as f64) / (n_samples as f64);
357 let base = cyl.evaluate(u, 0.0);
358 let n_dot_axis = normal.dot(cyl.axis());
359 let n_dot_base = dot_np(normal, base);
360
361 if n_dot_axis.abs() < 1e-12 {
362 if (n_dot_base - d).abs() < 1e-6 {
363 points.push(base);
364 }
365 } else {
366 let v = (d - n_dot_base) / n_dot_axis;
367 if v.abs() <= 100.0 {
368 points.push(cyl.evaluate(u, v));
369 }
370 }
371 }
372
373 if points.len() < 2 {
374 Ok(vec![])
375 } else {
376 Ok(vec![points])
377 }
378}
379
380#[allow(clippy::cast_precision_loss)]
382fn sample_plane_sphere(
383 sphere: &SphericalSurface,
384 normal: Vec3,
385 d: f64,
386) -> Result<Vec<Vec<Point3>>, MathError> {
387 let h = dot_np(normal, sphere.center()) - d;
388 let r = sphere.radius();
389
390 if h.abs() > r - 1e-10 {
391 return Ok(vec![]);
392 }
393
394 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
395 let circle_center = Point3::new(
396 h.mul_add(-normal.x(), sphere.center().x()),
397 h.mul_add(-normal.y(), sphere.center().y()),
398 h.mul_add(-normal.z(), sphere.center().z()),
399 );
400
401 let basis = Frame3::from_normal(circle_center, normal)?;
402 let u_dir = basis.x;
403 let v_dir = basis.y;
404
405 let n_samples = 64_usize;
406 let mut points = Vec::with_capacity(n_samples + 1);
407
408 for i in 0..=n_samples {
409 let theta = TAU * (i as f64) / (n_samples as f64);
410 let (sin_t, cos_t) = theta.sin_cos();
411 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
412 }
413
414 Ok(vec![points])
415}
416
417#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
429fn sample_plane_cone(
430 cone: &ConicalSurface,
431 normal: Vec3,
432 d: f64,
433) -> Result<Vec<Vec<Point3>>, MathError> {
434 let apex = cone.apex();
435 let n_dot_apex = dot_np(normal, apex);
436 let e = d - n_dot_apex;
437
438 let n_samples = 512_usize;
442 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
443 let mut v_min = f64::INFINITY;
444 for i in 0..n_samples {
445 let u = TAU * (i as f64) / (n_samples as f64);
446 let g = cone.evaluate(u, 1.0) - apex;
447 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
448 if n_dot_g.abs() < 1e-12 {
449 vs.push(None);
450 continue;
451 }
452 let v = e / n_dot_g;
453 if v >= -1e-12 {
454 let v = v.max(0.0);
455 v_min = v_min.min(v);
456 vs.push(Some(v));
457 } else {
458 vs.push(None);
459 }
460 }
461
462 if !v_min.is_finite() {
463 return Ok(Vec::new());
464 }
465
466 let v_max = (8.0 * v_min).max(v_min + 4.0);
473
474 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
477
478 let point_at = |u: f64, v: f64| -> Point3 {
479 let g = cone.evaluate(u, 1.0) - apex;
480 apex + g * v
481 };
482 #[allow(clippy::cast_precision_loss)]
483 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
484 let n_dot_g_at = |u: f64| -> f64 {
485 let g = cone.evaluate(u, 1.0) - apex;
486 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
487 };
488
489 if kept.iter().all(Option::is_some) {
490 let mut pts: Vec<Point3> = kept
492 .iter()
493 .enumerate()
494 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
495 .collect();
496 if let Some(&first) = pts.first() {
497 pts.push(first);
498 }
499 return Ok(vec![pts]);
500 }
501
502 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
511 let Some(v_end) = kept[i_end] else {
512 return Vec::new();
513 };
514 let u_end = u_of(i_end);
515 #[allow(clippy::cast_precision_loss)]
516 let pitch = TAU / (n_samples as f64);
517 let u_next = if forward {
518 u_end + pitch
519 } else {
520 u_end - pitch
521 };
522 let target = e / v_max;
523 let h_end = n_dot_g_at(u_end) - target;
524 let h_next = n_dot_g_at(u_next) - target;
525 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
526 return Vec::new();
527 }
528 let (mut lo, mut hi) = (u_end, u_next);
529 for _ in 0..60 {
530 let mid = f64::midpoint(lo, hi);
531 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
532 lo = mid;
533 } else {
534 hi = mid;
535 }
536 }
537 let u_star = f64::midpoint(lo, hi);
538 let tail_n = 8_usize;
539 (1..=tail_n)
540 .filter_map(|k| {
541 #[allow(clippy::cast_precision_loss)]
542 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
543 let ng = n_dot_g_at(u);
544 if ng.abs() < 1e-12 {
545 return None;
546 }
547 let v = e / ng;
548 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
549 })
550 .collect()
551 };
552
553 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
556 let mut chains: Vec<Vec<Point3>> = Vec::new();
557 let mut run: Vec<usize> = Vec::new();
558 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
559 if run.len() >= 2 {
560 let first = run[0];
561 let last = run[run.len() - 1];
562 let mut pts: Vec<Point3> = tail(first, false, &kept);
563 pts.reverse();
564 pts.extend(
565 run.iter()
566 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
567 );
568 pts.extend(tail(last, true, &kept));
569 chains.push(pts);
570 }
571 run.clear();
572 };
573 for k in 0..n_samples {
574 let idx = (gap + k) % n_samples;
575 if kept[idx].is_some() {
576 run.push(idx);
577 } else {
578 flush(&mut run, &mut chains);
579 }
580 }
581 flush(&mut run, &mut chains);
582 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
583}
584
585#[allow(clippy::cast_precision_loss)]
590fn sample_plane_torus(
591 torus: &ToroidalSurface,
592 normal: Vec3,
593 d: f64,
594) -> Result<Vec<Vec<Point3>>, MathError> {
595 let curves = intersect_plane_torus(torus, normal, d)?;
596 Ok(curves
597 .into_iter()
598 .map(|c| c.points.into_iter().map(|p| p.point).collect())
599 .collect())
600}
601
602#[allow(clippy::cast_precision_loss)]
612pub fn intersect_plane_cylinder(
613 cyl: &CylindricalSurface,
614 normal: Vec3,
615 d: f64,
616) -> Result<Vec<IntersectionCurve>, MathError> {
617 let n_samples = 64_usize;
618 let mut points_3d = Vec::new();
619 let mut ipoints = Vec::new();
620
621 for i in 0..=n_samples {
622 let u = TAU * (i as f64) / (n_samples as f64);
623 let base = cyl.evaluate(u, 0.0);
626 let n_dot_axis = normal.dot(cyl.axis());
627 let n_dot_base = dot_np(normal, base);
628
629 if n_dot_axis.abs() < 1e-12 {
630 if (n_dot_base - d).abs() < 1e-6 {
632 let pt = base;
633 points_3d.push(pt);
634 ipoints.push(IntersectionPoint {
635 point: pt,
636 param1: (u, 0.0),
637 param2: (0.0, 0.0),
638 });
639 }
640 } else {
641 let v = (d - n_dot_base) / n_dot_axis;
642 if v.abs() <= 100.0 {
644 let pt = cyl.evaluate(u, v);
645 points_3d.push(pt);
646 ipoints.push(IntersectionPoint {
647 point: pt,
648 param1: (u, v),
649 param2: (0.0, 0.0),
650 });
651 }
652 }
653 }
654
655 build_curves_from_points(&points_3d, ipoints)
656}
657
658#[allow(clippy::cast_precision_loss)]
667pub fn intersect_plane_sphere(
668 sphere: &SphericalSurface,
669 normal: Vec3,
670 d: f64,
671) -> Result<Vec<IntersectionCurve>, MathError> {
672 let h = dot_np(normal, sphere.center()) - d;
673 let r = sphere.radius();
674
675 if h.abs() > r - 1e-10 {
677 return Ok(vec![]);
678 }
679
680 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
681 let circle_center = Point3::new(
682 h.mul_add(-normal.x(), sphere.center().x()),
683 h.mul_add(-normal.y(), sphere.center().y()),
684 h.mul_add(-normal.z(), sphere.center().z()),
685 );
686
687 let basis = Frame3::from_normal(circle_center, normal)?;
689 let u_dir = basis.x;
690 let v_dir = basis.y;
691
692 let n_samples = 64_usize;
693 let mut points_3d = Vec::new();
694 let mut ipoints = Vec::new();
695
696 for i in 0..=n_samples {
697 let theta = TAU * (i as f64) / (n_samples as f64);
698 let (sin_t, cos_t) = theta.sin_cos();
699 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
700 points_3d.push(pt);
701 ipoints.push(IntersectionPoint {
702 point: pt,
703 param1: (theta, 0.0),
704 param2: (0.0, 0.0),
705 });
706 }
707
708 build_curves_from_points(&points_3d, ipoints)
709}
710
711#[allow(clippy::cast_precision_loss)]
720pub fn intersect_plane_cone(
721 cone: &ConicalSurface,
722 normal: Vec3,
723 d: f64,
724) -> Result<Vec<IntersectionCurve>, MathError> {
725 let n_samples = 64_usize;
726 let mut points_3d = Vec::new();
727 let mut ipoints = Vec::new();
728
729 for i in 0..n_samples {
730 let u = TAU * (i as f64) / (n_samples as f64);
731 let apex = cone.apex();
734 let n_dot_apex = dot_np(normal, apex);
735 let p1 = cone.evaluate(u, 1.0);
737 let dir = p1 - apex;
738 let n_dot_dir = normal.dot(dir);
739
740 if n_dot_dir.abs() < 1e-12 {
741 continue;
742 }
743
744 let v = (d - n_dot_apex) / n_dot_dir;
745 if v.abs() > 1e-10 && v.abs() < 100.0 {
747 let pt = cone.evaluate(u, v);
748 points_3d.push(pt);
749 ipoints.push(IntersectionPoint {
750 point: pt,
751 param1: (u, v),
752 param2: (0.0, 0.0),
753 });
754 }
755 }
756
757 build_curves_from_points(&points_3d, ipoints)
758}
759
760#[allow(
769 clippy::cast_precision_loss,
770 clippy::too_many_lines,
771 clippy::unnecessary_wraps
772)]
773pub fn intersect_plane_torus(
774 torus: &ToroidalSurface,
775 normal: Vec3,
776 d: f64,
777) -> Result<Vec<IntersectionCurve>, MathError> {
778 let n_grid = 128_usize;
779
780 let sdf = |u: f64, v: f64| -> f64 { dot_np(normal, torus.evaluate(u, v)) - d };
782
783 let mut crossing_pts: Vec<(f64, f64, Point3)> = Vec::new();
785
786 let du = TAU / (n_grid as f64);
787 let dv = TAU / (n_grid as f64);
788
789 let u_off = du * 0.5;
792 let v_off = dv * 0.5;
793
794 for iu in 0..n_grid {
795 for iv in 0..n_grid {
796 let u0 = (iu as f64).mul_add(du, u_off);
797 let v0 = (iv as f64).mul_add(dv, v_off);
798 let u1 = u0 + du;
799 let v1 = v0 + dv;
800
801 let f00 = sdf(u0, v0);
802 let f10 = sdf(u1, v0);
803 let f01 = sdf(u0, v1);
804
805 if f00 * f10 < 0.0 {
807 let t = f00 / (f00 - f10);
808 let u = t.mul_add(u1 - u0, u0);
809 let (u_r, v_r) = newton_refine_torus(torus, normal, d, u, v0);
810 crossing_pts.push((u_r, v_r, torus.evaluate(u_r, v_r)));
811 }
812
813 if f00 * f01 < 0.0 {
815 let t = f00 / (f00 - f01);
816 let v = t.mul_add(v1 - v0, v0);
817 let (u_r, v_r) = newton_refine_torus(torus, normal, d, u0, v);
818 crossing_pts.push((u_r, v_r, torus.evaluate(u_r, v_r)));
819 }
820 }
821 }
822
823 if crossing_pts.is_empty() {
824 return Ok(vec![]);
825 }
826
827 let mut used = vec![false; crossing_pts.len()];
829 let mut curves = Vec::new();
830
831 for start in 0..crossing_pts.len() {
832 if used[start] {
833 continue;
834 }
835 used[start] = true;
836 let mut chain = vec![start];
837
838 loop {
839 let last = chain[chain.len() - 1];
840 let last_pt = crossing_pts[last].2;
841 let mut best_idx = None;
842 let mut best_dist = 1.0_f64;
843
844 for (j, &is_used) in used.iter().enumerate() {
845 if is_used {
846 continue;
847 }
848 let dist = (crossing_pts[j].2 - last_pt).length();
849 if dist < best_dist {
850 best_dist = dist;
851 best_idx = Some(j);
852 }
853 }
854
855 if let Some(j) = best_idx {
856 used[j] = true;
857 chain.push(j);
858 } else {
859 break;
860 }
861 }
862
863 if chain.len() >= 4 {
864 let mut pts: Vec<Point3> = chain.iter().map(|&i| crossing_pts[i].2).collect();
865 let mut ipts: Vec<IntersectionPoint> = chain
866 .iter()
867 .map(|&i| IntersectionPoint {
868 point: crossing_pts[i].2,
869 param1: (crossing_pts[i].0, crossing_pts[i].1),
870 param2: (0.0, 0.0),
871 })
872 .collect();
873
874 let closing_gap = (pts[pts.len() - 1] - pts[0]).length();
886 let median_spacing = {
887 let mut spac: Vec<f64> = pts.windows(2).map(|w| (w[1] - w[0]).length()).collect();
888 spac.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
889 spac.get(spac.len() / 2).copied().unwrap_or(0.0)
890 };
891 let wrapped =
894 closing_gap > 1e-9 && median_spacing > 1e-12 && closing_gap <= 2.0 * median_spacing;
895 if wrapped {
896 pts.push(pts[0]);
897 ipts.push(ipts[0]);
898 }
899
900 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
901 curves.push(IntersectionCurve {
902 curve,
903 points: ipts,
904 });
905 }
906 }
907 }
908
909 Ok(curves)
910}
911
912fn newton_refine_torus(
914 torus: &ToroidalSurface,
915 normal: Vec3,
916 d: f64,
917 mut u: f64,
918 mut v: f64,
919) -> (f64, f64) {
920 let eps = 1e-6;
921 for _ in 0..10 {
922 let f = dot_np(normal, torus.evaluate(u, v)) - d;
923 if f.abs() < 1e-12 {
924 break;
925 }
926 let fu = (dot_np(normal, torus.evaluate(u + eps, v))
928 - dot_np(normal, torus.evaluate(u - eps, v)))
929 / (2.0 * eps);
930 let fv = (dot_np(normal, torus.evaluate(u, v + eps))
931 - dot_np(normal, torus.evaluate(u, v - eps)))
932 / (2.0 * eps);
933
934 let grad_sq = fu.mul_add(fu, fv * fv);
935 if grad_sq < 1e-20 {
936 break;
937 }
938 let step = f / grad_sq;
939 u -= step * fu;
940 v -= step * fv;
941 }
942 (u, v)
943}
944
945#[must_use]
958pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
959 let c = torus.center();
960 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
961 let big_r = torus.major_radius();
962 let small_r = torus.minor_radius();
963
964 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
966 let (a0, a1) = (xa.dot(o), xa.dot(dir));
967 let (b0, b1) = (ya.dot(o), ya.dot(dir));
968 let (c0, c1) = (za.dot(o), za.dot(dir));
969
970 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
972 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
973 let g0 = a0.mul_add(
974 a0,
975 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
976 );
977
978 let four_rr = 4.0 * big_r * big_r;
980 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
981 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
982 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
983
984 let e4 = g2 * g2;
986 let e3 = 2.0 * g2 * g1;
987 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
988 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
989 let e0 = g0.mul_add(g0, -h0);
990
991 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
992 let impl_f = |t: f64| -> f64 {
994 let p = origin + dir * t;
995 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
996 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
997 (a.hypot(b) - big_r).hypot(cc) - small_r
998 };
999 for t in &mut roots {
1000 let eps = 1e-7;
1001 let f = impl_f(*t);
1002 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1003 if df.abs() > 1e-12 {
1004 *t -= f / df;
1005 }
1006 }
1007 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1008 roots
1009}
1010
1011fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1014 if c4.abs() < 1e-14 {
1016 return real_roots_cubic(c3, c2, c1, c0);
1017 }
1018 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1020 let eval = |z: Complex| -> Complex {
1021 let mut acc = Complex::new(1.0, 0.0);
1023 acc = acc * z + Complex::new(a, 0.0);
1024 acc = acc * z + Complex::new(b, 0.0);
1025 acc = acc * z + Complex::new(c, 0.0);
1026 acc * z + Complex::new(d, 0.0)
1027 };
1028 let seed = Complex::new(0.4, 0.9);
1030 let mut r = [
1031 Complex::new(1.0, 0.0),
1032 seed,
1033 seed * seed,
1034 seed * seed * seed,
1035 ];
1036 for _ in 0..100 {
1037 let mut max_step = 0.0_f64;
1038 for i in 0..4 {
1039 let mut denom = Complex::new(1.0, 0.0);
1040 for j in 0..4 {
1041 if i != j {
1042 denom = denom * (r[i] - r[j]);
1043 }
1044 }
1045 if denom.norm() < 1e-300 {
1046 continue;
1047 }
1048 let step = eval(r[i]) / denom;
1049 r[i] = r[i] - step;
1050 max_step = max_step.max(step.norm());
1051 }
1052 if max_step < 1e-14 {
1053 break;
1054 }
1055 }
1056 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1063 let mut out: Vec<f64> = Vec::new();
1064 for z in r {
1065 if z.im.abs() >= 1e-7 {
1066 continue;
1067 }
1068 let x = z.re;
1069 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1072 if p_real(x).abs() > 1e-6 * scale {
1073 continue;
1074 }
1075 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1076 continue;
1077 }
1078 out.push(x);
1079 }
1080 out
1081}
1082
1083fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1085 if a.abs() < 1e-14 {
1086 return real_roots_quadratic(b, c, d);
1087 }
1088 let (b, c, d) = (b / a, c / a, d / a);
1090 let p = c - b * b / 3.0;
1091 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1092 let shift = -b / 3.0;
1093 let disc = q * q / 4.0 + p * p * p / 27.0;
1094 if disc > 1e-14 {
1095 let sq = disc.sqrt();
1096 let u = (-q / 2.0 + sq).cbrt();
1097 let v = (-q / 2.0 - sq).cbrt();
1098 vec![u + v + shift]
1099 } else if disc < -1e-14 {
1100 let m = 2.0 * (-p / 3.0).sqrt();
1102 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1103 (0..3)
1104 .map(|k| {
1105 m.mul_add(
1106 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1107 shift,
1108 )
1109 })
1110 .collect()
1111 } else {
1112 let u = (-q / 2.0).cbrt();
1114 vec![2.0 * u + shift, -u + shift]
1115 }
1116}
1117
1118fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1120 if a.abs() < 1e-14 {
1121 if b.abs() < 1e-14 {
1122 return Vec::new();
1123 }
1124 return vec![-c / b];
1125 }
1126 let disc = b * b - 4.0 * a * c;
1127 if disc < 0.0 {
1128 Vec::new()
1129 } else {
1130 let sq = disc.sqrt();
1131 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1132 }
1133}
1134
1135#[derive(Clone, Copy)]
1137struct Complex {
1138 re: f64,
1139 im: f64,
1140}
1141
1142impl Complex {
1143 const fn new(re: f64, im: f64) -> Self {
1144 Self { re, im }
1145 }
1146 fn norm(self) -> f64 {
1147 self.re.hypot(self.im)
1148 }
1149}
1150
1151impl std::ops::Add for Complex {
1152 type Output = Self;
1153 fn add(self, o: Self) -> Self {
1154 Self::new(self.re + o.re, self.im + o.im)
1155 }
1156}
1157
1158impl std::ops::Sub for Complex {
1159 type Output = Self;
1160 fn sub(self, o: Self) -> Self {
1161 Self::new(self.re - o.re, self.im - o.im)
1162 }
1163}
1164
1165impl std::ops::Mul for Complex {
1166 type Output = Self;
1167 fn mul(self, o: Self) -> Self {
1168 Self::new(
1169 self.re.mul_add(o.re, -(self.im * o.im)),
1170 self.re.mul_add(o.im, self.im * o.re),
1171 )
1172 }
1173}
1174
1175impl std::ops::Div for Complex {
1176 type Output = Self;
1177 fn div(self, o: Self) -> Self {
1178 let den = o.re.mul_add(o.re, o.im * o.im);
1179 Self::new(
1180 self.re.mul_add(o.re, self.im * o.im) / den,
1181 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1182 )
1183 }
1184}
1185
1186fn build_curves_from_points(
1190 points_3d: &[Point3],
1191 ipoints: Vec<IntersectionPoint>,
1192) -> Result<Vec<IntersectionCurve>, MathError> {
1193 if points_3d.len() < 2 {
1194 return Ok(vec![]);
1195 }
1196
1197 let degree = 3.min(points_3d.len() - 1);
1198 let curve = interpolate(points_3d, degree)?;
1199 Ok(vec![IntersectionCurve {
1200 curve,
1201 points: ipoints,
1202 }])
1203}
1204
1205#[allow(
1217 clippy::cast_precision_loss,
1218 clippy::too_many_lines,
1219 clippy::similar_names,
1220 clippy::unnecessary_wraps,
1221 clippy::type_complexity
1222)]
1223pub fn intersect_analytic_analytic(
1224 a: AnalyticSurface<'_>,
1225 b: AnalyticSurface<'_>,
1226 grid_res: usize,
1227) -> Result<Vec<IntersectionCurve>, MathError> {
1228 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1229}
1230
1231pub fn intersect_analytic_analytic_bounded(
1242 a: AnalyticSurface<'_>,
1243 b: AnalyticSurface<'_>,
1244 grid_res: usize,
1245 v_range_hint_a: Option<(f64, f64)>,
1246 v_range_hint_b: Option<(f64, f64)>,
1247) -> Result<Vec<IntersectionCurve>, MathError> {
1248 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1251 return Ok(result);
1252 }
1253
1254 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1255 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1256 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1257 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1258
1259 let diag_a = {
1261 let p00 = surf_a(u_range_a.0, v_range_a.0);
1262 let p11 = surf_a(u_range_a.1, v_range_a.1);
1263 (p00 - p11).length()
1264 };
1265 let diag_b = {
1266 let p00 = surf_b(u_range_b.0, v_range_b.0);
1267 let p11 = surf_b(u_range_b.1, v_range_b.1);
1268 (p00 - p11).length()
1269 };
1270 let char_size = diag_a.min(diag_b).max(0.1);
1271
1272 #[allow(clippy::type_complexity)]
1276 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1277 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1281 let mut min_dist = f64::INFINITY;
1282
1283 #[allow(clippy::cast_precision_loss)]
1284 for ia in 0..grid_res {
1285 for ja in 0..grid_res {
1286 let ua =
1287 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1288 let va =
1289 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1290
1291 let pa = surf_a(ua, va);
1292
1293 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1295 let pb = surf_b(ub, vb);
1296 let dist = (pa - pb).length();
1297 min_dist = min_dist.min(dist);
1298
1299 if dist < seed_threshold {
1300 let mid = Point3::new(
1305 (pa.x() + pb.x()) * 0.5,
1306 (pa.y() + pb.y()) * 0.5,
1307 (pa.z() + pb.z()) * 0.5,
1308 );
1309 seeds.push((mid, (ua, va), (ub, vb)));
1310 }
1311 }
1312 }
1313
1314 let reject_dist = (char_size / grid_res as f64) * 3.0;
1323 if min_dist > reject_dist {
1324 return Ok(vec![]);
1325 }
1326
1327 if seeds.is_empty() {
1328 return Ok(vec![]);
1329 }
1330
1331 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1335 let dedup_radius = march_step * 10.0;
1336 let mut unique_seeds = Vec::new();
1337 for seed in &seeds {
1338 let dominated = unique_seeds
1339 .iter()
1340 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1341 if !dominated {
1342 unique_seeds.push(*seed);
1343 }
1344 }
1345
1346 let mut curves = Vec::new();
1348 let mut used_seeds = vec![false; unique_seeds.len()];
1349
1350 for si in 0..unique_seeds.len() {
1351 if used_seeds[si] {
1352 continue;
1353 }
1354 used_seeds[si] = true;
1355
1356 let march_result = march_analytic_intersection(
1357 &a,
1358 &b,
1359 surf_a.as_ref(),
1360 norm_a.as_ref(),
1361 surf_b.as_ref(),
1362 norm_b.as_ref(),
1363 unique_seeds[si].0,
1364 u_range_a,
1365 v_range_a,
1366 u_range_b,
1367 v_range_b,
1368 march_step,
1369 is_u_periodic(&a),
1370 is_u_periodic(&b),
1371 );
1372
1373 if march_result.len() >= 2 {
1374 for (sj, other) in unique_seeds.iter().enumerate() {
1375 if !used_seeds[sj]
1376 && march_result
1377 .iter()
1378 .any(|p| (*p - other.0).length() < dedup_radius)
1379 {
1380 used_seeds[sj] = true;
1381 }
1382 }
1383
1384 let ipts: Vec<IntersectionPoint> = march_result
1385 .iter()
1386 .map(|&pt| IntersectionPoint {
1387 point: pt,
1388 param1: (0.0, 0.0),
1389 param2: (0.0, 0.0),
1390 })
1391 .collect();
1392
1393 let degree = 3.min(march_result.len() - 1);
1394 if let Ok(curve) = interpolate(&march_result, degree) {
1395 curves.push(IntersectionCurve {
1396 curve,
1397 points: ipts,
1398 });
1399 }
1400 }
1401 }
1402
1403 Ok(curves)
1404}
1405
1406#[allow(clippy::too_many_lines)]
1416fn try_algebraic_intersection(
1417 a: &AnalyticSurface<'_>,
1418 b: &AnalyticSurface<'_>,
1419 v_range_a: Option<(f64, f64)>,
1420 v_range_b: Option<(f64, f64)>,
1421) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1422 match (a, b) {
1423 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => {
1424 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)
1425 }
1426 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => {
1427 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)
1428 }
1429 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1430 algebraic_sphere_sphere(s1, s2).map(Some)
1431 }
1432 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1433 let axis_dot = c1.axis().dot(c2.axis()).abs();
1434 if axis_dot > 1.0 - 1e-10 {
1435 let delta = c2.origin() - c1.origin();
1437 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1438 let along = delta_vec.dot(c1.axis());
1439 let perp = (delta_vec - c1.axis() * along).length();
1440 if perp < 1e-8 {
1441 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1444 return Ok(None); }
1446 return Ok(Some(vec![])); }
1448 }
1449 algebraic_cylinder_cylinder(c1, c2)
1451 }
1452 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c))
1454 | (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1455 algebraic_sphere_cylinder(s, c)
1456 }
1457 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1458 _ => Ok(None),
1459 }
1460}
1461
1462pub fn exact_cone_cone(
1484 c1: &ConicalSurface,
1485 c2: &ConicalSurface,
1486) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1487 let axis = c1.axis();
1488 let axis2 = c2.axis();
1489
1490 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
1492 return Ok(None); }
1494 let apex1 = c1.apex();
1495 let apex2 = c2.apex();
1496 let delta = apex2 - apex1;
1497 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1498 let along = delta_v.dot(axis);
1499 if (delta_v - axis * along).length() > 1e-8 {
1500 return Ok(None); }
1502
1503 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
1504 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
1505 return Ok(None); }
1507 let m1 = c1.half_angle().cos() / s1;
1508 let m2 = c2.half_angle().cos() / s2;
1509 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
1510 let d2 = along; let denom = m1 - m2 * sigma;
1513 if denom.abs() < 1e-12 {
1514 if sigma > 0.0 && d2.abs() < 1e-9 {
1517 return Ok(None);
1518 }
1519 return Ok(Some(vec![]));
1520 }
1521
1522 let t_star = (-m2 * sigma * d2) / denom;
1523 let radius = m1 * t_star;
1524 if radius < 1e-12 {
1525 return Ok(Some(vec![])); }
1527
1528 let center = Point3::new(
1529 apex1.x() + axis.x() * t_star,
1530 apex1.y() + axis.y() * t_star,
1531 apex1.z() + axis.z() * t_star,
1532 );
1533 let circle = Circle3D::new(center, axis, radius)?;
1534 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
1535}
1536
1537pub fn exact_cone_cylinder(
1557 cone: &ConicalSurface,
1558 cyl: &CylindricalSurface,
1559) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1560 let axis = cone.axis();
1561 let cyl_axis = cyl.axis();
1562
1563 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
1565 return Ok(None);
1566 }
1567 let apex = cone.apex();
1568 let delta = apex - cyl.origin();
1569 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1570 let along = delta_v.dot(cyl_axis);
1571 if (delta_v - cyl_axis * along).length() > 1e-8 {
1572 return Ok(None);
1573 }
1574
1575 let s = cone.half_angle().sin();
1576 if s.abs() < 1e-12 {
1577 return Ok(None); }
1579 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
1581 return Ok(None); }
1583
1584 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
1586 return Ok(Some(vec![])); }
1588 let center = Point3::new(
1589 apex.x() + axis.x() * t_star,
1590 apex.y() + axis.y() * t_star,
1591 apex.z() + axis.z() * t_star,
1592 );
1593 let circle = Circle3D::new(center, axis, cyl.radius())?;
1594 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
1595}
1596
1597fn algebraic_cone_cone(
1605 c1: &ConicalSurface,
1606 c2: &ConicalSurface,
1607) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1608 let Some(exacts) = exact_cone_cone(c1, c2)? else {
1609 return Ok(None);
1610 };
1611 let mut curves = Vec::new();
1612 for exact in exacts {
1613 let ExactIntersectionCurve::Circle(circle) = exact else {
1614 continue;
1615 };
1616 let n_samples = 33;
1617 let mut positions = Vec::with_capacity(n_samples);
1618 let mut points = Vec::with_capacity(n_samples);
1619 #[allow(clippy::cast_precision_loss)]
1620 for i in 0..n_samples {
1621 let theta = TAU * i as f64 / (n_samples - 1) as f64;
1622 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
1623 positions.push(pt);
1624 points.push(IntersectionPoint {
1625 point: pt,
1626 param1: (0.0, 0.0),
1627 param2: (0.0, 0.0),
1628 });
1629 }
1630 let degree = 3.min(positions.len() - 1);
1631 let curve = interpolate(&positions, degree)?;
1632 curves.push(IntersectionCurve { curve, points });
1633 }
1634 Ok(Some(curves))
1635}
1636
1637pub fn exact_sphere_cylinder(
1657 sphere: &SphericalSurface,
1658 cyl: &CylindricalSurface,
1659) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1660 let sc = sphere.center();
1661 let r_sphere = sphere.radius();
1662 let co = cyl.origin();
1663 let axis = cyl.axis();
1664 let r_cyl = cyl.radius();
1665
1666 let delta = sc - co;
1668 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1669 let along = delta_vec.dot(axis);
1670 let perp_vec = delta_vec - axis * along;
1671 let d_perp = perp_vec.length();
1672
1673 if d_perp > 1e-7 {
1676 return Ok(None);
1677 }
1678
1679 if r_cyl > r_sphere + 1e-10 {
1682 return Ok(Some(vec![]));
1683 }
1684 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
1685 if z_sq < 0.0 {
1686 return Ok(Some(vec![]));
1687 }
1688 let z = z_sq.sqrt();
1689
1690 let center_axis_pt = Point3::new(
1693 co.x() + axis.x() * along,
1694 co.y() + axis.y() * along,
1695 co.z() + axis.z() * along,
1696 );
1697
1698 let mut circles = Vec::new();
1699 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
1700 for &z_offset in offsets {
1701 let center = Point3::new(
1702 center_axis_pt.x() + axis.x() * z_offset,
1703 center_axis_pt.y() + axis.y() * z_offset,
1704 center_axis_pt.z() + axis.z() * z_offset,
1705 );
1706 let circle = Circle3D::new(center, axis, r_cyl)?;
1707 circles.push(ExactIntersectionCurve::Circle(circle));
1708 }
1709 Ok(Some(circles))
1710}
1711
1712fn algebraic_sphere_cylinder(
1720 sphere: &SphericalSurface,
1721 cyl: &CylindricalSurface,
1722) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1723 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
1724 return Ok(None);
1725 };
1726
1727 let mut curves = Vec::new();
1728 for exact in exacts {
1729 let ExactIntersectionCurve::Circle(circle) = exact else {
1730 continue;
1731 };
1732 let n_samples = 33;
1733 let mut points = Vec::with_capacity(n_samples);
1734 let mut positions = Vec::with_capacity(n_samples);
1735 #[allow(clippy::cast_precision_loss)]
1736 for i in 0..n_samples {
1737 let theta = TAU * i as f64 / (n_samples - 1) as f64;
1738 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
1739 positions.push(pt);
1740 points.push(IntersectionPoint {
1741 point: pt,
1742 param1: (0.0, 0.0),
1743 param2: (0.0, 0.0),
1744 });
1745 }
1746 let degree = 3.min(positions.len() - 1);
1747 let curve = interpolate(&positions, degree)?;
1748 curves.push(IntersectionCurve { curve, points });
1749 }
1750
1751 Ok(Some(curves))
1752}
1753
1754#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
1768fn algebraic_cylinder_cylinder(
1769 c1: &CylindricalSurface,
1770 c2: &CylindricalSurface,
1771) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1772 let alpha = c1.axis().dot(c2.axis());
1773 let a_coeff = 1.0 - alpha * alpha;
1774
1775 if a_coeff.abs() < 1e-12 {
1777 return Ok(None);
1778 }
1779
1780 let r1 = c1.radius();
1781 let r2 = c2.radius();
1782 let o1 = c1.origin();
1783 let o2 = c2.origin();
1784 let a1 = c1.axis();
1785 let a2 = c2.axis();
1786 let x1 = c1.x_axis();
1787 let y1 = c1.y_axis();
1788
1789 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
1792 let cross = a1.cross(a2);
1793 let cross_len = cross.length();
1794 if cross_len > 1e-12 {
1795 let axis_dist = delta.dot(cross).abs() / cross_len;
1796 if axis_dist > r1 + r2 + Tolerance::new().linear {
1797 return Ok(Some(vec![])); }
1799 }
1800
1801 let n_samples = 128;
1806 let mut curve_plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
1807 let mut curve_minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
1808 let u_offset = TAU / (n_samples as f64 * 2.0); #[allow(clippy::cast_precision_loss)]
1813 for i in 0..n_samples {
1814 let u = u_offset + TAU * i as f64 / n_samples as f64;
1815 let (sin_u, cos_u) = u.sin_cos();
1816
1817 let qx = o1.x() + r1 * (cos_u * x1.x() + sin_u * y1.x()) - o2.x();
1820 let qy = o1.y() + r1 * (cos_u * x1.y() + sin_u * y1.y()) - o2.y();
1821 let qz = o1.z() + r1 * (cos_u * x1.z() + sin_u * y1.z()) - o2.z();
1822
1823 let q_dot_a1 = qx * a1.x() + qy * a1.y() + qz * a1.z();
1824 let q_dot_a2 = qx * a2.x() + qy * a2.y() + qz * a2.z();
1825 let q_sq = qx * qx + qy * qy + qz * qz;
1826
1827 let b_coeff = 2.0 * (q_dot_a1 - alpha * q_dot_a2);
1828 let c_coeff = q_sq - q_dot_a2 * q_dot_a2 - r2 * r2;
1829
1830 let disc = b_coeff * b_coeff - 4.0 * a_coeff * c_coeff;
1831 if disc < -Tolerance::new().linear {
1834 continue;
1835 }
1836
1837 let sqrt_disc = disc.max(0.0).sqrt();
1838 let v_plus = (-b_coeff + sqrt_disc) / (2.0 * a_coeff);
1839 let v_minus = (-b_coeff - sqrt_disc) / (2.0 * a_coeff);
1840
1841 curve_plus.push(c1.evaluate(u, v_plus));
1842 curve_minus.push(c1.evaluate(u, v_minus));
1843 }
1844
1845 if !curve_plus.is_empty() {
1848 curve_plus.push(curve_plus[0]);
1849 }
1850 if !curve_minus.is_empty() {
1851 curve_minus.push(curve_minus[0]);
1852 }
1853
1854 let mut curves = Vec::new();
1855
1856 for pts in [&curve_plus, &curve_minus] {
1857 if pts.len() < 4 {
1858 continue;
1859 }
1860
1861 let ipts: Vec<IntersectionPoint> = pts
1862 .iter()
1863 .map(|&p| {
1864 let (u1, v1) = c1.project_point(p);
1865 let (u2, v2) = c2.project_point(p);
1866 IntersectionPoint {
1867 point: p,
1868 param1: (u1, v1),
1869 param2: (u2, v2),
1870 }
1871 })
1872 .collect();
1873
1874 let degree = 3.min(pts.len() - 1);
1875 if let Ok(curve) = interpolate(pts, degree) {
1876 curves.push(IntersectionCurve {
1877 curve,
1878 points: ipts,
1879 });
1880 }
1881 }
1882
1883 Ok(Some(curves))
1884}
1885
1886#[allow(clippy::unnecessary_wraps)]
1912fn algebraic_parallel_cone_cylinder(
1913 cone: &ConicalSurface,
1914 cyl: &CylindricalSurface,
1915 v_range_cone: Option<(f64, f64)>,
1916 v_range_cyl: Option<(f64, f64)>,
1917) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1918 let axis = cone.axis();
1919 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
1920 return Ok(None); }
1922
1923 let apex = cone.apex();
1924 let delta = cyl.origin() - apex;
1925 let along = delta.dot(axis);
1926 let perp = delta - axis * along;
1927 let d = perp.length();
1928 if d < 1e-9 {
1929 return Ok(None); }
1931
1932 let (e1, e2) = (cone.x_axis(), cone.y_axis());
1933 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
1934
1935 let (sin_t, cos_t) = cone.half_angle().sin_cos();
1936 if cos_t < 1e-12 || sin_t < 1e-12 {
1937 return Ok(None);
1938 }
1939 let r = cyl.radius();
1940
1941 let mut v_min = (d - r).abs() / cos_t;
1943 let mut v_max = (d + r) / cos_t;
1944 if v_max <= v_min {
1945 return Ok(Some(vec![]));
1946 }
1947
1948 let mut lo = v_min;
1954 let mut hi = v_max;
1955 if let Some((a, b)) = v_range_cone {
1960 let (a, b) = if a <= b { (a, b) } else { (b, a) };
1961 lo = lo.max(a);
1962 hi = hi.min(b);
1963 }
1964 if let Some((a, b)) = v_range_cyl {
1965 let flip = cyl.axis().dot(axis);
1968 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
1969 let (a, b) = (to_cone_v(a), to_cone_v(b));
1970 let (a, b) = if a <= b { (a, b) } else { (b, a) };
1971 lo = lo.max(a);
1972 hi = hi.min(b);
1973 }
1974 v_min = lo.max(v_min);
1975 v_max = hi.min(v_max);
1976 if v_max - v_min <= 1e-12 {
1977 return Ok(Some(vec![]));
1978 }
1979
1980 let n_samples = 128;
1981 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
1982 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
1983 #[allow(clippy::cast_precision_loss)]
1984 for i in 0..=n_samples {
1985 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
1986 let rho = v * cos_t;
1987 if rho < 1e-12 {
1988 if (d - r).abs() < 1e-12 {
1996 let apex = cone.evaluate(phi0, v);
1997 plus.push(apex);
1998 minus.push(apex);
1999 }
2000 continue;
2001 }
2002 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
2003 let alpha = cos_alpha.acos();
2004 plus.push(cone.evaluate(phi0 + alpha, v));
2005 minus.push(cone.evaluate(phi0 - alpha, v));
2006 }
2007
2008 let mut curves = Vec::new();
2009 for pts in [&plus, &minus] {
2010 if pts.len() < 4 {
2013 continue;
2014 }
2015 let ipts: Vec<IntersectionPoint> = pts
2016 .iter()
2017 .map(|&p| IntersectionPoint {
2018 point: p,
2019 param1: cone.project_point(p),
2020 param2: cyl.project_point(p),
2021 })
2022 .collect();
2023 let degree = 3.min(pts.len() - 1);
2024 match interpolate(pts, degree) {
2025 Ok(curve) => curves.push(IntersectionCurve {
2026 curve,
2027 points: ipts,
2028 }),
2029 Err(_) => return Ok(None),
2034 }
2035 }
2036
2037 Ok(Some(curves))
2038}
2039
2040fn algebraic_sphere_sphere(
2048 s1: &SphericalSurface,
2049 s2: &SphericalSurface,
2050) -> Result<Vec<IntersectionCurve>, MathError> {
2051 let c1 = s1.center();
2052 let c2 = s2.center();
2053 let r1 = s1.radius();
2054 let r2 = s2.radius();
2055
2056 let delta = c2 - c1;
2057 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
2058 let d = d_sq.sqrt();
2059
2060 if d < 1e-12 {
2061 return Ok(vec![]);
2063 }
2064
2065 if d > r1 + r2 + 1e-10 {
2067 return Ok(vec![]); }
2069 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
2070 return Ok(vec![]); }
2072
2073 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
2075
2076 let r_circle_sq = r1 * r1 - d1 * d1;
2078 if r_circle_sq < 0.0 {
2079 if r_circle_sq > -1e-10 {
2081 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2083 let tangent_pt = Point3::new(
2084 c1.x() + axis.x() * d1,
2085 c1.y() + axis.y() * d1,
2086 c1.z() + axis.z() * d1,
2087 );
2088 let ipt = IntersectionPoint {
2089 point: tangent_pt,
2090 param1: (0.0, 0.0),
2091 param2: (0.0, 0.0),
2092 };
2093 return Ok(vec![IntersectionCurve {
2095 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
2096 points: vec![ipt],
2097 }]);
2098 }
2099 return Ok(vec![]);
2100 }
2101
2102 let r_circle = r_circle_sq.sqrt();
2103 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2104 let center = Point3::new(
2105 c1.x() + axis.x() * d1,
2106 c1.y() + axis.y() * d1,
2107 c1.z() + axis.z() * d1,
2108 );
2109
2110 let basis = Frame3::from_normal(center, axis)?;
2112 let u_dir = basis.x;
2113 let v_dir = basis.y;
2114
2115 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
2118 let mut positions = Vec::with_capacity(n_samples);
2119 #[allow(clippy::cast_precision_loss)]
2120 for i in 0..n_samples {
2121 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2122 let (sin_t, cos_t) = theta.sin_cos();
2123 let pt = Point3::new(
2124 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
2125 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
2126 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
2127 );
2128 positions.push(pt);
2129 points.push(IntersectionPoint {
2130 point: pt,
2131 param1: (0.0, 0.0),
2132 param2: (0.0, 0.0),
2133 });
2134 }
2135
2136 let degree = 3.min(positions.len() - 1);
2137 let curve = interpolate(&positions, degree)?;
2138
2139 Ok(vec![IntersectionCurve { curve, points }])
2140}
2141
2142#[allow(clippy::too_many_arguments)]
2148fn correct_to_intersection(
2149 a: &AnalyticSurface<'_>,
2150 b: &AnalyticSurface<'_>,
2151 surf_a: &dyn Fn(f64, f64) -> Point3,
2152 norm_a: &dyn Fn(f64, f64) -> Vec3,
2153 surf_b: &dyn Fn(f64, f64) -> Point3,
2154 norm_b: &dyn Fn(f64, f64) -> Vec3,
2155 point: Point3,
2156 u_range_a: (f64, f64),
2157 v_range_a: (f64, f64),
2158 u_range_b: (f64, f64),
2159 v_range_b: (f64, f64),
2160 max_iters: usize,
2161) -> Point3 {
2162 let mut p = point;
2163 for _ in 0..max_iters {
2164 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
2165 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
2166 let pa = surf_a(ua, va);
2167 let pb = surf_b(ub, vb);
2168 let na = norm_a(ua, va);
2169 let nb = norm_b(ub, vb);
2170 let pv = Vec3::new(p.x(), p.y(), p.z());
2171
2172 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
2173 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
2174
2175 if da.abs() < 1e-7 && db.abs() < 1e-7 {
2176 break;
2177 }
2178
2179 let t = na.cross(nb);
2180 let t_len = t.length();
2181 if t_len < 1e-10 {
2182 return Point3::new(
2184 (pa.x() + pb.x()) * 0.5,
2185 (pa.y() + pb.y()) * 0.5,
2186 (pa.z() + pb.z()) * 0.5,
2187 );
2188 }
2189 let t_hat = t * (1.0 / t_len);
2190
2191 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2193 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2194 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
2195 if det.abs() < 1e-15 {
2196 return Point3::new(
2197 (pa.x() + pb.x()) * 0.5,
2198 (pa.y() + pb.y()) * 0.5,
2199 (pa.z() + pb.z()) * 0.5,
2200 );
2201 }
2202 let inv = 1.0 / det;
2203 let dx = inv
2205 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2206 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
2207 let dy = inv
2208 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2209 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
2210 let dz = inv
2211 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
2212 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
2213 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
2214
2215 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
2218 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
2219 let pc_a = surf_a(uc, vc);
2220 let pc_b = surf_b(ud, vd);
2221 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
2222 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
2223 .dot(norm_a(uc, vc))
2224 .abs();
2225 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
2226 .dot(norm_b(ud, vd))
2227 .abs();
2228 if da_new > da.abs() && db_new > db.abs() {
2229 return p;
2230 }
2231
2232 p = candidate;
2233 }
2234 p
2235}
2236
2237#[allow(clippy::too_many_arguments)]
2243fn march_analytic_intersection(
2244 a: &AnalyticSurface<'_>,
2245 b: &AnalyticSurface<'_>,
2246 surf_a: &dyn Fn(f64, f64) -> Point3,
2247 norm_a: &dyn Fn(f64, f64) -> Vec3,
2248 surf_b: &dyn Fn(f64, f64) -> Point3,
2249 norm_b: &dyn Fn(f64, f64) -> Vec3,
2250 seed: Point3,
2251 u_range_a: (f64, f64),
2252 v_range_a: (f64, f64),
2253 u_range_b: (f64, f64),
2254 v_range_b: (f64, f64),
2255 initial_step: f64,
2256 u_periodic_a: bool,
2257 u_periodic_b: bool,
2258) -> Vec<Point3> {
2259 let max_steps = 500;
2260 let h_min = 1e-6;
2261 let h_max = initial_step * 4.0;
2262 let closure_dist = initial_step * 5.0;
2266 let max_angle = 10.0_f64.to_radians();
2268 let min_angle = 2.0_f64.to_radians();
2269
2270 let mut forward = Vec::new();
2272 let mut backward = Vec::new();
2274
2275 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
2276 let mut current = seed;
2277 let mut h = initial_step;
2278 let mut prev_tangent: Option<Vec3> = None;
2279
2280 for _ in 0..max_steps {
2281 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
2282 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
2283
2284 let na = norm_a(ua, va);
2285 let nb = norm_b(ub, vb);
2286
2287 let tangent = na.cross(nb);
2288 let t_len = tangent.length();
2289 if t_len < 1e-10 {
2290 break;
2291 }
2292 let t_dir = tangent * (direction / t_len);
2293
2294 if let Some(prev_t) = prev_tangent {
2296 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
2297 let angle = cos_angle.acos();
2298 if angle > max_angle && h > h_min {
2299 h = (h * 0.5).max(h_min);
2300 } else if angle < min_angle {
2301 h = (h * 2.0).min(h_max);
2302 }
2303 }
2304 prev_tangent = Some(t_dir);
2305
2306 let next = Point3::new(
2307 h.mul_add(t_dir.x(), current.x()),
2308 h.mul_add(t_dir.y(), current.y()),
2309 h.mul_add(t_dir.z(), current.z()),
2310 );
2311
2312 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
2313 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
2314
2315 let pa = surf_a(ua2, va2);
2316 let pb = surf_b(ub2, vb2);
2317 let mid = Point3::new(
2318 (pa.x() + pb.x()) * 0.5,
2319 (pa.y() + pb.y()) * 0.5,
2320 (pa.z() + pb.z()) * 0.5,
2321 );
2322 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
2323 || va2 <= v_range_a.0
2324 || va2 >= v_range_a.1;
2325 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
2326 || vb2 <= v_range_b.0
2327 || vb2 >= v_range_b.1;
2328
2329 if out_a || out_b {
2330 break;
2331 }
2332
2333 let dist_to_seed = (mid - seed).length();
2337 if points.len() > 10 && dist_to_seed < closure_dist {
2338 points.push(seed);
2339 break;
2340 }
2341
2342 points.push(mid);
2343 current = mid;
2344 }
2345 }
2346
2347 backward.reverse();
2349 let mut result = backward;
2350 result.push(seed);
2351 result.append(&mut forward);
2352
2353 for pt in &mut result {
2355 *pt = correct_to_intersection(
2356 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
2357 5,
2358 );
2359 }
2360
2361 result
2362}
2363
2364fn project_analytic(
2368 surface: &AnalyticSurface<'_>,
2369 point: Point3,
2370 u_range: (f64, f64),
2371 v_range: (f64, f64),
2372) -> (f64, f64) {
2373 match surface {
2374 AnalyticSurface::Cylinder(cyl) => {
2375 let (u, v) = cyl.project_point(point);
2376 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2377 }
2378 AnalyticSurface::Sphere(sphere) => {
2379 let (u, v) = sphere.project_point(point);
2380 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2381 }
2382 AnalyticSurface::Cone(cone) => {
2383 let (u, v) = cone.project_point(point);
2384 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2385 }
2386 AnalyticSurface::Torus(torus) => {
2387 let (u, v) = torus.project_point(point);
2388 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2389 }
2390 }
2391}
2392
2393fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
2397 matches!(
2398 surface,
2399 AnalyticSurface::Cylinder(_)
2400 | AnalyticSurface::Cone(_)
2401 | AnalyticSurface::Sphere(_)
2402 | AnalyticSurface::Torus(_)
2403 )
2404}
2405
2406#[allow(clippy::type_complexity)]
2408fn surface_closures<'a>(
2409 surface: &'a AnalyticSurface<'a>,
2410) -> (
2411 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
2412 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
2413 (f64, f64),
2414 (f64, f64),
2415) {
2416 match surface {
2417 AnalyticSurface::Cylinder(cyl) => (
2418 Box::new(|u, v| cyl.evaluate(u, v)),
2419 Box::new(|u, v| cyl.normal(u, v)),
2420 (0.0, TAU),
2421 (-1.0, 1.0),
2422 ),
2423 AnalyticSurface::Cone(cone) => (
2424 Box::new(|u, v| cone.evaluate(u, v)),
2425 Box::new(|u, v| cone.normal(u, v)),
2426 (0.0, TAU),
2427 (0.01, 2.0),
2428 ),
2429 AnalyticSurface::Sphere(sphere) => (
2430 Box::new(|u, v| sphere.evaluate(u, v)),
2431 Box::new(|u, v| sphere.normal(u, v)),
2432 (0.0, TAU),
2433 (-FRAC_PI_2, FRAC_PI_2),
2434 ),
2435 AnalyticSurface::Torus(torus) => (
2436 Box::new(|u, v| torus.evaluate(u, v)),
2437 Box::new(|u, v| torus.normal(u, v)),
2438 (0.0, TAU),
2439 (0.0, TAU),
2440 ),
2441 }
2442}
2443
2444#[cfg(test)]
2445#[allow(clippy::unwrap_used, clippy::expect_used)]
2446mod tests {
2447 use super::*;
2448 use crate::tolerance::Tolerance;
2449
2450 #[test]
2451 fn plane_cylinder_perpendicular() {
2452 let cyl =
2453 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
2454 .unwrap();
2455
2456 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
2458 assert!(!curves.is_empty(), "should find intersection curve");
2459 assert!(
2460 curves[0].points.len() > 10,
2461 "should have many sample points"
2462 );
2463
2464 let tol = Tolerance::loose();
2465 for pt in &curves[0].points {
2466 assert!(
2467 tol.approx_eq(pt.point.z(), 3.0),
2468 "z should be ~3.0, got {}",
2469 pt.point.z()
2470 );
2471 let r = pt.point.x().hypot(pt.point.y());
2472 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
2473 }
2474 }
2475
2476 #[test]
2477 fn plane_sphere_equator() {
2478 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
2479
2480 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
2481 assert!(!curves.is_empty());
2482
2483 let tol = Tolerance::loose();
2484 for pt in &curves[0].points {
2485 assert!(
2486 tol.approx_eq(pt.point.z(), 0.0),
2487 "z should be ~0, got {}",
2488 pt.point.z()
2489 );
2490 let r = pt.point.x().hypot(pt.point.y());
2491 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
2492 }
2493 }
2494
2495 #[test]
2496 fn plane_sphere_no_intersection() {
2497 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
2498
2499 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
2500 assert!(curves.is_empty());
2501 }
2502
2503 #[test]
2504 fn plane_cone_cross_section() {
2505 let cone = ConicalSurface::new(
2506 Point3::new(0.0, 0.0, 0.0),
2507 Vec3::new(0.0, 0.0, 1.0),
2508 std::f64::consts::FRAC_PI_4,
2509 )
2510 .unwrap();
2511
2512 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
2513 assert!(!curves.is_empty(), "should find intersection with cone");
2514 }
2515
2516 #[test]
2517 fn coaxial_cones_cross_at_single_circle() {
2518 let outer = ConicalSurface::new(
2523 Point3::new(0.0, 0.0, 50.0),
2524 Vec3::new(0.0, 0.0, -1.0),
2525 5.0_f64.atan(),
2526 )
2527 .unwrap();
2528 let inner = ConicalSurface::new(
2529 Point3::new(0.0, 0.0, 90.0),
2530 Vec3::new(0.0, 0.0, -1.0),
2531 10.0_f64.atan(),
2532 )
2533 .unwrap();
2534
2535 let curves = intersect_analytic_analytic_bounded(
2536 AnalyticSurface::Cone(&outer),
2537 AnalyticSurface::Cone(&inner),
2538 32,
2539 None,
2540 None,
2541 )
2542 .unwrap();
2543
2544 assert_eq!(
2545 curves.len(),
2546 1,
2547 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
2548 curves.len()
2549 );
2550 for p in &curves[0].points {
2551 let r = p.point.x().hypot(p.point.y());
2552 assert!(
2553 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
2554 "intersection point off the expected z=10,r=8 circle: {:?}",
2555 p.point
2556 );
2557 }
2558 }
2559
2560 #[test]
2561 fn plane_torus_cross_section() {
2562 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
2563
2564 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
2565 assert!(
2566 !curves.is_empty(),
2567 "should find intersection curves with torus"
2568 );
2569 }
2570
2571 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
2574 let rho = p.x().hypot(p.y());
2575 ((rho - major).hypot(p.z())) - minor
2576 }
2577
2578 #[test]
2584 fn parallel_cone_cylinder_gives_two_exact_branches() {
2585 use crate::traits::ParametricCurve;
2586 let cone = ConicalSurface::new(
2587 Point3::new(-5.45, -36.55, -4.85),
2588 Vec3::new(0.0, 0.0, 1.0),
2589 std::f64::consts::FRAC_PI_4,
2590 )
2591 .unwrap();
2592 let cyl = CylindricalSurface::new(
2593 Point3::new(-8.0, -34.0, -5.0),
2594 Vec3::new(0.0, 0.0, 1.0),
2595 4.45,
2596 )
2597 .unwrap();
2598 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
2600 let curves = intersect_analytic_analytic_bounded(
2601 AnalyticSurface::Cone(&cone),
2602 AnalyticSurface::Cylinder(&cyl),
2603 32,
2604 Some(v_hint),
2605 Some((0.0, 2.5)),
2606 )
2607 .unwrap();
2608
2609 assert_eq!(curves.len(), 2, "expected exactly the two branches");
2610 for c in &curves {
2611 let (t0, t1) = c.curve.domain();
2612 for k in 0..=32 {
2613 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
2614 let p = ParametricCurve::evaluate(&c.curve, t);
2615 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
2617 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
2618 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
2620 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
2621 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
2623 }
2624 }
2625 }
2626
2627 #[test]
2630 fn coaxial_cone_cylinder_defers_to_other_paths() {
2631 let cone = ConicalSurface::new(
2632 Point3::new(0.0, 0.0, 0.0),
2633 Vec3::new(0.0, 0.0, 1.0),
2634 std::f64::consts::FRAC_PI_4,
2635 )
2636 .unwrap();
2637 let cyl =
2638 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
2639 .unwrap();
2640 assert!(
2641 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
2642 .unwrap()
2643 .is_none()
2644 );
2645 }
2646
2647 #[test]
2648 fn oblique_cone_cylinder_defers_to_other_paths() {
2649 let cone = ConicalSurface::new(
2650 Point3::new(0.0, 0.0, 0.0),
2651 Vec3::new(0.0, 0.0, 1.0),
2652 std::f64::consts::FRAC_PI_4,
2653 )
2654 .unwrap();
2655 let cyl =
2656 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
2657 .unwrap();
2658 assert!(
2659 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
2660 .unwrap()
2661 .is_none()
2662 );
2663 }
2664
2665 #[test]
2666 fn plane_torus_lobe_closes_and_stays_on_surface() {
2667 use crate::traits::ParametricCurve;
2668 let (major, minor) = (10.0, 3.0);
2669 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
2670
2671 for (n, d) in [
2675 (Vec3::new(0.0, -1.0, 0.0), 4.0), (Vec3::new(-1.0, 0.0, 0.0), -6.0), (Vec3::new(0.0, 0.0, 1.0), 0.0), ] {
2679 let curves = intersect_plane_torus(&torus, n, d).unwrap();
2680 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
2681 for c in &curves {
2682 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
2683 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
2684 assert!(
2685 (p0 - p1).length() < 1e-7,
2686 "lobe not closed: gap={} (n={n:?} d={d})",
2687 (p0 - p1).length()
2688 );
2689 for k in 0..=64 {
2691 let t = f64::from(k) / 64.0;
2692 let p = ParametricCurve::evaluate(&c.curve, t);
2693 assert!(
2694 torus_implicit(p, major, minor).abs() < 1e-2,
2695 "off-surface point {p:?} implicit={}",
2696 torus_implicit(p, major, minor)
2697 );
2698 }
2699 }
2700 }
2701 }
2702
2703 #[test]
2704 fn plane_torus_inner_tangent_figure_eight_stays_open() {
2705 use crate::traits::ParametricCurve;
2706 let (major, minor) = (10.0, 3.0);
2707 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
2708
2709 let curves =
2715 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
2716 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
2717 let max_gap = curves
2718 .iter()
2719 .map(|c| {
2720 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
2721 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
2722 (p0 - p1).length()
2723 })
2724 .fold(0.0_f64, f64::max);
2725 assert!(
2726 max_gap > 1e-2,
2727 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
2728 );
2729 }
2730
2731 #[test]
2732 fn line_torus_box_edge_crossing_is_exact() {
2733 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
2736 let ts = intersect_line_torus(
2737 &torus,
2738 Point3::new(6.0, -4.0, -5.0),
2739 Vec3::new(0.0, 0.0, 1.0),
2740 );
2741 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
2743 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
2744 let rho = 6.0_f64.hypot(4.0);
2745 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
2746 assert!(
2747 (zs[0] - (-z_exp)).abs() < 1e-9,
2748 "z0={} exp={}",
2749 zs[0],
2750 -z_exp
2751 );
2752 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
2753 for &t in &ts {
2755 let p = Point3::new(6.0, -4.0, -5.0 + t);
2756 let rho = p.x().hypot(p.y());
2757 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
2758 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
2759 }
2760 }
2761
2762 #[test]
2763 fn line_torus_miss_and_tangent() {
2764 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
2765 let miss = intersect_line_torus(
2767 &torus,
2768 Point3::new(20.0, 0.0, 0.0),
2769 Vec3::new(0.0, 0.0, 1.0),
2770 );
2771 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
2772 let axis =
2774 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
2775 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
2776 }
2777
2778 #[test]
2779 fn dispatch_via_analytic_surface() {
2780 let cyl =
2781 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
2782 .unwrap();
2783 let curves = intersect_plane_analytic(
2784 AnalyticSurface::Cylinder(&cyl),
2785 Vec3::new(0.0, 0.0, 1.0),
2786 0.0,
2787 )
2788 .unwrap();
2789 assert!(!curves.is_empty());
2790 }
2791
2792 #[test]
2793 fn perpendicular_cylinders_intersect() {
2794 let cyl_z =
2795 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
2796 .unwrap();
2797 let cyl_x =
2798 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
2799 .unwrap();
2800
2801 let curves = intersect_analytic_analytic(
2802 AnalyticSurface::Cylinder(&cyl_z),
2803 AnalyticSurface::Cylinder(&cyl_x),
2804 16,
2805 )
2806 .unwrap();
2807
2808 assert!(
2809 !curves.is_empty(),
2810 "perpendicular cylinders should intersect"
2811 );
2812
2813 for c in &curves {
2814 assert!(
2815 c.points.len() >= 2,
2816 "intersection curve should have >= 2 points, got {}",
2817 c.points.len()
2818 );
2819 }
2820 }
2821
2822 #[test]
2823 fn sphere_cylinder_intersect() {
2824 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
2825 let cyl =
2826 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
2827 .unwrap();
2828
2829 let curves = intersect_analytic_analytic(
2830 AnalyticSurface::Sphere(&sphere),
2831 AnalyticSurface::Cylinder(&cyl),
2832 16,
2833 )
2834 .unwrap();
2835
2836 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
2840 }
2841
2842 #[test]
2843 fn exact_sphere_cylinder_coaxial_two_circles() {
2844 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
2847 let cyl =
2848 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
2849 .unwrap();
2850 let circles = exact_sphere_cylinder(&sphere, &cyl)
2851 .unwrap()
2852 .expect("coaxial case returns Some");
2853 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
2854 let mut zs: Vec<f64> = circles
2855 .iter()
2856 .filter_map(|c| match c {
2857 ExactIntersectionCurve::Circle(circle) => {
2858 assert!(
2859 (circle.radius() - 3.0).abs() < 1e-9,
2860 "rim radius == cyl radius"
2861 );
2862 Some(circle.center().z())
2863 }
2864 _ => None,
2865 })
2866 .collect();
2867 assert_eq!(zs.len(), 2, "both sections must be exact circles");
2868 zs.sort_by(f64::total_cmp);
2869 let z = 27.0_f64.sqrt();
2870 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
2871 }
2872
2873 #[test]
2874 fn exact_sphere_cylinder_non_coaxial_defers() {
2875 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
2877 let cyl =
2878 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
2879 .unwrap();
2880 assert!(
2881 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
2882 "non-coaxial sphere/cylinder defers to the marcher"
2883 );
2884 }
2885
2886 #[test]
2887 fn disjoint_cylinders_no_intersection() {
2888 let cyl_a =
2889 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
2890 .unwrap();
2891 let cyl_b =
2892 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
2893 .unwrap();
2894
2895 let curves = intersect_analytic_analytic(
2896 AnalyticSurface::Cylinder(&cyl_a),
2897 AnalyticSurface::Cylinder(&cyl_b),
2898 16,
2899 )
2900 .unwrap();
2901
2902 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
2903 }
2904
2905 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
2909 use crate::traits::ParametricCurve;
2910 match curve {
2911 ExactIntersectionCurve::Circle(c) => (0..=64)
2912 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
2913 .collect(),
2914 ExactIntersectionCurve::Ellipse(e) => (0..=64)
2915 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
2916 .collect(),
2917 ExactIntersectionCurve::Points(pts) => pts.clone(),
2918 }
2919 }
2920
2921 fn assert_on_plane_and_cone(
2924 curves: &[ExactIntersectionCurve],
2925 cone: &ConicalSurface,
2926 n: Vec3,
2927 d: f64,
2928 z_bound: (f64, f64),
2929 ) {
2930 assert!(!curves.is_empty(), "expected at least one section curve");
2931 let mut total = 0;
2932 for curve in curves {
2933 for p in collect_points(curve) {
2934 total += 1;
2935 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
2936 assert!(
2937 plane_err < 1e-9,
2938 "point off plane by {plane_err:.2e}: {p:?}"
2939 );
2940 let (u, v) = cone.project_point(p);
2941 let q = cone.evaluate(u, v);
2942 let cone_err =
2943 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
2944 .sqrt();
2945 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
2946 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
2947 assert!(
2948 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
2949 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
2950 p.z()
2951 );
2952 }
2953 }
2954 assert!(total >= 8, "too few section points ({total})");
2955 }
2956
2957 #[test]
2958 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
2959 let cone = ConicalSurface::new(
2963 Point3::new(0.0, 0.0, 0.0),
2964 Vec3::new(0.0, 0.0, 1.0),
2965 std::f64::consts::FRAC_PI_4,
2966 )
2967 .unwrap();
2968 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
2969 let d = n.z() * 5.0;
2971 let curves = exact_plane_cone(&cone, n, d).unwrap();
2972 assert!(
2973 curves
2974 .iter()
2975 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
2976 "oblique steep plane × cone must yield an exact Ellipse"
2977 );
2978 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
2980 }
2981
2982 #[test]
2983 fn oblique_plane_cone_wrong_nappe_is_empty() {
2984 let cone = ConicalSurface::new(
2988 Point3::new(0.0, 0.0, 0.0),
2989 Vec3::new(0.0, 0.0, 1.0),
2990 std::f64::consts::FRAC_PI_4,
2991 )
2992 .unwrap();
2993 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
2994 let d = n.z() * -5.0;
2995 let curves = exact_plane_cone(&cone, n, d).unwrap();
2996 assert!(
2997 curves.is_empty(),
2998 "plane on the phantom-nappe side must yield no real curve, got {}",
2999 curves.len()
3000 );
3001 }
3002
3003 #[test]
3004 fn oblique_plane_cone_parabola_on_both_single_branch() {
3005 let cone = ConicalSurface::new(
3008 Point3::new(0.0, 0.0, 0.0),
3009 Vec3::new(0.0, 0.0, 1.0),
3010 std::f64::consts::FRAC_PI_4,
3011 )
3012 .unwrap();
3013 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
3014 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d).unwrap();
3016 assert_eq!(
3017 curves.len(),
3018 1,
3019 "a parabola is a single branch, got {}",
3020 curves.len()
3021 );
3022 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
3024 }
3025
3026 #[test]
3027 fn oblique_plane_cone_hyperbola_real_nappe_only() {
3028 let cone = ConicalSurface::new(
3036 Point3::new(-59.0, -59.0, 15.85),
3037 Vec3::new(0.0, 0.0, -1.0),
3038 std::f64::consts::FRAC_PI_4,
3039 )
3040 .unwrap();
3041 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
3042 let d = -58.360_56;
3043 let cos_theta = n.dot(cone.axis()).abs();
3044 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
3045 let curves = exact_plane_cone(&cone, n, d).unwrap();
3046 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
3049 for c in &curves {
3051 assert!(
3052 matches!(c, ExactIntersectionCurve::Points(_)),
3053 "hyperbola must be sampled Points, not a closed conic"
3054 );
3055 }
3056 }
3057}