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 exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
45}
46
47pub fn exact_plane_analytic_reaching(
55 surface: AnalyticSurface<'_>,
56 plane_normal: Vec3,
57 plane_d: f64,
58 reach: f64,
59) -> Result<Vec<ExactIntersectionCurve>, MathError> {
60 match surface {
61 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
62 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
63 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
64 AnalyticSurface::Torus(torus) => {
65 if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
66 return Ok(circles);
67 }
68 if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
69 return Ok(loops
70 .into_iter()
71 .map(ExactIntersectionCurve::Points)
72 .collect());
73 }
74 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
76 Ok(chains
77 .into_iter()
78 .map(ExactIntersectionCurve::Points)
79 .collect())
80 }
81 }
82}
83
84fn exact_plane_torus(
95 torus: &ToroidalSurface,
96 normal: Vec3,
97 d: f64,
98) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
99 let len = normal.length();
100 let n = normal.normalize()?;
101 let d = d / len;
102 let axis = torus.z_axis();
103 let center = torus.center();
104 let (big, small) = (torus.major_radius(), torus.minor_radius());
105 let height = d - dot_np(n, center);
106 let along = n.dot(axis);
107 if along.abs() > 1.0 - 1e-10 {
108 if height.abs() >= small - 1e-10 * small {
109 return Ok(if height.abs() > small + 1e-10 * small {
110 Some(Vec::new())
111 } else {
112 None
113 });
114 }
115 let reach = small.mul_add(small, -(height * height)).sqrt();
116 if big - reach <= 1e-10 * big {
117 return Ok(None);
118 }
119 let middle = center + n * height;
120 return Ok(Some(vec![
121 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
122 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
123 ]));
124 }
125 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
126 let out = axis.cross(n).normalize()?;
127 return Ok(Some(vec![
128 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
129 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
130 ]));
131 }
132 Ok(None)
133}
134
135fn exact_plane_cylinder(
141 cyl: &CylindricalSurface,
142 normal: Vec3,
143 d: f64,
144) -> Result<Vec<ExactIntersectionCurve>, MathError> {
145 let axis = cyl.axis();
146 let cos_theta = normal.dot(axis).abs();
147 let r = cyl.radius();
148
149 if cos_theta < 1e-10 {
150 let chains = sample_plane_cylinder(cyl, normal, d)?;
153 return Ok(chains
154 .into_iter()
155 .map(ExactIntersectionCurve::Points)
156 .collect());
157 }
158
159 let n_dot_axis = normal.dot(axis);
162 let n_dot_origin = dot_np(normal, cyl.origin());
163 let t = (d - n_dot_origin) / n_dot_axis;
164 let center_on_axis = Point3::new(
165 cyl.origin().x() + t * axis.x(),
166 cyl.origin().y() + t * axis.y(),
167 cyl.origin().z() + t * axis.z(),
168 );
169
170 if cos_theta > 1.0 - 1e-10 {
171 let circle = Circle3D::new(center_on_axis, normal, r)?;
173 Ok(vec![ExactIntersectionCurve::Circle(circle)])
174 } else {
175 let semi_minor = r;
179 let semi_major = r / cos_theta;
180
181 let axis_proj = Vec3::new(
185 axis.x() - n_dot_axis * normal.x(),
186 axis.y() - n_dot_axis * normal.y(),
187 axis.z() - n_dot_axis * normal.z(),
188 );
189 let u_axis = axis_proj.normalize()?;
190 let v_axis = normal.cross(u_axis);
191
192 let ellipse = Ellipse3D::with_axes(
193 center_on_axis,
194 normal,
195 semi_major,
196 semi_minor,
197 u_axis,
198 v_axis,
199 )?;
200 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
201 }
202}
203
204fn exact_plane_sphere(
208 sphere: &SphericalSurface,
209 normal: Vec3,
210 d: f64,
211) -> Result<Vec<ExactIntersectionCurve>, MathError> {
212 let h = dot_np(normal, sphere.center()) - d;
213 let r = sphere.radius();
214
215 if h.abs() > r - 1e-10 {
216 return Ok(vec![]);
217 }
218
219 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
220 let circle_center = Point3::new(
221 h.mul_add(-normal.x(), sphere.center().x()),
222 h.mul_add(-normal.y(), sphere.center().y()),
223 h.mul_add(-normal.z(), sphere.center().z()),
224 );
225
226 let circle = Circle3D::new(circle_center, normal, circle_r)?;
227 Ok(vec![ExactIntersectionCurve::Circle(circle)])
228}
229
230fn exact_plane_cone(
239 cone: &ConicalSurface,
240 normal: Vec3,
241 d: f64,
242 reach: f64,
243) -> Result<Vec<ExactIntersectionCurve>, MathError> {
244 let axis = cone.axis();
245 let cos_theta = normal.dot(axis).abs();
246 let half_angle = cone.half_angle();
247
248 if cos_theta > 1.0 - 1e-10 {
249 let n_dot_axis = normal.dot(axis);
252 let n_dot_apex = dot_np(normal, cone.apex());
253 let t = (d - n_dot_apex) / n_dot_axis;
254
255 if t.abs() < 1e-10 {
260 return Ok(vec![]);
261 }
262
263 let center = Point3::new(
264 cone.apex().x() + t * axis.x(),
265 cone.apex().y() + t * axis.y(),
266 cone.apex().z() + t * axis.z(),
267 );
268 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
272 if circle_r < 1e-15 {
273 return Ok(vec![]);
274 }
275
276 let circle = Circle3D::new(center, normal, circle_r)?;
277 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
278 }
279
280 let c = normal.dot(axis);
292 let p2 = (1.0 - c * c).max(0.0);
293 let p = p2.sqrt();
294 let k = half_angle.sin().powi(2);
295 let a_coeff = p2 - k;
296
297 let m = Vec3::new(
299 axis.x() - c * normal.x(),
300 axis.y() - c * normal.y(),
301 axis.z() - c * normal.z(),
302 );
303 let m_len = m.length();
304 if m_len < 1e-12 {
305 let chains = sample_plane_cone(cone, normal, d, reach)?;
308 return Ok(chains
309 .into_iter()
310 .map(ExactIntersectionCurve::Points)
311 .collect());
312 }
313 let e1 = m * (1.0 / m_len);
314 let e2 = normal.cross(e1);
315 let apex = cone.apex();
316 let e = d - dot_np(normal, apex);
317
318 if a_coeff < -1e-9 {
321 let abs_a = -a_coeff; if e * c < 0.0 {
328 return Ok(vec![]);
329 }
330 let s_c = e * c * p / abs_a;
333 let rhs = e * e * k * (1.0 - k) / abs_a;
334 if rhs <= 0.0 {
335 return Ok(vec![]);
336 }
337 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
340 return Ok(vec![]);
341 }
342 let center = apex + normal * e + e1 * s_c;
343 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
344 (semi_s, semi_t, e1, e2)
345 } else {
346 (semi_t, semi_s, e2, e1)
347 };
348 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
349 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
350 }
351
352 let chains = sample_plane_cone(cone, normal, d, reach)?;
355 Ok(chains
356 .into_iter()
357 .map(ExactIntersectionCurve::Points)
358 .collect())
359}
360
361#[derive(Clone, Copy)]
363pub enum AnalyticSurface<'a> {
364 Cylinder(&'a CylindricalSurface),
366 Cone(&'a ConicalSurface),
368 Sphere(&'a SphericalSurface),
370 Torus(&'a ToroidalSurface),
372}
373
374fn dot_np(n: Vec3, p: Point3) -> f64 {
376 n.dot(Vec3::new(p.x(), p.y(), p.z()))
377}
378
379pub fn intersect_plane_analytic(
387 surface: AnalyticSurface<'_>,
388 normal: Vec3,
389 d: f64,
390) -> Result<Vec<IntersectionCurve>, MathError> {
391 match surface {
392 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
393 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
394 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
395 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
396 }
397}
398
399pub fn sample_plane_analytic(
410 surface: AnalyticSurface<'_>,
411 normal: Vec3,
412 d: f64,
413) -> Result<Vec<Vec<Point3>>, MathError> {
414 match surface {
415 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
416 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
417 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
418 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
419 }
420}
421
422#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
424fn sample_plane_cylinder(
425 cyl: &CylindricalSurface,
426 normal: Vec3,
427 d: f64,
428) -> Result<Vec<Vec<Point3>>, MathError> {
429 let n_samples = 64_usize;
430 let mut points = Vec::with_capacity(n_samples + 1);
431
432 for i in 0..=n_samples {
433 let u = TAU * (i as f64) / (n_samples as f64);
434 let base = cyl.evaluate(u, 0.0);
435 let n_dot_axis = normal.dot(cyl.axis());
436 let n_dot_base = dot_np(normal, base);
437
438 if n_dot_axis.abs() < 1e-12 {
439 if (n_dot_base - d).abs() < 1e-6 {
440 points.push(base);
441 }
442 } else {
443 let v = (d - n_dot_base) / n_dot_axis;
444 if v.abs() <= 100.0 {
445 points.push(cyl.evaluate(u, v));
446 }
447 }
448 }
449
450 if points.len() < 2 {
451 Ok(vec![])
452 } else {
453 Ok(vec![points])
454 }
455}
456
457#[allow(clippy::cast_precision_loss)]
459fn sample_plane_sphere(
460 sphere: &SphericalSurface,
461 normal: Vec3,
462 d: f64,
463) -> Result<Vec<Vec<Point3>>, MathError> {
464 let h = dot_np(normal, sphere.center()) - d;
465 let r = sphere.radius();
466
467 if h.abs() > r - 1e-10 {
468 return Ok(vec![]);
469 }
470
471 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
472 let circle_center = Point3::new(
473 h.mul_add(-normal.x(), sphere.center().x()),
474 h.mul_add(-normal.y(), sphere.center().y()),
475 h.mul_add(-normal.z(), sphere.center().z()),
476 );
477
478 let basis = Frame3::from_normal(circle_center, normal)?;
479 let u_dir = basis.x;
480 let v_dir = basis.y;
481
482 let n_samples = 64_usize;
483 let mut points = Vec::with_capacity(n_samples + 1);
484
485 for i in 0..=n_samples {
486 let theta = TAU * (i as f64) / (n_samples as f64);
487 let (sin_t, cos_t) = theta.sin_cos();
488 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
489 }
490
491 Ok(vec![points])
492}
493
494#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
506fn sample_plane_cone(
507 cone: &ConicalSurface,
508 normal: Vec3,
509 d: f64,
510 reach: f64,
511) -> Result<Vec<Vec<Point3>>, MathError> {
512 let apex = cone.apex();
513 let n_dot_apex = dot_np(normal, apex);
514 let e = d - n_dot_apex;
515
516 let n_samples = 512_usize;
520 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
521 let mut v_min = f64::INFINITY;
522 for i in 0..n_samples {
523 let u = TAU * (i as f64) / (n_samples as f64);
524 let g = cone.evaluate(u, 1.0) - apex;
525 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
526 if n_dot_g.abs() < 1e-12 {
527 vs.push(None);
528 continue;
529 }
530 let v = e / n_dot_g;
531 if v >= -1e-12 {
532 let v = v.max(0.0);
533 v_min = v_min.min(v);
534 vs.push(Some(v));
535 } else {
536 vs.push(None);
537 }
538 }
539
540 if !v_min.is_finite() {
541 return Ok(Vec::new());
542 }
543
544 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
553
554 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
557
558 let point_at = |u: f64, v: f64| -> Point3 {
559 let g = cone.evaluate(u, 1.0) - apex;
560 apex + g * v
561 };
562 #[allow(clippy::cast_precision_loss)]
563 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
564 let n_dot_g_at = |u: f64| -> f64 {
565 let g = cone.evaluate(u, 1.0) - apex;
566 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
567 };
568
569 if kept.iter().all(Option::is_some) {
570 let mut pts: Vec<Point3> = kept
572 .iter()
573 .enumerate()
574 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
575 .collect();
576 if let Some(&first) = pts.first() {
577 pts.push(first);
578 }
579 return Ok(vec![pts]);
580 }
581
582 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
591 let Some(v_end) = kept[i_end] else {
592 return Vec::new();
593 };
594 let u_end = u_of(i_end);
595 #[allow(clippy::cast_precision_loss)]
596 let pitch = TAU / (n_samples as f64);
597 let u_next = if forward {
598 u_end + pitch
599 } else {
600 u_end - pitch
601 };
602 let target = e / v_max;
603 let h_end = n_dot_g_at(u_end) - target;
604 let h_next = n_dot_g_at(u_next) - target;
605 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
606 return Vec::new();
607 }
608 let (mut lo, mut hi) = (u_end, u_next);
609 for _ in 0..60 {
610 let mid = f64::midpoint(lo, hi);
611 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
612 lo = mid;
613 } else {
614 hi = mid;
615 }
616 }
617 let u_star = f64::midpoint(lo, hi);
618 let tail_n = 8_usize;
619 (1..=tail_n)
620 .filter_map(|k| {
621 #[allow(clippy::cast_precision_loss)]
622 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
623 let ng = n_dot_g_at(u);
624 if ng.abs() < 1e-12 {
625 return None;
626 }
627 let v = e / ng;
628 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
629 })
630 .collect()
631 };
632
633 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
636 let mut chains: Vec<Vec<Point3>> = Vec::new();
637 let mut run: Vec<usize> = Vec::new();
638 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
639 if run.len() >= 2 {
640 let first = run[0];
641 let last = run[run.len() - 1];
642 let mut pts: Vec<Point3> = tail(first, false, &kept);
643 pts.reverse();
644 pts.extend(
645 run.iter()
646 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
647 );
648 pts.extend(tail(last, true, &kept));
649 chains.push(pts);
650 }
651 run.clear();
652 };
653 for k in 0..n_samples {
654 let idx = (gap + k) % n_samples;
655 if kept[idx].is_some() {
656 run.push(idx);
657 } else {
658 flush(&mut run, &mut chains);
659 }
660 }
661 flush(&mut run, &mut chains);
662 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
663}
664
665#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
671 torus: &ToroidalSurface,
672 normal: Vec3,
673 d: f64,
674) -> Result<Vec<Vec<Point3>>, MathError> {
675 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
676 Ok(chain_torus_crossings(&crossing_pts)
677 .into_iter()
678 .map(|run| run.into_iter().map(|p| p.point).collect())
679 .collect())
680}
681
682#[allow(clippy::cast_precision_loss)]
692pub fn intersect_plane_cylinder(
693 cyl: &CylindricalSurface,
694 normal: Vec3,
695 d: f64,
696) -> Result<Vec<IntersectionCurve>, MathError> {
697 let n_samples = 64_usize;
698 let mut points_3d = Vec::new();
699 let mut ipoints = Vec::new();
700
701 for i in 0..=n_samples {
702 let u = TAU * (i as f64) / (n_samples as f64);
703 let base = cyl.evaluate(u, 0.0);
706 let n_dot_axis = normal.dot(cyl.axis());
707 let n_dot_base = dot_np(normal, base);
708
709 if n_dot_axis.abs() < 1e-12 {
710 if (n_dot_base - d).abs() < 1e-6 {
712 let pt = base;
713 points_3d.push(pt);
714 ipoints.push(IntersectionPoint {
715 point: pt,
716 param1: (u, 0.0),
717 param2: (0.0, 0.0),
718 });
719 }
720 } else {
721 let v = (d - n_dot_base) / n_dot_axis;
722 if v.abs() <= 100.0 {
724 let pt = cyl.evaluate(u, v);
725 points_3d.push(pt);
726 ipoints.push(IntersectionPoint {
727 point: pt,
728 param1: (u, v),
729 param2: (0.0, 0.0),
730 });
731 }
732 }
733 }
734
735 build_curves_from_points(&points_3d, ipoints)
736}
737
738#[allow(clippy::cast_precision_loss)]
747pub fn intersect_plane_sphere(
748 sphere: &SphericalSurface,
749 normal: Vec3,
750 d: f64,
751) -> Result<Vec<IntersectionCurve>, MathError> {
752 let h = dot_np(normal, sphere.center()) - d;
753 let r = sphere.radius();
754
755 if h.abs() > r - 1e-10 {
757 return Ok(vec![]);
758 }
759
760 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
761 let circle_center = Point3::new(
762 h.mul_add(-normal.x(), sphere.center().x()),
763 h.mul_add(-normal.y(), sphere.center().y()),
764 h.mul_add(-normal.z(), sphere.center().z()),
765 );
766
767 let basis = Frame3::from_normal(circle_center, normal)?;
769 let u_dir = basis.x;
770 let v_dir = basis.y;
771
772 let n_samples = 64_usize;
773 let mut points_3d = Vec::new();
774 let mut ipoints = Vec::new();
775
776 for i in 0..=n_samples {
777 let theta = TAU * (i as f64) / (n_samples as f64);
778 let (sin_t, cos_t) = theta.sin_cos();
779 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
780 points_3d.push(pt);
781 ipoints.push(IntersectionPoint {
782 point: pt,
783 param1: (theta, 0.0),
784 param2: (0.0, 0.0),
785 });
786 }
787
788 build_curves_from_points(&points_3d, ipoints)
789}
790
791#[allow(clippy::cast_precision_loss)]
800pub fn intersect_plane_cone(
801 cone: &ConicalSurface,
802 normal: Vec3,
803 d: f64,
804) -> Result<Vec<IntersectionCurve>, MathError> {
805 let n_samples = 64_usize;
806 let mut points_3d = Vec::new();
807 let mut ipoints = Vec::new();
808
809 for i in 0..n_samples {
810 let u = TAU * (i as f64) / (n_samples as f64);
811 let apex = cone.apex();
814 let n_dot_apex = dot_np(normal, apex);
815 let p1 = cone.evaluate(u, 1.0);
817 let dir = p1 - apex;
818 let n_dot_dir = normal.dot(dir);
819
820 if n_dot_dir.abs() < 1e-12 {
821 continue;
822 }
823
824 let v = (d - n_dot_apex) / n_dot_dir;
825 if v.abs() > 1e-10 && v.abs() < 100.0 {
827 let pt = cone.evaluate(u, v);
828 points_3d.push(pt);
829 ipoints.push(IntersectionPoint {
830 point: pt,
831 param1: (u, v),
832 param2: (0.0, 0.0),
833 });
834 }
835 }
836
837 build_curves_from_points(&points_3d, ipoints)
838}
839
840#[allow(clippy::unnecessary_wraps)]
852pub fn intersect_plane_torus(
853 torus: &ToroidalSurface,
854 normal: Vec3,
855 d: f64,
856) -> Result<Vec<IntersectionCurve>, MathError> {
857 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
861
862 let mut curves = Vec::new();
863 for ipts in chain_torus_crossings(&crossing_pts) {
864 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
865 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
866 curves.push(IntersectionCurve {
867 curve,
868 points: ipts,
869 });
870 }
871 }
872
873 Ok(curves)
874}
875
876fn chain_torus_crossings(crossing_pts: &[(f64, f64, Point3)]) -> Vec<Vec<IntersectionPoint>> {
888 let mut used = vec![false; crossing_pts.len()];
889 let mut runs = Vec::new();
890
891 for start in 0..crossing_pts.len() {
892 if used[start] {
893 continue;
894 }
895 used[start] = true;
896 let mut chain = vec![start];
897
898 loop {
899 let last = chain[chain.len() - 1];
900 let last_pt = crossing_pts[last].2;
901 let mut best_idx = None;
902 let mut best_dist = 1.0_f64;
903
904 for (j, &is_used) in used.iter().enumerate() {
905 if is_used {
906 continue;
907 }
908 let dist = (crossing_pts[j].2 - last_pt).length();
909 if dist < best_dist {
910 best_dist = dist;
911 best_idx = Some(j);
912 }
913 }
914
915 if let Some(j) = best_idx {
916 used[j] = true;
917 chain.push(j);
918 } else {
919 break;
920 }
921 }
922
923 if chain.len() < 4 {
924 continue;
925 }
926 let mut ipts: Vec<IntersectionPoint> = chain
927 .iter()
928 .map(|&i| IntersectionPoint {
929 point: crossing_pts[i].2,
930 param1: (crossing_pts[i].0, crossing_pts[i].1),
931 param2: (0.0, 0.0),
932 })
933 .collect();
934
935 let closing_gap = (ipts[ipts.len() - 1].point - ipts[0].point).length();
936 let median_spacing = {
937 let mut spac: Vec<f64> = ipts
938 .windows(2)
939 .map(|w| (w[1].point - w[0].point).length())
940 .collect();
941 spac.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
942 spac.get(spac.len() / 2).copied().unwrap_or(0.0)
943 };
944 if closing_gap > 1e-9
951 && median_spacing > 1e-12
952 && closing_gap <= 2.0 * median_spacing
953 && !chain_self_touches(&ipts, median_spacing)
954 {
955 ipts.push(ipts[0]);
956 }
957 runs.push(ipts);
958 }
959
960 runs
961}
962
963fn chain_self_touches(ipts: &[IntersectionPoint], median_spacing: f64) -> bool {
973 let m = ipts.len();
974 let k = (m / 4).clamp(1, 6);
975 if m < 3 * k || median_spacing <= 0.0 {
976 return false;
977 }
978 let thresh = median_spacing * 1.5;
979 for i in k..(m - k) {
980 for j in (i + k)..(m - k) {
981 if (ipts[i].point - ipts[j].point).length() < thresh {
982 return true;
983 }
984 }
985 }
986 false
987}
988
989#[allow(clippy::cast_precision_loss)]
1006fn plane_torus_crossings(
1007 torus: &ToroidalSurface,
1008 normal: Vec3,
1009 d: f64,
1010 n_v: usize,
1011) -> Vec<(f64, f64, Point3)> {
1012 let big_r = torus.major_radius();
1013 let small_r = torus.minor_radius();
1014 let a = normal.dot(torus.x_axis());
1015 let b = normal.dot(torus.y_axis());
1016 let c = normal.dot(torus.z_axis());
1017 let s = a.hypot(b);
1018 let phi = b.atan2(a);
1019 let d_local = d - dot_np(normal, torus.center());
1020
1021 let mut pts: Vec<(f64, f64, Point3)> = Vec::new();
1022
1023 if s < 1e-12 {
1025 if c.abs() < 1e-12 {
1026 return pts;
1027 }
1028 let sin_v = d_local / (small_r * c);
1029 if sin_v.abs() > 1.0 + 1e-9 {
1030 return pts;
1031 }
1032 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1033 let v1 = std::f64::consts::PI - v0;
1034 let mut vs = vec![v0];
1035 if (v1 - v0).abs() > 1e-9 {
1037 vs.push(v1);
1038 }
1039 for v in vs {
1040 for i in 0..n_v {
1041 let u = TAU * (i as f64) / (n_v as f64);
1042 pts.push((u, v, torus.evaluate(u, v)));
1043 }
1044 }
1045 return pts;
1046 }
1047
1048 let v_off = TAU / (n_v as f64) * 0.5;
1054 for i in 0..n_v {
1055 let v = (i as f64).mul_add(TAU / (n_v as f64), v_off);
1056 let tube_r = small_r.mul_add(v.cos(), big_r); let rhs = (d_local - small_r * c * v.sin()) / (s * tube_r);
1058 if rhs.abs() > 1.0 {
1059 continue;
1060 }
1061 let delta = rhs.clamp(-1.0, 1.0).acos();
1062 for u in [phi + delta, phi - delta] {
1063 pts.push((u, v, torus.evaluate(u, v)));
1064 }
1065 }
1066 pts
1067}
1068
1069#[allow(clippy::cast_precision_loss)]
1078fn plane_torus_winding_loops(
1079 torus: &ToroidalSurface,
1080 normal: Vec3,
1081 d: f64,
1082 n_v: usize,
1083) -> Option<Vec<Vec<Point3>>> {
1084 let big_r = torus.major_radius();
1085 let small_r = torus.minor_radius();
1086 let a = normal.dot(torus.x_axis());
1087 let b = normal.dot(torus.y_axis());
1088 let c = normal.dot(torus.z_axis());
1089 let s = a.hypot(b);
1090 if s < 1e-12 * normal.length() || small_r >= big_r {
1091 return None;
1092 }
1093 let phi = b.atan2(a);
1094 let d_local = d - dot_np(normal, torus.center());
1095 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1096 let dense = 8 * n_v;
1097 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1098 return None;
1099 }
1100 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1101 for i in 0..n_v {
1102 let v = TAU * i as f64 / n_v as f64;
1103 let delta = rhs(v).acos();
1104 loops[0].push(torus.evaluate(phi + delta, v));
1105 loops[1].push(torus.evaluate(phi - delta, v));
1106 }
1107 Some(
1108 loops
1109 .into_iter()
1110 .map(|mut run| {
1111 run.push(run[0]);
1112 run
1113 })
1114 .collect(),
1115 )
1116}
1117
1118#[must_use]
1131pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1132 let c = torus.center();
1133 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1134 let big_r = torus.major_radius();
1135 let small_r = torus.minor_radius();
1136
1137 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1139 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1140 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1141 let (c0, c1) = (za.dot(o), za.dot(dir));
1142
1143 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1145 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1146 let g0 = a0.mul_add(
1147 a0,
1148 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1149 );
1150
1151 let four_rr = 4.0 * big_r * big_r;
1153 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1154 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1155 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1156
1157 let e4 = g2 * g2;
1159 let e3 = 2.0 * g2 * g1;
1160 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1161 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1162 let e0 = g0.mul_add(g0, -h0);
1163
1164 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1165 let impl_f = |t: f64| -> f64 {
1167 let p = origin + dir * t;
1168 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1169 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1170 (a.hypot(b) - big_r).hypot(cc) - small_r
1171 };
1172 for t in &mut roots {
1173 let eps = 1e-7;
1174 let f = impl_f(*t);
1175 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1176 if df.abs() > 1e-12 {
1177 *t -= f / df;
1178 }
1179 }
1180 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1181 roots
1182}
1183
1184fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1187 if c4.abs() < 1e-14 {
1189 return real_roots_cubic(c3, c2, c1, c0);
1190 }
1191 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1193 let eval = |z: Complex| -> Complex {
1194 let mut acc = Complex::new(1.0, 0.0);
1196 acc = acc * z + Complex::new(a, 0.0);
1197 acc = acc * z + Complex::new(b, 0.0);
1198 acc = acc * z + Complex::new(c, 0.0);
1199 acc * z + Complex::new(d, 0.0)
1200 };
1201 let seed = Complex::new(0.4, 0.9);
1203 let mut r = [
1204 Complex::new(1.0, 0.0),
1205 seed,
1206 seed * seed,
1207 seed * seed * seed,
1208 ];
1209 for _ in 0..100 {
1210 let mut max_step = 0.0_f64;
1211 for i in 0..4 {
1212 let mut denom = Complex::new(1.0, 0.0);
1213 for j in 0..4 {
1214 if i != j {
1215 denom = denom * (r[i] - r[j]);
1216 }
1217 }
1218 if denom.norm() < 1e-300 {
1219 continue;
1220 }
1221 let step = eval(r[i]) / denom;
1222 r[i] = r[i] - step;
1223 max_step = max_step.max(step.norm());
1224 }
1225 if max_step < 1e-14 {
1226 break;
1227 }
1228 }
1229 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1236 let mut out: Vec<f64> = Vec::new();
1237 for z in r {
1238 if z.im.abs() >= 1e-7 {
1239 continue;
1240 }
1241 let x = z.re;
1242 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1245 if p_real(x).abs() > 1e-6 * scale {
1246 continue;
1247 }
1248 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1249 continue;
1250 }
1251 out.push(x);
1252 }
1253 out
1254}
1255
1256fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1258 if a.abs() < 1e-14 {
1259 return real_roots_quadratic(b, c, d);
1260 }
1261 let (b, c, d) = (b / a, c / a, d / a);
1263 let p = c - b * b / 3.0;
1264 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1265 let shift = -b / 3.0;
1266 let disc = q * q / 4.0 + p * p * p / 27.0;
1267 if disc > 1e-14 {
1268 let sq = disc.sqrt();
1269 let u = (-q / 2.0 + sq).cbrt();
1270 let v = (-q / 2.0 - sq).cbrt();
1271 vec![u + v + shift]
1272 } else if disc < -1e-14 {
1273 let m = 2.0 * (-p / 3.0).sqrt();
1275 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1276 (0..3)
1277 .map(|k| {
1278 m.mul_add(
1279 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1280 shift,
1281 )
1282 })
1283 .collect()
1284 } else {
1285 let u = (-q / 2.0).cbrt();
1287 vec![2.0 * u + shift, -u + shift]
1288 }
1289}
1290
1291fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1293 if a.abs() < 1e-14 {
1294 if b.abs() < 1e-14 {
1295 return Vec::new();
1296 }
1297 return vec![-c / b];
1298 }
1299 let disc = b * b - 4.0 * a * c;
1300 if disc < 0.0 {
1301 Vec::new()
1302 } else {
1303 let sq = disc.sqrt();
1304 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1305 }
1306}
1307
1308#[derive(Clone, Copy)]
1310struct Complex {
1311 re: f64,
1312 im: f64,
1313}
1314
1315impl Complex {
1316 const fn new(re: f64, im: f64) -> Self {
1317 Self { re, im }
1318 }
1319 fn norm(self) -> f64 {
1320 self.re.hypot(self.im)
1321 }
1322}
1323
1324impl std::ops::Add for Complex {
1325 type Output = Self;
1326 fn add(self, o: Self) -> Self {
1327 Self::new(self.re + o.re, self.im + o.im)
1328 }
1329}
1330
1331impl std::ops::Sub for Complex {
1332 type Output = Self;
1333 fn sub(self, o: Self) -> Self {
1334 Self::new(self.re - o.re, self.im - o.im)
1335 }
1336}
1337
1338impl std::ops::Mul for Complex {
1339 type Output = Self;
1340 fn mul(self, o: Self) -> Self {
1341 Self::new(
1342 self.re.mul_add(o.re, -(self.im * o.im)),
1343 self.re.mul_add(o.im, self.im * o.re),
1344 )
1345 }
1346}
1347
1348impl std::ops::Div for Complex {
1349 type Output = Self;
1350 fn div(self, o: Self) -> Self {
1351 let den = o.re.mul_add(o.re, o.im * o.im);
1352 Self::new(
1353 self.re.mul_add(o.re, self.im * o.im) / den,
1354 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1355 )
1356 }
1357}
1358
1359fn build_curves_from_points(
1363 points_3d: &[Point3],
1364 ipoints: Vec<IntersectionPoint>,
1365) -> Result<Vec<IntersectionCurve>, MathError> {
1366 if points_3d.len() < 2 {
1367 return Ok(vec![]);
1368 }
1369
1370 let degree = 3.min(points_3d.len() - 1);
1371 let curve = interpolate(points_3d, degree)?;
1372 Ok(vec![IntersectionCurve {
1373 curve,
1374 points: ipoints,
1375 }])
1376}
1377
1378#[allow(
1390 clippy::cast_precision_loss,
1391 clippy::too_many_lines,
1392 clippy::similar_names,
1393 clippy::unnecessary_wraps,
1394 clippy::type_complexity
1395)]
1396pub fn intersect_analytic_analytic(
1397 a: AnalyticSurface<'_>,
1398 b: AnalyticSurface<'_>,
1399 grid_res: usize,
1400) -> Result<Vec<IntersectionCurve>, MathError> {
1401 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1402}
1403
1404pub fn intersect_analytic_analytic_bounded(
1415 a: AnalyticSurface<'_>,
1416 b: AnalyticSurface<'_>,
1417 grid_res: usize,
1418 v_range_hint_a: Option<(f64, f64)>,
1419 v_range_hint_b: Option<(f64, f64)>,
1420) -> Result<Vec<IntersectionCurve>, MathError> {
1421 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1424 return Ok(result);
1425 }
1426
1427 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1428 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1429 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1430 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1431
1432 let diag_a = {
1434 let p00 = surf_a(u_range_a.0, v_range_a.0);
1435 let p11 = surf_a(u_range_a.1, v_range_a.1);
1436 (p00 - p11).length()
1437 };
1438 let diag_b = {
1439 let p00 = surf_b(u_range_b.0, v_range_b.0);
1440 let p11 = surf_b(u_range_b.1, v_range_b.1);
1441 (p00 - p11).length()
1442 };
1443 let char_size = diag_a.min(diag_b).max(0.1);
1444
1445 #[allow(clippy::type_complexity)]
1449 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1450 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1454 let mut min_dist = f64::INFINITY;
1455
1456 #[allow(clippy::cast_precision_loss)]
1457 for ia in 0..grid_res {
1458 for ja in 0..grid_res {
1459 let ua =
1460 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1461 let va =
1462 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1463
1464 let pa = surf_a(ua, va);
1465
1466 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1468 let pb = surf_b(ub, vb);
1469 let dist = (pa - pb).length();
1470 min_dist = min_dist.min(dist);
1471
1472 if dist < seed_threshold {
1473 let mid = Point3::new(
1478 (pa.x() + pb.x()) * 0.5,
1479 (pa.y() + pb.y()) * 0.5,
1480 (pa.z() + pb.z()) * 0.5,
1481 );
1482 seeds.push((mid, (ua, va), (ub, vb)));
1483 }
1484 }
1485 }
1486
1487 let reject_dist = (char_size / grid_res as f64) * 3.0;
1496 if min_dist > reject_dist {
1497 return Ok(vec![]);
1498 }
1499
1500 if seeds.is_empty() {
1501 return Ok(vec![]);
1502 }
1503
1504 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1508 let dedup_radius = march_step * 10.0;
1509 let mut unique_seeds = Vec::new();
1510 for seed in &seeds {
1511 let dominated = unique_seeds
1512 .iter()
1513 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1514 if !dominated {
1515 unique_seeds.push(*seed);
1516 }
1517 }
1518
1519 let mut curves = Vec::new();
1521 let mut used_seeds = vec![false; unique_seeds.len()];
1522
1523 for si in 0..unique_seeds.len() {
1524 if used_seeds[si] {
1525 continue;
1526 }
1527 used_seeds[si] = true;
1528
1529 let march_result = march_analytic_intersection(
1530 &a,
1531 &b,
1532 surf_a.as_ref(),
1533 norm_a.as_ref(),
1534 surf_b.as_ref(),
1535 norm_b.as_ref(),
1536 unique_seeds[si].0,
1537 u_range_a,
1538 v_range_a,
1539 u_range_b,
1540 v_range_b,
1541 march_step,
1542 is_u_periodic(&a),
1543 is_u_periodic(&b),
1544 );
1545
1546 if march_result.len() >= 2 {
1547 for (sj, other) in unique_seeds.iter().enumerate() {
1548 if !used_seeds[sj]
1549 && march_result
1550 .iter()
1551 .any(|p| (*p - other.0).length() < dedup_radius)
1552 {
1553 used_seeds[sj] = true;
1554 }
1555 }
1556
1557 let ipts: Vec<IntersectionPoint> = march_result
1558 .iter()
1559 .map(|&pt| IntersectionPoint {
1560 point: pt,
1561 param1: (0.0, 0.0),
1562 param2: (0.0, 0.0),
1563 })
1564 .collect();
1565
1566 let degree = 3.min(march_result.len() - 1);
1567 if let Ok(curve) = interpolate(&march_result, degree) {
1568 curves.push(IntersectionCurve {
1569 curve,
1570 points: ipts,
1571 });
1572 }
1573 }
1574 }
1575
1576 Ok(curves)
1577}
1578
1579#[allow(clippy::too_many_lines)]
1589fn try_algebraic_intersection(
1590 a: &AnalyticSurface<'_>,
1591 b: &AnalyticSurface<'_>,
1592 v_range_a: Option<(f64, f64)>,
1593 v_range_b: Option<(f64, f64)>,
1594) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1595 match (a, b) {
1596 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => {
1597 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)
1598 }
1599 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => {
1600 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)
1601 }
1602 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1603 algebraic_sphere_sphere(s1, s2).map(Some)
1604 }
1605 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1606 let axis_dot = c1.axis().dot(c2.axis()).abs();
1607 if axis_dot > 1.0 - 1e-10 {
1608 let delta = c2.origin() - c1.origin();
1610 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1611 let along = delta_vec.dot(c1.axis());
1612 let perp = (delta_vec - c1.axis() * along).length();
1613 if perp < 1e-8 {
1614 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1617 return Ok(None); }
1619 return Ok(Some(vec![])); }
1621 }
1622 algebraic_cylinder_cylinder(c1, c2)
1624 }
1625 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1627 algebraic_sphere_cylinder(s, c, true)
1628 }
1629 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1630 algebraic_sphere_cylinder(s, c, false)
1631 }
1632 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1633 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1634 Ok(parallel_axis_torus_cylinder(t, c, true))
1635 }
1636 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1637 Ok(parallel_axis_torus_cylinder(t, c, false))
1638 }
1639 _ => Ok(None),
1640 }
1641}
1642
1643fn parallel_axis_torus_cylinder(
1650 torus: &ToroidalSurface,
1651 cyl: &CylindricalSurface,
1652 torus_first: bool,
1653) -> Option<Vec<IntersectionCurve>> {
1654 let axis = torus.z_axis();
1655 let along = cyl.axis().dot(axis);
1656 if along.abs() < 1.0 - 1e-10 {
1657 return None;
1658 }
1659 let offset = cyl.origin() - torus.center();
1660 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1661 return None;
1662 }
1663 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1664 let roots = |u: f64| {
1665 let q = cyl.evaluate(u, 0.0) - torus.center();
1666 let height = q.dot(axis);
1667 let rho = (q - axis * height).length();
1668 let reach = minor * minor - (rho - major) * (rho - major);
1669 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1670 };
1671 let samples = ruling_samples(cyl, &roots);
1672 let loops = if samples.iter().all(Option::is_some) {
1673 closed_ruling_loops(&samples)
1674 } else {
1675 partial_ruling_loops(cyl, &roots, &samples)
1676 };
1677 if loops.is_empty() {
1678 return None;
1679 }
1680 Some(fit_ruling_loops(&loops, |p| {
1681 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1682 }))
1683}
1684
1685fn meridian_crossings(
1691 first: (f64, f64, f64),
1692 second: (f64, f64, f64),
1693 scale: f64,
1694) -> Option<Vec<(f64, f64)>> {
1695 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1696 let (dx, dz) = (x2 - x1, z2 - z1);
1697 let dist = dx.hypot(dz);
1698 let slack = 1e-9 * scale;
1699 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1700 return None;
1701 }
1702 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1703 return Some(Vec::new());
1704 }
1705 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1706 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1707 let (ux, uz) = (dx / dist, dz / dist);
1708 let mut crossings = Vec::with_capacity(2);
1709 for side in [1.0, -1.0] {
1710 let rho = x1 + along * ux - side * across * uz;
1711 if rho <= slack {
1712 return None;
1713 }
1714 crossings.push((rho, z1 + along * uz + side * across * ux));
1715 }
1716 Some(crossings)
1717}
1718
1719fn circles_about_axis(
1721 base: Point3,
1722 axis: Vec3,
1723 crossings: &[(f64, f64)],
1724) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1725 crossings
1726 .iter()
1727 .map(|&(rho, z)| {
1728 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1729 })
1730 .collect()
1731}
1732
1733pub fn exact_torus_torus(
1744 first: &ToroidalSurface,
1745 second: &ToroidalSurface,
1746) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1747 let axis = first.z_axis();
1748 let scale = first.major_radius() + second.major_radius();
1749 let offset = second.center() - first.center();
1750 if first.minor_radius() >= first.major_radius()
1752 || second.minor_radius() >= second.major_radius()
1753 || axis.cross(second.z_axis()).length() > 1e-9
1754 || offset.cross(axis).length() > 1e-9 * scale
1755 {
1756 return Ok(None);
1757 }
1758 let Some(crossings) = meridian_crossings(
1759 (first.major_radius(), 0.0, first.minor_radius()),
1760 (
1761 second.major_radius(),
1762 offset.dot(axis),
1763 second.minor_radius(),
1764 ),
1765 scale,
1766 ) else {
1767 return Ok(None);
1768 };
1769 circles_about_axis(first.center(), axis, &crossings).map(Some)
1770}
1771
1772pub fn exact_cylinder_torus(
1784 cylinder: &CylindricalSurface,
1785 torus: &ToroidalSurface,
1786) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1787 let axis = torus.z_axis();
1788 let scale = torus.major_radius() + cylinder.radius();
1789 let offset = cylinder.origin() - torus.center();
1790 if torus.minor_radius() >= torus.major_radius()
1792 || axis.cross(cylinder.axis()).length() > 1e-9
1793 || offset.cross(axis).length() > 1e-9 * scale
1794 {
1795 return Ok(None);
1796 }
1797 let gap = cylinder.radius() - torus.major_radius();
1798 let small = torus.minor_radius();
1799 if (gap.abs() - small).abs() < 1e-9 * scale {
1800 return Ok(None);
1801 }
1802 if gap.abs() > small {
1803 return Ok(Some(Vec::new()));
1804 }
1805 let height = small.mul_add(small, -(gap * gap)).sqrt();
1806 circles_about_axis(
1807 torus.center(),
1808 axis,
1809 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
1810 )
1811 .map(Some)
1812}
1813
1814pub fn exact_sphere_torus(
1827 sphere: &SphericalSurface,
1828 torus: &ToroidalSurface,
1829) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1830 let axis = torus.z_axis();
1831 let scale = torus.major_radius() + sphere.radius();
1832 let offset = sphere.center() - torus.center();
1833 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
1835 return Ok(None);
1836 }
1837 let Some(crossings) = meridian_crossings(
1838 (0.0, offset.dot(axis), sphere.radius()),
1839 (torus.major_radius(), 0.0, torus.minor_radius()),
1840 scale,
1841 ) else {
1842 return Ok(None);
1843 };
1844 circles_about_axis(torus.center(), axis, &crossings).map(Some)
1845}
1846
1847pub fn exact_cone_cone(
1872 c1: &ConicalSurface,
1873 c2: &ConicalSurface,
1874) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1875 let axis = c1.axis();
1876 let axis2 = c2.axis();
1877
1878 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
1880 return Ok(None); }
1882 let apex1 = c1.apex();
1883 let apex2 = c2.apex();
1884 let delta = apex2 - apex1;
1885 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1886 let along = delta_v.dot(axis);
1887 if (delta_v - axis * along).length() > 1e-8 {
1888 return offset_parallel_cone_cone(c1, c2);
1889 }
1890
1891 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
1892 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
1893 return Ok(None); }
1895 let m1 = c1.half_angle().cos() / s1;
1896 let m2 = c2.half_angle().cos() / s2;
1897 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
1898 let d2 = along; let denom = m1 - m2 * sigma;
1901 if denom.abs() < 1e-12 {
1902 if sigma > 0.0 && d2.abs() < 1e-9 {
1905 return Ok(None);
1906 }
1907 return Ok(Some(vec![]));
1908 }
1909
1910 let t_star = (-m2 * sigma * d2) / denom;
1911 let radius = m1 * t_star;
1912 if radius < 1e-12 {
1913 return Ok(Some(vec![])); }
1915
1916 let center = Point3::new(
1917 apex1.x() + axis.x() * t_star,
1918 apex1.y() + axis.y() * t_star,
1919 apex1.z() + axis.z() * t_star,
1920 );
1921 let circle = Circle3D::new(center, axis, radius)?;
1922 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
1923}
1924
1925fn offset_parallel_cone_cone(
1936 c1: &ConicalSurface,
1937 c2: &ConicalSurface,
1938) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1939 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
1940 return Ok(None); }
1942 let t1 = c1.half_angle().tan();
1943 let t2 = c2.half_angle().tan();
1944 if !t1.is_finite() || !t2.is_finite() {
1945 return Ok(None);
1946 }
1947 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
1948 return Ok(None);
1949 }
1950
1951 let w = c1.axis();
1952 let apex1 = c1.apex();
1953 let apex2 = c2.apex();
1954 let delta = apex2 - apex1;
1955 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1956 let s = delta_v.dot(w);
1957 let tm = 0.5 * (t1 + t2);
1958 let k = 1.0 + tm * tm;
1959
1960 let n = (delta_v - w * (k * s)) * 2.0;
1964 let n_len = n.length();
1965 if n_len < 1e-12 {
1966 return Ok(None);
1967 }
1968 let n_hat = n * (1.0 / n_len);
1969 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
1970
1971 let axis2 = c2.axis();
1977 let scale = 1.0 + delta_v.length();
1978 let mut out = Vec::new();
1979 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
1980 let samples: Vec<Point3> = match &curve {
1981 ExactIntersectionCurve::Circle(c) => (0..4)
1982 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
1983 .collect(),
1984 ExactIntersectionCurve::Ellipse(e) => (0..4)
1985 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
1986 .collect(),
1987 ExactIntersectionCurve::Points(_) => return Ok(None),
1988 };
1989 let on_real_nappe = |p: &Point3| {
1990 let rel = *p - apex2;
1991 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
1992 };
1993 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
1994 match hits {
1995 0 => {}
1996 4 => out.push(curve),
1997 _ => return Ok(None),
1998 }
1999 }
2000 Ok(Some(out))
2001}
2002
2003pub fn exact_cone_cylinder(
2023 cone: &ConicalSurface,
2024 cyl: &CylindricalSurface,
2025) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2026 let axis = cone.axis();
2027 let cyl_axis = cyl.axis();
2028
2029 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2031 return Ok(None);
2032 }
2033 let apex = cone.apex();
2034 let delta = apex - cyl.origin();
2035 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2036 let along = delta_v.dot(cyl_axis);
2037 if (delta_v - cyl_axis * along).length() > 1e-8 {
2038 return Ok(None);
2039 }
2040
2041 let s = cone.half_angle().sin();
2042 if s.abs() < 1e-12 {
2043 return Ok(None); }
2045 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2047 return Ok(None); }
2049
2050 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2052 return Ok(Some(vec![])); }
2054 let center = Point3::new(
2055 apex.x() + axis.x() * t_star,
2056 apex.y() + axis.y() * t_star,
2057 apex.z() + axis.z() * t_star,
2058 );
2059 let circle = Circle3D::new(center, axis, cyl.radius())?;
2060 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2061}
2062
2063fn algebraic_cone_cone(
2072 c1: &ConicalSurface,
2073 c2: &ConicalSurface,
2074) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2075 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2076 return Ok(None);
2077 };
2078 let mut curves = Vec::new();
2079 for exact in exacts {
2080 let n_samples = 33;
2081 let mut positions = Vec::with_capacity(n_samples);
2082 let mut points = Vec::with_capacity(n_samples);
2083 #[allow(clippy::cast_precision_loss)]
2084 for i in 0..n_samples {
2085 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2086 let pt = match &exact {
2087 ExactIntersectionCurve::Circle(circle) => {
2088 crate::traits::ParametricCurve::evaluate(circle, theta)
2089 }
2090 ExactIntersectionCurve::Ellipse(ellipse) => {
2091 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2092 }
2093 ExactIntersectionCurve::Points(_) => break,
2094 };
2095 positions.push(pt);
2096 points.push(IntersectionPoint {
2097 point: pt,
2098 param1: (0.0, 0.0),
2099 param2: (0.0, 0.0),
2100 });
2101 }
2102 if positions.is_empty() {
2103 continue;
2104 }
2105 let degree = 3.min(positions.len() - 1);
2106 let curve = interpolate(&positions, degree)?;
2107 curves.push(IntersectionCurve { curve, points });
2108 }
2109 Ok(Some(curves))
2110}
2111
2112pub fn exact_sphere_cylinder(
2132 sphere: &SphericalSurface,
2133 cyl: &CylindricalSurface,
2134) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2135 let sc = sphere.center();
2136 let r_sphere = sphere.radius();
2137 let co = cyl.origin();
2138 let axis = cyl.axis();
2139 let r_cyl = cyl.radius();
2140
2141 let delta = sc - co;
2143 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2144 let along = delta_vec.dot(axis);
2145 let perp_vec = delta_vec - axis * along;
2146 let d_perp = perp_vec.length();
2147
2148 if d_perp > 1e-7 {
2151 return Ok(None);
2152 }
2153
2154 if r_cyl > r_sphere + 1e-10 {
2157 return Ok(Some(vec![]));
2158 }
2159 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2160 if z_sq < 0.0 {
2161 return Ok(Some(vec![]));
2162 }
2163 let z = z_sq.sqrt();
2164
2165 let center_axis_pt = Point3::new(
2168 co.x() + axis.x() * along,
2169 co.y() + axis.y() * along,
2170 co.z() + axis.z() * along,
2171 );
2172
2173 let mut circles = Vec::new();
2174 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2175 for &z_offset in offsets {
2176 let center = Point3::new(
2177 center_axis_pt.x() + axis.x() * z_offset,
2178 center_axis_pt.y() + axis.y() * z_offset,
2179 center_axis_pt.z() + axis.z() * z_offset,
2180 );
2181 let circle = Circle3D::new(center, axis, r_cyl)?;
2182 circles.push(ExactIntersectionCurve::Circle(circle));
2183 }
2184 Ok(Some(circles))
2185}
2186
2187fn algebraic_sphere_cylinder(
2196 sphere: &SphericalSurface,
2197 cyl: &CylindricalSurface,
2198 sphere_first: bool,
2199) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2200 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2201 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2202 };
2203
2204 let mut curves = Vec::new();
2205 for exact in exacts {
2206 let ExactIntersectionCurve::Circle(circle) = exact else {
2207 continue;
2208 };
2209 let n_samples = 33;
2210 let mut points = Vec::with_capacity(n_samples);
2211 let mut positions = Vec::with_capacity(n_samples);
2212 #[allow(clippy::cast_precision_loss)]
2213 for i in 0..n_samples {
2214 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2215 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2216 positions.push(pt);
2217 let (param1, param2) = in_order(
2218 sphere.project_point(pt),
2219 cyl.project_point(pt),
2220 sphere_first,
2221 );
2222 points.push(IntersectionPoint {
2223 point: pt,
2224 param1,
2225 param2,
2226 });
2227 }
2228 let degree = 3.min(positions.len() - 1);
2229 let curve = interpolate(&positions, degree)?;
2230 curves.push(IntersectionCurve { curve, points });
2231 }
2232
2233 Ok(Some(curves))
2234}
2235
2236fn off_axis_sphere_cylinder(
2245 sphere: &SphericalSurface,
2246 cyl: &CylindricalSurface,
2247 sphere_first: bool,
2248) -> Option<Vec<IntersectionCurve>> {
2249 let (centre, radius) = (sphere.center(), sphere.radius());
2250 let axis = cyl.axis();
2251 let offset = centre - cyl.origin();
2252 let axis_distance = (offset - axis * offset.dot(axis)).length();
2253 let lin_tol = Tolerance::new().linear;
2254 if axis_distance > radius + cyl.radius() + lin_tol
2255 || axis_distance + radius < cyl.radius() - lin_tol
2256 {
2257 return Some(Vec::new());
2258 }
2259 let roots = |u: f64| {
2260 let q = cyl.evaluate(u, 0.0) - centre;
2261 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2262 };
2263 let samples = ruling_samples(cyl, &roots);
2264 let loops = if samples.iter().all(Option::is_some) {
2265 closed_ruling_loops(&samples)
2266 } else {
2267 partial_ruling_loops(cyl, &roots, &samples)
2268 };
2269 if loops.is_empty() {
2270 return None;
2271 }
2272 Some(fit_ruling_loops(&loops, |p| {
2273 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2274 }))
2275}
2276
2277const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2280 if a_first { (a, b) } else { (b, a) }
2281}
2282
2283#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2297fn algebraic_cylinder_cylinder(
2298 c1: &CylindricalSurface,
2299 c2: &CylindricalSurface,
2300) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2301 let alpha = c1.axis().dot(c2.axis());
2302 let a_coeff = 1.0 - alpha * alpha;
2303
2304 if a_coeff.abs() < 1e-12 {
2306 return Ok(None);
2307 }
2308
2309 let r1 = c1.radius();
2310 let r2 = c2.radius();
2311 let o1 = c1.origin();
2312 let o2 = c2.origin();
2313 let a1 = c1.axis();
2314 let a2 = c2.axis();
2315
2316 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2319 let cross = a1.cross(a2);
2320 let cross_len = cross.length();
2321 if cross_len > 1e-12 {
2322 let axis_dist = delta.dot(cross).abs() / cross_len;
2323 if axis_dist > r1 + r2 + Tolerance::new().linear {
2324 return Ok(Some(vec![])); }
2326 }
2327
2328 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2334 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2335 let alpha = sweep.axis().dot(a);
2336 let quad = 1.0 - alpha * alpha;
2337 let (axis, sweep) = (sweep.axis(), sweep.clone());
2338 move |u: f64| {
2339 let q = sweep.evaluate(u, 0.0) - o;
2340 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2341 let b = 2.0 * (q_a1 - alpha * q_a2);
2342 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2343 ruling_quadratic(quad, b, c)
2344 }
2345 };
2346 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2347 let samples1 = ruling_samples(c1, &roots1);
2348 let loops = if samples1.iter().all(Option::is_some) {
2349 closed_ruling_loops(&samples1)
2350 } else {
2351 let samples2 = ruling_samples(c2, &roots2);
2352 if samples2.iter().all(Option::is_some) {
2353 closed_ruling_loops(&samples2)
2354 } else if samples1.iter().any(Option::is_some) {
2355 partial_ruling_loops(c1, &roots1, &samples1)
2356 } else {
2357 partial_ruling_loops(c2, &roots2, &samples2)
2358 }
2359 };
2360 if loops.is_empty() {
2361 return Ok(None);
2362 }
2363 Ok(Some(fit_ruling_loops(&loops, |p| {
2364 (c1.project_point(p), c2.project_point(p))
2365 })))
2366}
2367
2368const RULING_SAMPLES: usize = 128;
2372
2373#[allow(clippy::cast_precision_loss)]
2374fn ruling_u(i: usize) -> f64 {
2375 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2376}
2377
2378fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2380 let disc = b * b - 4.0 * quad * c;
2381 let root = disc.max(0.0).sqrt();
2382 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2383}
2384
2385fn ruling_samples(
2389 sweep: &CylindricalSurface,
2390 roots: &impl Fn(f64) -> (f64, f64, f64),
2391) -> Vec<Option<(Point3, Point3)>> {
2392 let lin_tol = Tolerance::new().linear;
2393 (0..RULING_SAMPLES)
2394 .map(|i| {
2395 let u = ruling_u(i);
2396 let (disc, vp, vm) = roots(u);
2397 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2398 })
2399 .collect()
2400}
2401
2402fn closed_ruling_loops(samples: &[Option<(Point3, Point3)>]) -> Vec<Vec<Point3>> {
2404 let mut plus: Vec<Point3> = samples.iter().flatten().map(|s| s.0).collect();
2405 let mut minus: Vec<Point3> = samples.iter().flatten().map(|s| s.1).collect();
2406 plus.push(plus[0]);
2407 minus.push(minus[0]);
2408 vec![plus, minus]
2409}
2410
2411fn partial_ruling_loops(
2416 sweep: &CylindricalSurface,
2417 roots: &impl Fn(f64) -> (f64, f64, f64),
2418 samples: &[Option<(Point3, Point3)>],
2419) -> Vec<Vec<Point3>> {
2420 let branch_point = |inside: usize, outside: usize| -> Point3 {
2421 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
2422 if (hi - lo).abs() > std::f64::consts::PI {
2423 hi += if hi < lo { TAU } else { -TAU };
2424 }
2425 for _ in 0..60 {
2426 let mid = 0.5 * (lo + hi);
2427 if roots(mid).0 >= 0.0 {
2428 lo = mid;
2429 } else {
2430 hi = mid;
2431 }
2432 }
2433 let (_, vp, vm) = roots(lo);
2434 sweep.evaluate(lo, 0.5 * (vp + vm))
2435 };
2436 let Some(first_gap) = samples.iter().position(Option::is_none) else {
2437 return Vec::new();
2438 };
2439 let mut loops = Vec::new();
2440 let mut k = 0;
2441 while k < RULING_SAMPLES {
2442 let i = (first_gap + k) % RULING_SAMPLES;
2443 if samples[i].is_none() {
2444 k += 1;
2445 continue;
2446 }
2447 let start = i;
2448 let mut run = Vec::new();
2449 while k < RULING_SAMPLES {
2450 let j = (first_gap + k) % RULING_SAMPLES;
2451 let Some(pair) = samples[j] else { break };
2452 run.push(pair);
2453 k += 1;
2454 }
2455 let end = (start + run.len() - 1) % RULING_SAMPLES;
2456 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
2457 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
2458 let mut pts = vec![head];
2459 pts.extend(run.iter().map(|p| p.0));
2460 pts.push(tail);
2461 pts.extend(run.iter().rev().map(|p| p.1));
2462 pts.push(head);
2463 loops.push(pts);
2464 }
2465 loops
2466}
2467
2468fn fit_ruling_loops(
2471 loops: &[Vec<Point3>],
2472 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
2473) -> Vec<IntersectionCurve> {
2474 let mut curves = Vec::new();
2475 for pts in loops {
2476 if pts.len() < 4 {
2477 continue;
2478 }
2479 let ipts: Vec<IntersectionPoint> = pts
2480 .iter()
2481 .map(|&p| {
2482 let (param1, param2) = params(p);
2483 IntersectionPoint {
2484 point: p,
2485 param1,
2486 param2,
2487 }
2488 })
2489 .collect();
2490 let degree = 3.min(pts.len() - 1);
2491 if let Ok(curve) = interpolate(pts, degree) {
2492 curves.push(IntersectionCurve {
2493 curve,
2494 points: ipts,
2495 });
2496 }
2497 }
2498 curves
2499}
2500
2501#[allow(clippy::unnecessary_wraps)]
2527fn algebraic_parallel_cone_cylinder(
2528 cone: &ConicalSurface,
2529 cyl: &CylindricalSurface,
2530 v_range_cone: Option<(f64, f64)>,
2531 v_range_cyl: Option<(f64, f64)>,
2532) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2533 let axis = cone.axis();
2534 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
2535 return Ok(None); }
2537
2538 let apex = cone.apex();
2539 let delta = cyl.origin() - apex;
2540 let along = delta.dot(axis);
2541 let perp = delta - axis * along;
2542 let d = perp.length();
2543 if d < 1e-9 {
2544 return Ok(None); }
2546
2547 let (e1, e2) = (cone.x_axis(), cone.y_axis());
2548 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
2549
2550 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2551 if cos_t < 1e-12 || sin_t < 1e-12 {
2552 return Ok(None);
2553 }
2554 let r = cyl.radius();
2555
2556 let mut v_min = (d - r).abs() / cos_t;
2558 let mut v_max = (d + r) / cos_t;
2559 if v_max <= v_min {
2560 return Ok(Some(vec![]));
2561 }
2562
2563 let mut lo = v_min;
2569 let mut hi = v_max;
2570 if let Some((a, b)) = v_range_cone {
2575 let (a, b) = if a <= b { (a, b) } else { (b, a) };
2576 lo = lo.max(a);
2577 hi = hi.min(b);
2578 }
2579 if let Some((a, b)) = v_range_cyl {
2580 let flip = cyl.axis().dot(axis);
2583 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
2584 let (a, b) = (to_cone_v(a), to_cone_v(b));
2585 let (a, b) = if a <= b { (a, b) } else { (b, a) };
2586 lo = lo.max(a);
2587 hi = hi.min(b);
2588 }
2589 v_min = lo.max(v_min);
2590 v_max = hi.min(v_max);
2591 if v_max - v_min <= 1e-12 {
2592 return Ok(Some(vec![]));
2593 }
2594
2595 let n_samples = 128;
2596 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
2597 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
2598 #[allow(clippy::cast_precision_loss)]
2599 for i in 0..=n_samples {
2600 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
2601 let rho = v * cos_t;
2602 if rho < 1e-12 {
2603 if (d - r).abs() < 1e-12 {
2611 let apex = cone.evaluate(phi0, v);
2612 plus.push(apex);
2613 minus.push(apex);
2614 }
2615 continue;
2616 }
2617 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
2618 let alpha = cos_alpha.acos();
2619 plus.push(cone.evaluate(phi0 + alpha, v));
2620 minus.push(cone.evaluate(phi0 - alpha, v));
2621 }
2622
2623 let mut curves = Vec::new();
2624 for pts in [&plus, &minus] {
2625 if pts.len() < 4 {
2628 continue;
2629 }
2630 let ipts: Vec<IntersectionPoint> = pts
2631 .iter()
2632 .map(|&p| IntersectionPoint {
2633 point: p,
2634 param1: cone.project_point(p),
2635 param2: cyl.project_point(p),
2636 })
2637 .collect();
2638 let degree = 3.min(pts.len() - 1);
2639 match interpolate(pts, degree) {
2640 Ok(curve) => curves.push(IntersectionCurve {
2641 curve,
2642 points: ipts,
2643 }),
2644 Err(_) => return Ok(None),
2649 }
2650 }
2651
2652 Ok(Some(curves))
2653}
2654
2655fn algebraic_sphere_sphere(
2663 s1: &SphericalSurface,
2664 s2: &SphericalSurface,
2665) -> Result<Vec<IntersectionCurve>, MathError> {
2666 let c1 = s1.center();
2667 let c2 = s2.center();
2668 let r1 = s1.radius();
2669 let r2 = s2.radius();
2670
2671 let delta = c2 - c1;
2672 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
2673 let d = d_sq.sqrt();
2674
2675 if d < 1e-12 {
2676 return Ok(vec![]);
2678 }
2679
2680 if d > r1 + r2 + 1e-10 {
2682 return Ok(vec![]); }
2684 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
2685 return Ok(vec![]); }
2687
2688 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
2690
2691 let r_circle_sq = r1 * r1 - d1 * d1;
2693 if r_circle_sq < 0.0 {
2694 if r_circle_sq > -1e-10 {
2696 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2698 let tangent_pt = Point3::new(
2699 c1.x() + axis.x() * d1,
2700 c1.y() + axis.y() * d1,
2701 c1.z() + axis.z() * d1,
2702 );
2703 let ipt = IntersectionPoint {
2704 point: tangent_pt,
2705 param1: (0.0, 0.0),
2706 param2: (0.0, 0.0),
2707 };
2708 return Ok(vec![IntersectionCurve {
2710 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
2711 points: vec![ipt],
2712 }]);
2713 }
2714 return Ok(vec![]);
2715 }
2716
2717 let r_circle = r_circle_sq.sqrt();
2718 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2719 let center = Point3::new(
2720 c1.x() + axis.x() * d1,
2721 c1.y() + axis.y() * d1,
2722 c1.z() + axis.z() * d1,
2723 );
2724
2725 let basis = Frame3::from_normal(center, axis)?;
2727 let u_dir = basis.x;
2728 let v_dir = basis.y;
2729
2730 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
2733 let mut positions = Vec::with_capacity(n_samples);
2734 #[allow(clippy::cast_precision_loss)]
2735 for i in 0..n_samples {
2736 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2737 let (sin_t, cos_t) = theta.sin_cos();
2738 let pt = Point3::new(
2739 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
2740 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
2741 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
2742 );
2743 positions.push(pt);
2744 points.push(IntersectionPoint {
2745 point: pt,
2746 param1: (0.0, 0.0),
2747 param2: (0.0, 0.0),
2748 });
2749 }
2750
2751 let degree = 3.min(positions.len() - 1);
2752 let curve = interpolate(&positions, degree)?;
2753
2754 Ok(vec![IntersectionCurve { curve, points }])
2755}
2756
2757#[allow(clippy::too_many_arguments)]
2763fn correct_to_intersection(
2764 a: &AnalyticSurface<'_>,
2765 b: &AnalyticSurface<'_>,
2766 surf_a: &dyn Fn(f64, f64) -> Point3,
2767 norm_a: &dyn Fn(f64, f64) -> Vec3,
2768 surf_b: &dyn Fn(f64, f64) -> Point3,
2769 norm_b: &dyn Fn(f64, f64) -> Vec3,
2770 point: Point3,
2771 u_range_a: (f64, f64),
2772 v_range_a: (f64, f64),
2773 u_range_b: (f64, f64),
2774 v_range_b: (f64, f64),
2775 max_iters: usize,
2776) -> Point3 {
2777 let mut p = point;
2778 for _ in 0..max_iters {
2779 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
2780 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
2781 let pa = surf_a(ua, va);
2782 let pb = surf_b(ub, vb);
2783 let na = norm_a(ua, va);
2784 let nb = norm_b(ub, vb);
2785 let pv = Vec3::new(p.x(), p.y(), p.z());
2786
2787 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
2788 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
2789
2790 if da.abs() < 1e-7 && db.abs() < 1e-7 {
2791 break;
2792 }
2793
2794 let t = na.cross(nb);
2795 let t_len = t.length();
2796 if t_len < 1e-10 {
2797 return Point3::new(
2799 (pa.x() + pb.x()) * 0.5,
2800 (pa.y() + pb.y()) * 0.5,
2801 (pa.z() + pb.z()) * 0.5,
2802 );
2803 }
2804 let t_hat = t * (1.0 / t_len);
2805
2806 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2808 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2809 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
2810 if det.abs() < 1e-15 {
2811 return Point3::new(
2812 (pa.x() + pb.x()) * 0.5,
2813 (pa.y() + pb.y()) * 0.5,
2814 (pa.z() + pb.z()) * 0.5,
2815 );
2816 }
2817 let inv = 1.0 / det;
2818 let dx = inv
2820 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2821 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
2822 let dy = inv
2823 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2824 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
2825 let dz = inv
2826 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
2827 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
2828 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
2829
2830 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
2833 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
2834 let pc_a = surf_a(uc, vc);
2835 let pc_b = surf_b(ud, vd);
2836 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
2837 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
2838 .dot(norm_a(uc, vc))
2839 .abs();
2840 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
2841 .dot(norm_b(ud, vd))
2842 .abs();
2843 if da_new > da.abs() && db_new > db.abs() {
2844 return p;
2845 }
2846
2847 p = candidate;
2848 }
2849 p
2850}
2851
2852#[allow(clippy::too_many_arguments)]
2858fn march_analytic_intersection(
2859 a: &AnalyticSurface<'_>,
2860 b: &AnalyticSurface<'_>,
2861 surf_a: &dyn Fn(f64, f64) -> Point3,
2862 norm_a: &dyn Fn(f64, f64) -> Vec3,
2863 surf_b: &dyn Fn(f64, f64) -> Point3,
2864 norm_b: &dyn Fn(f64, f64) -> Vec3,
2865 seed: Point3,
2866 u_range_a: (f64, f64),
2867 v_range_a: (f64, f64),
2868 u_range_b: (f64, f64),
2869 v_range_b: (f64, f64),
2870 initial_step: f64,
2871 u_periodic_a: bool,
2872 u_periodic_b: bool,
2873) -> Vec<Point3> {
2874 let max_steps = 500;
2875 let h_min = 1e-6;
2876 let h_max = initial_step * 4.0;
2877 let closure_dist = initial_step * 5.0;
2881 let max_angle = 10.0_f64.to_radians();
2883 let min_angle = 2.0_f64.to_radians();
2884
2885 let mut forward = Vec::new();
2887 let mut backward = Vec::new();
2889
2890 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
2891 let mut current = seed;
2892 let mut h = initial_step;
2893 let mut prev_tangent: Option<Vec3> = None;
2894
2895 for _ in 0..max_steps {
2896 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
2897 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
2898
2899 let na = norm_a(ua, va);
2900 let nb = norm_b(ub, vb);
2901
2902 let tangent = na.cross(nb);
2903 let t_len = tangent.length();
2904 if t_len < 1e-10 {
2905 break;
2906 }
2907 let t_dir = tangent * (direction / t_len);
2908
2909 if let Some(prev_t) = prev_tangent {
2911 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
2912 let angle = cos_angle.acos();
2913 if angle > max_angle && h > h_min {
2914 h = (h * 0.5).max(h_min);
2915 } else if angle < min_angle {
2916 h = (h * 2.0).min(h_max);
2917 }
2918 }
2919 prev_tangent = Some(t_dir);
2920
2921 let next = Point3::new(
2922 h.mul_add(t_dir.x(), current.x()),
2923 h.mul_add(t_dir.y(), current.y()),
2924 h.mul_add(t_dir.z(), current.z()),
2925 );
2926
2927 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
2928 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
2929
2930 let pa = surf_a(ua2, va2);
2931 let pb = surf_b(ub2, vb2);
2932 let mid = Point3::new(
2933 (pa.x() + pb.x()) * 0.5,
2934 (pa.y() + pb.y()) * 0.5,
2935 (pa.z() + pb.z()) * 0.5,
2936 );
2937 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
2938 || va2 <= v_range_a.0
2939 || va2 >= v_range_a.1;
2940 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
2941 || vb2 <= v_range_b.0
2942 || vb2 >= v_range_b.1;
2943
2944 if out_a || out_b {
2945 break;
2946 }
2947
2948 let dist_to_seed = (mid - seed).length();
2952 if points.len() > 10 && dist_to_seed < closure_dist {
2953 points.push(seed);
2954 break;
2955 }
2956
2957 points.push(mid);
2958 current = mid;
2959 }
2960 }
2961
2962 backward.reverse();
2964 let mut result = backward;
2965 result.push(seed);
2966 result.append(&mut forward);
2967
2968 for pt in &mut result {
2970 *pt = correct_to_intersection(
2971 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
2972 5,
2973 );
2974 }
2975
2976 result
2977}
2978
2979fn project_analytic(
2983 surface: &AnalyticSurface<'_>,
2984 point: Point3,
2985 u_range: (f64, f64),
2986 v_range: (f64, f64),
2987) -> (f64, f64) {
2988 match surface {
2989 AnalyticSurface::Cylinder(cyl) => {
2990 let (u, v) = cyl.project_point(point);
2991 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2992 }
2993 AnalyticSurface::Sphere(sphere) => {
2994 let (u, v) = sphere.project_point(point);
2995 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2996 }
2997 AnalyticSurface::Cone(cone) => {
2998 let (u, v) = cone.project_point(point);
2999 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3000 }
3001 AnalyticSurface::Torus(torus) => {
3002 let (u, v) = torus.project_point(point);
3003 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3004 }
3005 }
3006}
3007
3008fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3012 matches!(
3013 surface,
3014 AnalyticSurface::Cylinder(_)
3015 | AnalyticSurface::Cone(_)
3016 | AnalyticSurface::Sphere(_)
3017 | AnalyticSurface::Torus(_)
3018 )
3019}
3020
3021#[allow(clippy::type_complexity)]
3023fn surface_closures<'a>(
3024 surface: &'a AnalyticSurface<'a>,
3025) -> (
3026 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3027 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3028 (f64, f64),
3029 (f64, f64),
3030) {
3031 match surface {
3032 AnalyticSurface::Cylinder(cyl) => (
3033 Box::new(|u, v| cyl.evaluate(u, v)),
3034 Box::new(|u, v| cyl.normal(u, v)),
3035 (0.0, TAU),
3036 (-1.0, 1.0),
3037 ),
3038 AnalyticSurface::Cone(cone) => (
3039 Box::new(|u, v| cone.evaluate(u, v)),
3040 Box::new(|u, v| cone.normal(u, v)),
3041 (0.0, TAU),
3042 (0.01, 2.0),
3043 ),
3044 AnalyticSurface::Sphere(sphere) => (
3045 Box::new(|u, v| sphere.evaluate(u, v)),
3046 Box::new(|u, v| sphere.normal(u, v)),
3047 (0.0, TAU),
3048 (-FRAC_PI_2, FRAC_PI_2),
3049 ),
3050 AnalyticSurface::Torus(torus) => (
3051 Box::new(|u, v| torus.evaluate(u, v)),
3052 Box::new(|u, v| torus.normal(u, v)),
3053 (0.0, TAU),
3054 (0.0, TAU),
3055 ),
3056 }
3057}
3058
3059#[cfg(test)]
3060#[allow(clippy::unwrap_used, clippy::expect_used)]
3061mod tests {
3062 use super::*;
3063 use crate::tolerance::Tolerance;
3064
3065 #[test]
3066 fn plane_cylinder_perpendicular() {
3067 let cyl =
3068 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3069 .unwrap();
3070
3071 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3073 assert!(!curves.is_empty(), "should find intersection curve");
3074 assert!(
3075 curves[0].points.len() > 10,
3076 "should have many sample points"
3077 );
3078
3079 let tol = Tolerance::loose();
3080 for pt in &curves[0].points {
3081 assert!(
3082 tol.approx_eq(pt.point.z(), 3.0),
3083 "z should be ~3.0, got {}",
3084 pt.point.z()
3085 );
3086 let r = pt.point.x().hypot(pt.point.y());
3087 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3088 }
3089 }
3090
3091 #[test]
3092 fn plane_sphere_equator() {
3093 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3094
3095 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3096 assert!(!curves.is_empty());
3097
3098 let tol = Tolerance::loose();
3099 for pt in &curves[0].points {
3100 assert!(
3101 tol.approx_eq(pt.point.z(), 0.0),
3102 "z should be ~0, got {}",
3103 pt.point.z()
3104 );
3105 let r = pt.point.x().hypot(pt.point.y());
3106 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3107 }
3108 }
3109
3110 #[test]
3111 fn plane_sphere_no_intersection() {
3112 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3113
3114 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3115 assert!(curves.is_empty());
3116 }
3117
3118 #[test]
3119 fn plane_cone_cross_section() {
3120 let cone = ConicalSurface::new(
3121 Point3::new(0.0, 0.0, 0.0),
3122 Vec3::new(0.0, 0.0, 1.0),
3123 std::f64::consts::FRAC_PI_4,
3124 )
3125 .unwrap();
3126
3127 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3128 assert!(!curves.is_empty(), "should find intersection with cone");
3129 }
3130
3131 #[test]
3138 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3139 let c1 = ConicalSurface::new(
3140 Point3::new(
3141 -16.999_999_999_999_975,
3142 -16.999_999_999_999_975,
3143 5.849_999_999_999_951,
3144 ),
3145 Vec3::new(0.0, 0.0, -1.0),
3146 0.785_398_163_397_433_5,
3147 )
3148 .unwrap();
3149 let c2 = ConicalSurface::new(
3150 Point3::new(
3151 -16.750_000_000_000_036,
3152 -16.750_000_000_000_018,
3153 0.749_999_999_999_881,
3154 ),
3155 Vec3::new(0.0, 0.0, 1.0),
3156 0.785_398_163_397_467_6,
3157 )
3158 .unwrap();
3159
3160 let curves = exact_cone_cone(&c1, &c2)
3161 .unwrap()
3162 .expect("offset parallel equal-angle cones must take the radical-plane path");
3163 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3164 assert!(
3165 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3166 "expected an ellipse section, got {:?}",
3167 curves[0]
3168 );
3169 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3170 return;
3171 };
3172
3173 for i in 0..16 {
3177 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3178 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3179 let rel = p - cone.apex();
3180 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3181 let axial = rel_v.dot(cone.axis());
3182 let radial = (rel_v - cone.axis() * axial).length();
3183 assert!(
3184 axial > 0.0,
3185 "{label}: sample on phantom nappe (axial {axial})"
3186 );
3187 let expect = cone.half_angle().tan() * axial;
3188 assert!(
3189 (radial - expect).abs() < 1e-9,
3190 "{label}: sample off surface by {}",
3191 (radial - expect).abs()
3192 );
3193 }
3194 }
3195 }
3196
3197 #[test]
3201 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3202 let c1 = ConicalSurface::new(
3203 Point3::new(0.0, 0.0, 5.0),
3204 Vec3::new(0.0, 0.0, -1.0),
3205 std::f64::consts::FRAC_PI_4,
3206 )
3207 .unwrap();
3208 let c2 = ConicalSurface::new(
3209 Point3::new(0.25, 0.25, 20.0),
3210 Vec3::new(0.0, 0.0, 1.0),
3211 std::f64::consts::FRAC_PI_4,
3212 )
3213 .unwrap();
3214 let curves = exact_cone_cone(&c1, &c2)
3215 .unwrap()
3216 .expect("radical-plane path");
3217 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3218 }
3219
3220 #[test]
3223 fn offset_parallel_cones_with_unequal_angles_defer() {
3224 let c1 = ConicalSurface::new(
3225 Point3::new(0.0, 0.0, 5.0),
3226 Vec3::new(0.0, 0.0, -1.0),
3227 std::f64::consts::FRAC_PI_4,
3228 )
3229 .unwrap();
3230 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
3231 .unwrap();
3232 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
3233 }
3234
3235 #[test]
3236 fn coaxial_cones_cross_at_single_circle() {
3237 let outer = ConicalSurface::new(
3242 Point3::new(0.0, 0.0, 50.0),
3243 Vec3::new(0.0, 0.0, -1.0),
3244 5.0_f64.atan(),
3245 )
3246 .unwrap();
3247 let inner = ConicalSurface::new(
3248 Point3::new(0.0, 0.0, 90.0),
3249 Vec3::new(0.0, 0.0, -1.0),
3250 10.0_f64.atan(),
3251 )
3252 .unwrap();
3253
3254 let curves = intersect_analytic_analytic_bounded(
3255 AnalyticSurface::Cone(&outer),
3256 AnalyticSurface::Cone(&inner),
3257 32,
3258 None,
3259 None,
3260 )
3261 .unwrap();
3262
3263 assert_eq!(
3264 curves.len(),
3265 1,
3266 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
3267 curves.len()
3268 );
3269 for p in &curves[0].points {
3270 let r = p.point.x().hypot(p.point.y());
3271 assert!(
3272 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
3273 "intersection point off the expected z=10,r=8 circle: {:?}",
3274 p.point
3275 );
3276 }
3277 }
3278
3279 #[test]
3280 fn plane_torus_cross_section() {
3281 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
3282
3283 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3284 assert!(
3285 !curves.is_empty(),
3286 "should find intersection curves with torus"
3287 );
3288 }
3289
3290 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
3293 let rho = p.x().hypot(p.y());
3294 ((rho - major).hypot(p.z())) - minor
3295 }
3296
3297 #[test]
3303 fn parallel_cone_cylinder_gives_two_exact_branches() {
3304 use crate::traits::ParametricCurve;
3305 let cone = ConicalSurface::new(
3306 Point3::new(-5.45, -36.55, -4.85),
3307 Vec3::new(0.0, 0.0, 1.0),
3308 std::f64::consts::FRAC_PI_4,
3309 )
3310 .unwrap();
3311 let cyl = CylindricalSurface::new(
3312 Point3::new(-8.0, -34.0, -5.0),
3313 Vec3::new(0.0, 0.0, 1.0),
3314 4.45,
3315 )
3316 .unwrap();
3317 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
3319 let curves = intersect_analytic_analytic_bounded(
3320 AnalyticSurface::Cone(&cone),
3321 AnalyticSurface::Cylinder(&cyl),
3322 32,
3323 Some(v_hint),
3324 Some((0.0, 2.5)),
3325 )
3326 .unwrap();
3327
3328 assert_eq!(curves.len(), 2, "expected exactly the two branches");
3329 for c in &curves {
3330 let (t0, t1) = c.curve.domain();
3331 for k in 0..=32 {
3332 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
3333 let p = ParametricCurve::evaluate(&c.curve, t);
3334 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
3336 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
3337 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
3339 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
3340 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
3342 }
3343 }
3344 }
3345
3346 #[test]
3349 fn coaxial_cone_cylinder_defers_to_other_paths() {
3350 let cone = ConicalSurface::new(
3351 Point3::new(0.0, 0.0, 0.0),
3352 Vec3::new(0.0, 0.0, 1.0),
3353 std::f64::consts::FRAC_PI_4,
3354 )
3355 .unwrap();
3356 let cyl =
3357 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3358 .unwrap();
3359 assert!(
3360 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
3361 .unwrap()
3362 .is_none()
3363 );
3364 }
3365
3366 #[test]
3367 fn oblique_cone_cylinder_defers_to_other_paths() {
3368 let cone = ConicalSurface::new(
3369 Point3::new(0.0, 0.0, 0.0),
3370 Vec3::new(0.0, 0.0, 1.0),
3371 std::f64::consts::FRAC_PI_4,
3372 )
3373 .unwrap();
3374 let cyl =
3375 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3376 .unwrap();
3377 assert!(
3378 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
3379 .unwrap()
3380 .is_none()
3381 );
3382 }
3383
3384 #[test]
3385 fn plane_torus_lobe_closes_and_stays_on_surface() {
3386 use crate::traits::ParametricCurve;
3387 let (major, minor) = (10.0, 3.0);
3388 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
3389
3390 for (n, d) in [
3394 (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), ] {
3398 let curves = intersect_plane_torus(&torus, n, d).unwrap();
3399 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
3400 for c in &curves {
3401 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
3402 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
3403 assert!(
3404 (p0 - p1).length() < 1e-7,
3405 "lobe not closed: gap={} (n={n:?} d={d})",
3406 (p0 - p1).length()
3407 );
3408 for k in 0..=64 {
3410 let t = f64::from(k) / 64.0;
3411 let p = ParametricCurve::evaluate(&c.curve, t);
3412 assert!(
3413 torus_implicit(p, major, minor).abs() < 1e-2,
3414 "off-surface point {p:?} implicit={}",
3415 torus_implicit(p, major, minor)
3416 );
3417 }
3418 }
3419 }
3420 }
3421
3422 #[test]
3423 fn plane_torus_inner_tangent_figure_eight_stays_open() {
3424 use crate::traits::ParametricCurve;
3425 let (major, minor) = (10.0, 3.0);
3426 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
3427
3428 let curves =
3434 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
3435 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
3436 let max_gap = curves
3437 .iter()
3438 .map(|c| {
3439 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
3440 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
3441 (p0 - p1).length()
3442 })
3443 .fold(0.0_f64, f64::max);
3444 assert!(
3445 max_gap > 1e-2,
3446 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
3447 );
3448 }
3449
3450 #[test]
3451 fn line_torus_box_edge_crossing_is_exact() {
3452 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
3455 let ts = intersect_line_torus(
3456 &torus,
3457 Point3::new(6.0, -4.0, -5.0),
3458 Vec3::new(0.0, 0.0, 1.0),
3459 );
3460 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
3462 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
3463 let rho = 6.0_f64.hypot(4.0);
3464 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
3465 assert!(
3466 (zs[0] - (-z_exp)).abs() < 1e-9,
3467 "z0={} exp={}",
3468 zs[0],
3469 -z_exp
3470 );
3471 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
3472 for &t in &ts {
3474 let p = Point3::new(6.0, -4.0, -5.0 + t);
3475 let rho = p.x().hypot(p.y());
3476 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
3477 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
3478 }
3479 }
3480
3481 #[test]
3482 fn line_torus_miss_and_tangent() {
3483 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
3484 let miss = intersect_line_torus(
3486 &torus,
3487 Point3::new(20.0, 0.0, 0.0),
3488 Vec3::new(0.0, 0.0, 1.0),
3489 );
3490 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
3491 let axis =
3493 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
3494 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
3495 }
3496
3497 #[test]
3498 fn dispatch_via_analytic_surface() {
3499 let cyl =
3500 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3501 .unwrap();
3502 let curves = intersect_plane_analytic(
3503 AnalyticSurface::Cylinder(&cyl),
3504 Vec3::new(0.0, 0.0, 1.0),
3505 0.0,
3506 )
3507 .unwrap();
3508 assert!(!curves.is_empty());
3509 }
3510
3511 #[test]
3512 fn perpendicular_cylinders_intersect() {
3513 let cyl_z =
3514 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3515 .unwrap();
3516 let cyl_x =
3517 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3518 .unwrap();
3519
3520 let curves = intersect_analytic_analytic(
3521 AnalyticSurface::Cylinder(&cyl_z),
3522 AnalyticSurface::Cylinder(&cyl_x),
3523 16,
3524 )
3525 .unwrap();
3526
3527 assert!(
3528 !curves.is_empty(),
3529 "perpendicular cylinders should intersect"
3530 );
3531
3532 for c in &curves {
3533 assert!(
3534 c.points.len() >= 2,
3535 "intersection curve should have >= 2 points, got {}",
3536 c.points.len()
3537 );
3538 }
3539 }
3540
3541 #[test]
3544 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
3545 let cyl_z =
3546 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3547 .unwrap();
3548 let cyl_x =
3549 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3550 .unwrap();
3551 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
3552 .unwrap()
3553 .unwrap();
3554 assert_eq!(curves.len(), 1);
3555 let curve = &curves[0].curve;
3556 let (t0, t1) = curve.domain();
3557 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
3558 let off = |p: Point3| {
3559 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
3560 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
3561 on_z.max(on_x)
3562 };
3563 let worst = (0..=400)
3564 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
3565 .fold(0.0, f64::max);
3566 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
3567 }
3568
3569 #[test]
3573 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
3574 let cyl_z =
3575 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3576 .unwrap();
3577 let cyl_x =
3578 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
3579 .unwrap();
3580 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
3581 .unwrap()
3582 .expect("the thin cylinder's sweep finds the loop");
3583 assert_eq!(curves.len(), 1);
3584 }
3585
3586 #[test]
3587 fn sphere_cylinder_intersect() {
3588 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
3589 let cyl =
3590 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3591 .unwrap();
3592
3593 let curves = intersect_analytic_analytic(
3594 AnalyticSurface::Sphere(&sphere),
3595 AnalyticSurface::Cylinder(&cyl),
3596 16,
3597 )
3598 .unwrap();
3599
3600 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
3604 }
3605
3606 #[test]
3607 fn exact_sphere_cylinder_coaxial_two_circles() {
3608 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
3611 let cyl =
3612 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
3613 .unwrap();
3614 let circles = exact_sphere_cylinder(&sphere, &cyl)
3615 .unwrap()
3616 .expect("coaxial case returns Some");
3617 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
3618 let mut zs: Vec<f64> = circles
3619 .iter()
3620 .filter_map(|c| match c {
3621 ExactIntersectionCurve::Circle(circle) => {
3622 assert!(
3623 (circle.radius() - 3.0).abs() < 1e-9,
3624 "rim radius == cyl radius"
3625 );
3626 Some(circle.center().z())
3627 }
3628 _ => None,
3629 })
3630 .collect();
3631 assert_eq!(zs.len(), 2, "both sections must be exact circles");
3632 zs.sort_by(f64::total_cmp);
3633 let z = 27.0_f64.sqrt();
3634 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
3635 }
3636
3637 #[test]
3638 fn exact_sphere_cylinder_non_coaxial_defers() {
3639 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
3641 let cyl =
3642 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
3643 .unwrap();
3644 assert!(
3645 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
3646 "non-coaxial sphere/cylinder defers to the marcher"
3647 );
3648 }
3649
3650 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
3652 curves
3653 .iter()
3654 .filter_map(|c| match c {
3655 ExactIntersectionCurve::Circle(circle) => Some(circle),
3656 _ => None,
3657 })
3658 .collect()
3659 }
3660
3661 fn worst_off(
3664 circles: &[&Circle3D],
3665 torus: &ToroidalSurface,
3666 other: impl Fn(Point3) -> f64,
3667 ) -> f64 {
3668 let mut worst = 0.0_f64;
3669 for circle in circles {
3670 for k in 0..16 {
3671 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
3672 let q = p - torus.center();
3673 let along = q.dot(torus.z_axis());
3674 let rho = (q - torus.z_axis() * along).length();
3675 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
3676 worst = worst.max(off).max(other(p).abs());
3677 }
3678 }
3679 worst
3680 }
3681
3682 #[test]
3683 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
3684 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
3685 for height in [0.0, 1.0] {
3686 let centre = Point3::new(0.0, 0.0, height);
3687 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
3688 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
3689 let circles = circles_of(&curves);
3690 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
3691 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
3692 assert!(worst < 1e-9, "height {height}: {worst}");
3693 }
3694 }
3695
3696 #[test]
3697 fn exact_sphere_torus_misses_touches_and_defers() {
3698 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
3699 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
3700 assert!(
3701 exact_sphere_torus(&ball(0.0, 1.0), &torus)
3702 .unwrap()
3703 .unwrap()
3704 .is_empty(),
3705 "a small ball in the hole misses"
3706 );
3707 assert!(
3708 exact_sphere_torus(&ball(0.0, 2.5), &torus)
3709 .unwrap()
3710 .is_none(),
3711 "a ball touching the inner equator defers"
3712 );
3713 assert!(
3714 exact_sphere_torus(&ball(1.0, 3.0), &torus)
3715 .unwrap()
3716 .is_none(),
3717 "a ball off the axis defers"
3718 );
3719 let spindle = ToroidalSurface::with_axis_and_ref_dir(
3720 Point3::new(0.0, 0.0, 0.0),
3721 1.0,
3722 2.0,
3723 Vec3::new(0.0, 0.0, 1.0),
3724 Vec3::new(1.0, 0.0, 0.0),
3725 )
3726 .unwrap();
3727 assert!(
3728 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
3729 .unwrap()
3730 .is_none()
3731 );
3732 }
3733
3734 #[test]
3735 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
3736 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
3737 let z = Vec3::new(0.0, 0.0, 1.0);
3738 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
3739 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
3740 let circles = circles_of(&curves);
3741 assert_eq!((curves.len(), circles.len()), (2, 2));
3742 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
3743 assert!(worst < 1e-9, "{worst}");
3744 assert!(
3745 exact_cylinder_torus(&rod(2.0), &torus)
3746 .unwrap()
3747 .unwrap()
3748 .is_empty(),
3749 "a rod clear in the hole misses"
3750 );
3751 assert!(
3752 exact_cylinder_torus(&rod(5.5), &torus).unwrap().is_none(),
3753 "a wall touching the outer equator defers"
3754 );
3755 let tilted =
3756 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
3757 .unwrap();
3758 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
3759 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
3760 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
3761 let spindle = ToroidalSurface::with_axis_and_ref_dir(
3762 Point3::new(0.0, 0.0, 0.0),
3763 1.0,
3764 2.0,
3765 z,
3766 Vec3::new(1.0, 0.0, 0.0),
3767 )
3768 .unwrap();
3769 assert!(
3770 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
3771 "a spindle torus's inner lemon also meets the rod"
3772 );
3773 }
3774
3775 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
3778 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
3779 let cyl =
3780 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
3781 .unwrap();
3782 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
3783 .unwrap()
3784 .unwrap();
3785 let mut worst: f64 = 0.0;
3786 for c in &curves {
3787 for ip in &c.points {
3788 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
3789 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
3790 worst = worst
3791 .max((on_sphere - ip.point).length())
3792 .max((on_cylinder - ip.point).length());
3793 }
3794 let (t0, t1) = c.curve.domain();
3795 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
3796 for k in 0..=400 {
3797 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
3798 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
3799 let on_cylinder = ((p.x() - cylinder_origin.x())
3800 .hypot(p.y() - cylinder_origin.y())
3801 - cylinder_radius)
3802 .abs();
3803 worst = worst.max(on_sphere).max(on_cylinder);
3804 }
3805 }
3806 (curves.len(), worst)
3807 }
3808
3809 #[test]
3812 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
3813 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
3814 assert_eq!(count, 2);
3815 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
3816 }
3817
3818 #[test]
3820 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
3821 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
3822 assert_eq!(count, 1);
3823 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
3824 }
3825
3826 #[test]
3827 fn disjoint_cylinders_no_intersection() {
3828 let cyl_a =
3829 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
3830 .unwrap();
3831 let cyl_b =
3832 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
3833 .unwrap();
3834
3835 let curves = intersect_analytic_analytic(
3836 AnalyticSurface::Cylinder(&cyl_a),
3837 AnalyticSurface::Cylinder(&cyl_b),
3838 16,
3839 )
3840 .unwrap();
3841
3842 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
3843 }
3844
3845 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
3849 use crate::traits::ParametricCurve;
3850 match curve {
3851 ExactIntersectionCurve::Circle(c) => (0..=64)
3852 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
3853 .collect(),
3854 ExactIntersectionCurve::Ellipse(e) => (0..=64)
3855 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
3856 .collect(),
3857 ExactIntersectionCurve::Points(pts) => pts.clone(),
3858 }
3859 }
3860
3861 fn assert_on_plane_and_cone(
3864 curves: &[ExactIntersectionCurve],
3865 cone: &ConicalSurface,
3866 n: Vec3,
3867 d: f64,
3868 z_bound: (f64, f64),
3869 ) {
3870 assert!(!curves.is_empty(), "expected at least one section curve");
3871 let mut total = 0;
3872 for curve in curves {
3873 for p in collect_points(curve) {
3874 total += 1;
3875 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
3876 assert!(
3877 plane_err < 1e-9,
3878 "point off plane by {plane_err:.2e}: {p:?}"
3879 );
3880 let (u, v) = cone.project_point(p);
3881 let q = cone.evaluate(u, v);
3882 let cone_err =
3883 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
3884 .sqrt();
3885 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
3886 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
3887 assert!(
3888 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
3889 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
3890 p.z()
3891 );
3892 }
3893 }
3894 assert!(total >= 8, "too few section points ({total})");
3895 }
3896
3897 #[test]
3898 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
3899 let cone = ConicalSurface::new(
3903 Point3::new(0.0, 0.0, 0.0),
3904 Vec3::new(0.0, 0.0, 1.0),
3905 std::f64::consts::FRAC_PI_4,
3906 )
3907 .unwrap();
3908 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
3909 let d = n.z() * 5.0;
3911 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
3912 assert!(
3913 curves
3914 .iter()
3915 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
3916 "oblique steep plane × cone must yield an exact Ellipse"
3917 );
3918 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
3920 }
3921
3922 #[test]
3923 fn oblique_plane_cone_wrong_nappe_is_empty() {
3924 let cone = ConicalSurface::new(
3928 Point3::new(0.0, 0.0, 0.0),
3929 Vec3::new(0.0, 0.0, 1.0),
3930 std::f64::consts::FRAC_PI_4,
3931 )
3932 .unwrap();
3933 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
3934 let d = n.z() * -5.0;
3935 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
3936 assert!(
3937 curves.is_empty(),
3938 "plane on the phantom-nappe side must yield no real curve, got {}",
3939 curves.len()
3940 );
3941 }
3942
3943 #[test]
3944 fn oblique_plane_cone_parabola_on_both_single_branch() {
3945 let cone = ConicalSurface::new(
3948 Point3::new(0.0, 0.0, 0.0),
3949 Vec3::new(0.0, 0.0, 1.0),
3950 std::f64::consts::FRAC_PI_4,
3951 )
3952 .unwrap();
3953 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
3954 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
3956 assert_eq!(
3957 curves.len(),
3958 1,
3959 "a parabola is a single branch, got {}",
3960 curves.len()
3961 );
3962 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
3964 }
3965
3966 #[test]
3967 fn oblique_plane_cone_hyperbola_real_nappe_only() {
3968 let cone = ConicalSurface::new(
3976 Point3::new(-59.0, -59.0, 15.85),
3977 Vec3::new(0.0, 0.0, -1.0),
3978 std::f64::consts::FRAC_PI_4,
3979 )
3980 .unwrap();
3981 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
3982 let d = -58.360_56;
3983 let cos_theta = n.dot(cone.axis()).abs();
3984 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
3985 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
3986 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
3989 for c in &curves {
3991 assert!(
3992 matches!(c, ExactIntersectionCurve::Points(_)),
3993 "hyperbola must be sampled Points, not a closed conic"
3994 );
3995 }
3996 }
3997}