1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::curves::{Circle3D, Ellipse3D};
11use crate::frame::Frame3;
12use crate::nurbs::curve::NurbsCurve;
13use crate::nurbs::fitting::interpolate;
14use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
15use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
16use crate::tolerance::Tolerance;
17use crate::vec::{Point3, Vec3};
18
19#[derive(Debug, Clone)]
21pub enum ExactIntersectionCurve {
22 Circle(Circle3D),
24 Ellipse(Ellipse3D),
26 Points(Vec<Point3>),
28}
29
30pub fn exact_plane_analytic(
41 surface: AnalyticSurface<'_>,
42 plane_normal: Vec3,
43 plane_d: f64,
44) -> Result<Vec<ExactIntersectionCurve>, MathError> {
45 exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
46}
47
48pub fn exact_plane_analytic_reaching(
56 surface: AnalyticSurface<'_>,
57 plane_normal: Vec3,
58 plane_d: f64,
59 reach: f64,
60) -> Result<Vec<ExactIntersectionCurve>, MathError> {
61 match surface {
62 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
63 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
64 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
65 AnalyticSurface::Torus(torus) => {
66 if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
67 return Ok(circles);
68 }
69 if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
70 return Ok(loops
71 .into_iter()
72 .map(ExactIntersectionCurve::Points)
73 .collect());
74 }
75 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
77 Ok(chains
78 .into_iter()
79 .map(ExactIntersectionCurve::Points)
80 .collect())
81 }
82 }
83}
84
85fn exact_plane_torus(
96 torus: &ToroidalSurface,
97 normal: Vec3,
98 d: f64,
99) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
100 let len = normal.length();
101 let n = normal.normalize()?;
102 let d = d / len;
103 let axis = torus.z_axis();
104 let center = torus.center();
105 let (big, small) = (torus.major_radius(), torus.minor_radius());
106 let height = d - dot_np(n, center);
107 let along = n.dot(axis);
108 if along.abs() > 1.0 - 1e-10 {
109 if height.abs() >= small - 1e-10 * small {
110 return Ok(if height.abs() > small + 1e-10 * small {
111 Some(Vec::new())
112 } else {
113 None
114 });
115 }
116 let reach = small.mul_add(small, -(height * height)).sqrt();
117 if big - reach <= 1e-10 * big {
118 return Ok(None);
119 }
120 let middle = center + n * height;
121 return Ok(Some(vec![
122 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
123 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
124 ]));
125 }
126 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
127 let out = axis.cross(n).normalize()?;
128 return Ok(Some(vec![
129 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
130 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
131 ]));
132 }
133 Ok(None)
134}
135
136fn exact_plane_cylinder(
142 cyl: &CylindricalSurface,
143 normal: Vec3,
144 d: f64,
145) -> Result<Vec<ExactIntersectionCurve>, MathError> {
146 let axis = cyl.axis();
147 let cos_theta = normal.dot(axis).abs();
148 let r = cyl.radius();
149
150 if cos_theta < 1e-10 {
151 let chains = sample_plane_cylinder(cyl, normal, d)?;
154 return Ok(chains
155 .into_iter()
156 .map(ExactIntersectionCurve::Points)
157 .collect());
158 }
159
160 let n_dot_axis = normal.dot(axis);
163 let n_dot_origin = dot_np(normal, cyl.origin());
164 let t = (d - n_dot_origin) / n_dot_axis;
165 let center_on_axis = Point3::new(
166 cyl.origin().x() + t * axis.x(),
167 cyl.origin().y() + t * axis.y(),
168 cyl.origin().z() + t * axis.z(),
169 );
170
171 if cos_theta > 1.0 - 1e-10 {
172 let circle = Circle3D::new(center_on_axis, normal, r)?;
174 Ok(vec![ExactIntersectionCurve::Circle(circle)])
175 } else {
176 let semi_minor = r;
180 let semi_major = r / cos_theta;
181
182 let axis_proj = Vec3::new(
186 axis.x() - n_dot_axis * normal.x(),
187 axis.y() - n_dot_axis * normal.y(),
188 axis.z() - n_dot_axis * normal.z(),
189 );
190 let u_axis = axis_proj.normalize()?;
191 let v_axis = normal.cross(u_axis);
192
193 let ellipse = Ellipse3D::with_axes(
194 center_on_axis,
195 normal,
196 semi_major,
197 semi_minor,
198 u_axis,
199 v_axis,
200 )?;
201 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
202 }
203}
204
205fn exact_plane_sphere(
209 sphere: &SphericalSurface,
210 normal: Vec3,
211 d: f64,
212) -> Result<Vec<ExactIntersectionCurve>, MathError> {
213 let h = dot_np(normal, sphere.center()) - d;
214 let r = sphere.radius();
215
216 if h.abs() > r - 1e-10 {
217 return Ok(vec![]);
218 }
219
220 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
221 let circle_center = Point3::new(
222 h.mul_add(-normal.x(), sphere.center().x()),
223 h.mul_add(-normal.y(), sphere.center().y()),
224 h.mul_add(-normal.z(), sphere.center().z()),
225 );
226
227 let circle = Circle3D::new(circle_center, normal, circle_r)?;
228 Ok(vec![ExactIntersectionCurve::Circle(circle)])
229}
230
231fn exact_plane_cone(
240 cone: &ConicalSurface,
241 normal: Vec3,
242 d: f64,
243 reach: f64,
244) -> Result<Vec<ExactIntersectionCurve>, MathError> {
245 let axis = cone.axis();
246 let cos_theta = normal.dot(axis).abs();
247 let half_angle = cone.half_angle();
248
249 if cos_theta > 1.0 - 1e-10 {
250 let n_dot_axis = normal.dot(axis);
253 let n_dot_apex = dot_np(normal, cone.apex());
254 let t = (d - n_dot_apex) / n_dot_axis;
255
256 if t.abs() < 1e-10 {
261 return Ok(vec![]);
262 }
263
264 let center = Point3::new(
265 cone.apex().x() + t * axis.x(),
266 cone.apex().y() + t * axis.y(),
267 cone.apex().z() + t * axis.z(),
268 );
269 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
273 if circle_r < 1e-15 {
274 return Ok(vec![]);
275 }
276
277 let circle = Circle3D::new(center, normal, circle_r)?;
278 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
279 }
280
281 let c = normal.dot(axis);
293 let p2 = (1.0 - c * c).max(0.0);
294 let p = p2.sqrt();
295 let k = half_angle.sin().powi(2);
296 let a_coeff = p2 - k;
297
298 let m = Vec3::new(
300 axis.x() - c * normal.x(),
301 axis.y() - c * normal.y(),
302 axis.z() - c * normal.z(),
303 );
304 let m_len = m.length();
305 if m_len < 1e-12 {
306 let chains = sample_plane_cone(cone, normal, d, reach)?;
309 return Ok(chains
310 .into_iter()
311 .map(ExactIntersectionCurve::Points)
312 .collect());
313 }
314 let e1 = m * (1.0 / m_len);
315 let e2 = normal.cross(e1);
316 let apex = cone.apex();
317 let e = d - dot_np(normal, apex);
318
319 if a_coeff < -1e-9 {
322 let abs_a = -a_coeff; if e * c < 0.0 {
329 return Ok(vec![]);
330 }
331 let s_c = e * c * p / abs_a;
334 let rhs = e * e * k * (1.0 - k) / abs_a;
335 if rhs <= 0.0 {
336 return Ok(vec![]);
337 }
338 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
341 return Ok(vec![]);
342 }
343 let center = apex + normal * e + e1 * s_c;
344 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
345 (semi_s, semi_t, e1, e2)
346 } else {
347 (semi_t, semi_s, e2, e1)
348 };
349 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
350 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
351 }
352
353 let chains = sample_plane_cone(cone, normal, d, reach)?;
356 Ok(chains
357 .into_iter()
358 .map(ExactIntersectionCurve::Points)
359 .collect())
360}
361
362#[allow(clippy::many_single_char_names)]
377pub fn plane_cone_conic_arc(
378 cone: &ConicalSurface,
379 normal: Vec3,
380 d: f64,
381 from: Point3,
382 to: Point3,
383) -> Result<Option<NurbsCurve>, MathError> {
384 let len = normal.length();
385 if len < 1e-15 {
386 return Err(MathError::ZeroVector);
387 }
388 let (normal, d) = (normal * (1.0 / len), d / len);
389 let axis = cone.axis();
390 let c = normal.dot(axis);
391 let p2 = (1.0 - c * c).max(0.0);
392 let p = p2.sqrt();
393 let k = cone.half_angle().sin().powi(2);
394 let a_coeff = p2 - k;
395 let m = Vec3::new(
396 axis.x() - c * normal.x(),
397 axis.y() - c * normal.y(),
398 axis.z() - c * normal.z(),
399 );
400 let m_len = m.length();
401 if m_len < 1e-12 || a_coeff < -1e-9 {
402 return Ok(None);
403 }
404 let e1 = m * (1.0 / m_len);
405 let e2 = normal.cross(e1);
406 let apex = cone.apex();
407 let e = d - dot_np(normal, apex);
408 let origin = apex + normal * e;
409 let plane_st = |q: Point3| {
410 let w = q - origin;
411 (w.dot(e1), w.dot(e2))
412 };
413 let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
414 let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
415 if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
416 return Ok(None);
417 }
418 let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
419 let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
420 let (control, weights) = if a_coeff.abs() <= 1e-9 {
421 let lin = 2.0 * e * c * p;
423 if lin.abs() < 1e-12 * scale {
424 return Ok(None);
425 }
426 let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
427 if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
428 || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
429 {
430 return Ok(None);
431 }
432 let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
433 (vec![from, mid, to], vec![1.0; 3])
434 } else {
435 let s_c = -e * c * p / a_coeff;
437 let r = e * e * k * (1.0 - k) / a_coeff;
438 if r <= 0.0 {
439 return Ok(None);
440 }
441 let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
442 let (x0, x1) = (s0 - s_c, s1 - s_c);
443 if x0 * x1 <= 0.0 {
444 return Ok(None);
445 }
446 let side = x0.signum();
447 let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
448 let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
449 if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
450 return Ok(None);
451 }
452 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
453 let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
454 let mut control = vec![from];
455 let mut weights = vec![1.0];
456 for i in 0..pieces {
457 #[allow(clippy::cast_precision_loss)]
458 let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
459 let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
460 let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
461 let w = half.cosh();
462 control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
463 weights.push(w);
464 control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
465 weights.push(1.0);
466 }
467 (control, weights)
468 };
469 let pieces = (control.len() - 1) / 2;
470 let mut knots = vec![0.0; 3];
471 for i in 1..pieces {
472 #[allow(clippy::cast_precision_loss)]
473 knots.extend([i as f64; 2]);
474 }
475 #[allow(clippy::cast_precision_loss)]
476 knots.extend([pieces as f64; 3]);
477 let curve = NurbsCurve::new(2, knots, control, weights)?;
478 let (sin_a, cos_a) = cone.half_angle().sin_cos();
483 let off_cone = |q: Point3| {
484 let w = q - apex;
485 let h = w.dot(axis);
486 (w - axis * h)
487 .length()
488 .mul_add(sin_a, -(h.abs() * cos_a))
489 .abs()
490 };
491 for i in 0..pieces {
492 for f in [0.25, 0.5, 0.75] {
493 #[allow(clippy::cast_precision_loss)]
494 if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
495 return Ok(None);
496 }
497 }
498 }
499 Ok(Some(curve))
500}
501
502#[derive(Clone, Copy)]
504pub enum AnalyticSurface<'a> {
505 Cylinder(&'a CylindricalSurface),
507 Cone(&'a ConicalSurface),
509 Sphere(&'a SphericalSurface),
511 Torus(&'a ToroidalSurface),
513}
514
515fn dot_np(n: Vec3, p: Point3) -> f64 {
517 n.dot(Vec3::new(p.x(), p.y(), p.z()))
518}
519
520pub fn intersect_plane_analytic(
528 surface: AnalyticSurface<'_>,
529 normal: Vec3,
530 d: f64,
531) -> Result<Vec<IntersectionCurve>, MathError> {
532 match surface {
533 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
534 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
535 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
536 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
537 }
538}
539
540pub fn sample_plane_analytic(
551 surface: AnalyticSurface<'_>,
552 normal: Vec3,
553 d: f64,
554) -> Result<Vec<Vec<Point3>>, MathError> {
555 match surface {
556 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
557 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
558 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
559 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
560 }
561}
562
563#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
565fn sample_plane_cylinder(
566 cyl: &CylindricalSurface,
567 normal: Vec3,
568 d: f64,
569) -> Result<Vec<Vec<Point3>>, MathError> {
570 let n_samples = 64_usize;
571 let mut points = Vec::with_capacity(n_samples + 1);
572
573 for i in 0..=n_samples {
574 let u = TAU * (i as f64) / (n_samples as f64);
575 let base = cyl.evaluate(u, 0.0);
576 let n_dot_axis = normal.dot(cyl.axis());
577 let n_dot_base = dot_np(normal, base);
578
579 if n_dot_axis.abs() < 1e-12 {
580 if (n_dot_base - d).abs() < 1e-6 {
581 points.push(base);
582 }
583 } else {
584 let v = (d - n_dot_base) / n_dot_axis;
585 if v.abs() <= 100.0 {
586 points.push(cyl.evaluate(u, v));
587 }
588 }
589 }
590
591 if points.len() < 2 {
592 Ok(vec![])
593 } else {
594 Ok(vec![points])
595 }
596}
597
598#[allow(clippy::cast_precision_loss)]
600fn sample_plane_sphere(
601 sphere: &SphericalSurface,
602 normal: Vec3,
603 d: f64,
604) -> Result<Vec<Vec<Point3>>, MathError> {
605 let h = dot_np(normal, sphere.center()) - d;
606 let r = sphere.radius();
607
608 if h.abs() > r - 1e-10 {
609 return Ok(vec![]);
610 }
611
612 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
613 let circle_center = Point3::new(
614 h.mul_add(-normal.x(), sphere.center().x()),
615 h.mul_add(-normal.y(), sphere.center().y()),
616 h.mul_add(-normal.z(), sphere.center().z()),
617 );
618
619 let basis = Frame3::from_normal(circle_center, normal)?;
620 let u_dir = basis.x;
621 let v_dir = basis.y;
622
623 let n_samples = 64_usize;
624 let mut points = Vec::with_capacity(n_samples + 1);
625
626 for i in 0..=n_samples {
627 let theta = TAU * (i as f64) / (n_samples as f64);
628 let (sin_t, cos_t) = theta.sin_cos();
629 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
630 }
631
632 Ok(vec![points])
633}
634
635#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
647fn sample_plane_cone(
648 cone: &ConicalSurface,
649 normal: Vec3,
650 d: f64,
651 reach: f64,
652) -> Result<Vec<Vec<Point3>>, MathError> {
653 let apex = cone.apex();
654 let n_dot_apex = dot_np(normal, apex);
655 let e = d - n_dot_apex;
656
657 let n_samples = 512_usize;
661 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
662 let mut v_min = f64::INFINITY;
663 for i in 0..n_samples {
664 let u = TAU * (i as f64) / (n_samples as f64);
665 let g = cone.evaluate(u, 1.0) - apex;
666 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
667 if n_dot_g.abs() < 1e-12 {
668 vs.push(None);
669 continue;
670 }
671 let v = e / n_dot_g;
672 if v >= -1e-12 {
673 let v = v.max(0.0);
674 v_min = v_min.min(v);
675 vs.push(Some(v));
676 } else {
677 vs.push(None);
678 }
679 }
680
681 if !v_min.is_finite() {
682 return Ok(Vec::new());
683 }
684
685 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
694
695 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
698
699 let point_at = |u: f64, v: f64| -> Point3 {
700 let g = cone.evaluate(u, 1.0) - apex;
701 apex + g * v
702 };
703 #[allow(clippy::cast_precision_loss)]
704 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
705 let n_dot_g_at = |u: f64| -> f64 {
706 let g = cone.evaluate(u, 1.0) - apex;
707 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
708 };
709
710 if kept.iter().all(Option::is_some) {
711 let mut pts: Vec<Point3> = kept
713 .iter()
714 .enumerate()
715 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
716 .collect();
717 if let Some(&first) = pts.first() {
718 pts.push(first);
719 }
720 return Ok(vec![pts]);
721 }
722
723 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
732 let Some(v_end) = kept[i_end] else {
733 return Vec::new();
734 };
735 let u_end = u_of(i_end);
736 #[allow(clippy::cast_precision_loss)]
737 let pitch = TAU / (n_samples as f64);
738 let u_next = if forward {
739 u_end + pitch
740 } else {
741 u_end - pitch
742 };
743 let target = e / v_max;
744 let h_end = n_dot_g_at(u_end) - target;
745 let h_next = n_dot_g_at(u_next) - target;
746 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
747 return Vec::new();
748 }
749 let (mut lo, mut hi) = (u_end, u_next);
750 for _ in 0..60 {
751 let mid = f64::midpoint(lo, hi);
752 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
753 lo = mid;
754 } else {
755 hi = mid;
756 }
757 }
758 let u_star = f64::midpoint(lo, hi);
759 let tail_n = 8_usize;
760 (1..=tail_n)
761 .filter_map(|k| {
762 #[allow(clippy::cast_precision_loss)]
763 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
764 let ng = n_dot_g_at(u);
765 if ng.abs() < 1e-12 {
766 return None;
767 }
768 let v = e / ng;
769 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
770 })
771 .collect()
772 };
773
774 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
777 let mut chains: Vec<Vec<Point3>> = Vec::new();
778 let mut run: Vec<usize> = Vec::new();
779 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
780 if run.len() >= 2 {
781 let first = run[0];
782 let last = run[run.len() - 1];
783 let mut pts: Vec<Point3> = tail(first, false, &kept);
784 pts.reverse();
785 pts.extend(
786 run.iter()
787 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
788 );
789 pts.extend(tail(last, true, &kept));
790 chains.push(pts);
791 }
792 run.clear();
793 };
794 for k in 0..n_samples {
795 let idx = (gap + k) % n_samples;
796 if kept[idx].is_some() {
797 run.push(idx);
798 } else {
799 flush(&mut run, &mut chains);
800 }
801 }
802 flush(&mut run, &mut chains);
803 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
804}
805
806#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
812 torus: &ToroidalSurface,
813 normal: Vec3,
814 d: f64,
815) -> Result<Vec<Vec<Point3>>, MathError> {
816 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
817 Ok(chain_torus_crossings(&crossing_pts)
818 .into_iter()
819 .map(|run| run.into_iter().map(|p| p.point).collect())
820 .collect())
821}
822
823#[allow(clippy::cast_precision_loss)]
833pub fn intersect_plane_cylinder(
834 cyl: &CylindricalSurface,
835 normal: Vec3,
836 d: f64,
837) -> Result<Vec<IntersectionCurve>, MathError> {
838 let n_samples = 64_usize;
839 let mut points_3d = Vec::new();
840 let mut ipoints = Vec::new();
841
842 for i in 0..=n_samples {
843 let u = TAU * (i as f64) / (n_samples as f64);
844 let base = cyl.evaluate(u, 0.0);
847 let n_dot_axis = normal.dot(cyl.axis());
848 let n_dot_base = dot_np(normal, base);
849
850 if n_dot_axis.abs() < 1e-12 {
851 if (n_dot_base - d).abs() < 1e-6 {
853 let pt = base;
854 points_3d.push(pt);
855 ipoints.push(IntersectionPoint {
856 point: pt,
857 param1: (u, 0.0),
858 param2: (0.0, 0.0),
859 });
860 }
861 } else {
862 let v = (d - n_dot_base) / n_dot_axis;
863 if v.abs() <= 100.0 {
865 let pt = cyl.evaluate(u, v);
866 points_3d.push(pt);
867 ipoints.push(IntersectionPoint {
868 point: pt,
869 param1: (u, v),
870 param2: (0.0, 0.0),
871 });
872 }
873 }
874 }
875
876 build_curves_from_points(&points_3d, ipoints)
877}
878
879#[allow(clippy::cast_precision_loss)]
888pub fn intersect_plane_sphere(
889 sphere: &SphericalSurface,
890 normal: Vec3,
891 d: f64,
892) -> Result<Vec<IntersectionCurve>, MathError> {
893 let h = dot_np(normal, sphere.center()) - d;
894 let r = sphere.radius();
895
896 if h.abs() > r - 1e-10 {
898 return Ok(vec![]);
899 }
900
901 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
902 let circle_center = Point3::new(
903 h.mul_add(-normal.x(), sphere.center().x()),
904 h.mul_add(-normal.y(), sphere.center().y()),
905 h.mul_add(-normal.z(), sphere.center().z()),
906 );
907
908 let basis = Frame3::from_normal(circle_center, normal)?;
910 let u_dir = basis.x;
911 let v_dir = basis.y;
912
913 let n_samples = 64_usize;
914 let mut points_3d = Vec::new();
915 let mut ipoints = Vec::new();
916
917 for i in 0..=n_samples {
918 let theta = TAU * (i as f64) / (n_samples as f64);
919 let (sin_t, cos_t) = theta.sin_cos();
920 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
921 points_3d.push(pt);
922 ipoints.push(IntersectionPoint {
923 point: pt,
924 param1: (theta, 0.0),
925 param2: (0.0, 0.0),
926 });
927 }
928
929 build_curves_from_points(&points_3d, ipoints)
930}
931
932#[allow(clippy::cast_precision_loss)]
941pub fn intersect_plane_cone(
942 cone: &ConicalSurface,
943 normal: Vec3,
944 d: f64,
945) -> Result<Vec<IntersectionCurve>, MathError> {
946 let n_samples = 64_usize;
947 let mut points_3d = Vec::new();
948 let mut ipoints = Vec::new();
949
950 for i in 0..n_samples {
951 let u = TAU * (i as f64) / (n_samples as f64);
952 let apex = cone.apex();
955 let n_dot_apex = dot_np(normal, apex);
956 let p1 = cone.evaluate(u, 1.0);
958 let dir = p1 - apex;
959 let n_dot_dir = normal.dot(dir);
960
961 if n_dot_dir.abs() < 1e-12 {
962 continue;
963 }
964
965 let v = (d - n_dot_apex) / n_dot_dir;
966 if v.abs() > 1e-10 && v.abs() < 100.0 {
968 let pt = cone.evaluate(u, v);
969 points_3d.push(pt);
970 ipoints.push(IntersectionPoint {
971 point: pt,
972 param1: (u, v),
973 param2: (0.0, 0.0),
974 });
975 }
976 }
977
978 build_curves_from_points(&points_3d, ipoints)
979}
980
981#[allow(clippy::unnecessary_wraps)]
993pub fn intersect_plane_torus(
994 torus: &ToroidalSurface,
995 normal: Vec3,
996 d: f64,
997) -> Result<Vec<IntersectionCurve>, MathError> {
998 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
1002
1003 let mut curves = Vec::new();
1004 for ipts in chain_torus_crossings(&crossing_pts) {
1005 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1006 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1007 curves.push(IntersectionCurve {
1008 curve,
1009 points: ipts,
1010 });
1011 }
1012 }
1013
1014 Ok(curves)
1015}
1016
1017fn chain_torus_crossings(crossing_pts: &[(f64, f64, Point3)]) -> Vec<Vec<IntersectionPoint>> {
1029 let mut used = vec![false; crossing_pts.len()];
1030 let mut runs = Vec::new();
1031
1032 for start in 0..crossing_pts.len() {
1033 if used[start] {
1034 continue;
1035 }
1036 used[start] = true;
1037 let mut chain = vec![start];
1038
1039 loop {
1040 let last = chain[chain.len() - 1];
1041 let last_pt = crossing_pts[last].2;
1042 let mut best_idx = None;
1043 let mut best_dist = 1.0_f64;
1044
1045 for (j, &is_used) in used.iter().enumerate() {
1046 if is_used {
1047 continue;
1048 }
1049 let dist = (crossing_pts[j].2 - last_pt).length();
1050 if dist < best_dist {
1051 best_dist = dist;
1052 best_idx = Some(j);
1053 }
1054 }
1055
1056 if let Some(j) = best_idx {
1057 used[j] = true;
1058 chain.push(j);
1059 } else {
1060 break;
1061 }
1062 }
1063
1064 if chain.len() < 4 {
1065 continue;
1066 }
1067 let mut ipts: Vec<IntersectionPoint> = chain
1068 .iter()
1069 .map(|&i| IntersectionPoint {
1070 point: crossing_pts[i].2,
1071 param1: (crossing_pts[i].0, crossing_pts[i].1),
1072 param2: (0.0, 0.0),
1073 })
1074 .collect();
1075
1076 let closing_gap = (ipts[ipts.len() - 1].point - ipts[0].point).length();
1077 let median_spacing = {
1078 let mut spac: Vec<f64> = ipts
1079 .windows(2)
1080 .map(|w| (w[1].point - w[0].point).length())
1081 .collect();
1082 spac.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1083 spac.get(spac.len() / 2).copied().unwrap_or(0.0)
1084 };
1085 if closing_gap > 1e-9
1092 && median_spacing > 1e-12
1093 && closing_gap <= 2.0 * median_spacing
1094 && !chain_self_touches(&ipts, median_spacing)
1095 {
1096 ipts.push(ipts[0]);
1097 }
1098 runs.push(ipts);
1099 }
1100
1101 runs
1102}
1103
1104fn chain_self_touches(ipts: &[IntersectionPoint], median_spacing: f64) -> bool {
1114 let m = ipts.len();
1115 let k = (m / 4).clamp(1, 6);
1116 if m < 3 * k || median_spacing <= 0.0 {
1117 return false;
1118 }
1119 let thresh = median_spacing * 1.5;
1120 for i in k..(m - k) {
1121 for j in (i + k)..(m - k) {
1122 if (ipts[i].point - ipts[j].point).length() < thresh {
1123 return true;
1124 }
1125 }
1126 }
1127 false
1128}
1129
1130#[allow(clippy::cast_precision_loss)]
1147fn plane_torus_crossings(
1148 torus: &ToroidalSurface,
1149 normal: Vec3,
1150 d: f64,
1151 n_v: usize,
1152) -> Vec<(f64, f64, Point3)> {
1153 let big_r = torus.major_radius();
1154 let small_r = torus.minor_radius();
1155 let a = normal.dot(torus.x_axis());
1156 let b = normal.dot(torus.y_axis());
1157 let c = normal.dot(torus.z_axis());
1158 let s = a.hypot(b);
1159 let phi = b.atan2(a);
1160 let d_local = d - dot_np(normal, torus.center());
1161
1162 let mut pts: Vec<(f64, f64, Point3)> = Vec::new();
1163
1164 if s < 1e-12 {
1166 if c.abs() < 1e-12 {
1167 return pts;
1168 }
1169 let sin_v = d_local / (small_r * c);
1170 if sin_v.abs() > 1.0 + 1e-9 {
1171 return pts;
1172 }
1173 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1174 let v1 = std::f64::consts::PI - v0;
1175 let mut vs = vec![v0];
1176 if (v1 - v0).abs() > 1e-9 {
1178 vs.push(v1);
1179 }
1180 for v in vs {
1181 for i in 0..n_v {
1182 let u = TAU * (i as f64) / (n_v as f64);
1183 pts.push((u, v, torus.evaluate(u, v)));
1184 }
1185 }
1186 return pts;
1187 }
1188
1189 let v_off = TAU / (n_v as f64) * 0.5;
1195 for i in 0..n_v {
1196 let v = (i as f64).mul_add(TAU / (n_v as f64), v_off);
1197 let tube_r = small_r.mul_add(v.cos(), big_r); let rhs = (d_local - small_r * c * v.sin()) / (s * tube_r);
1199 if rhs.abs() > 1.0 {
1200 continue;
1201 }
1202 let delta = rhs.clamp(-1.0, 1.0).acos();
1203 for u in [phi + delta, phi - delta] {
1204 pts.push((u, v, torus.evaluate(u, v)));
1205 }
1206 }
1207 pts
1208}
1209
1210#[allow(clippy::cast_precision_loss)]
1219fn plane_torus_winding_loops(
1220 torus: &ToroidalSurface,
1221 normal: Vec3,
1222 d: f64,
1223 n_v: usize,
1224) -> Option<Vec<Vec<Point3>>> {
1225 let big_r = torus.major_radius();
1226 let small_r = torus.minor_radius();
1227 let a = normal.dot(torus.x_axis());
1228 let b = normal.dot(torus.y_axis());
1229 let c = normal.dot(torus.z_axis());
1230 let s = a.hypot(b);
1231 if s < 1e-12 * normal.length() || small_r >= big_r {
1232 return None;
1233 }
1234 let phi = b.atan2(a);
1235 let d_local = d - dot_np(normal, torus.center());
1236 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1237 let dense = 8 * n_v;
1238 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1239 return None;
1240 }
1241 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1242 for i in 0..n_v {
1243 let v = TAU * i as f64 / n_v as f64;
1244 let delta = rhs(v).acos();
1245 loops[0].push(torus.evaluate(phi + delta, v));
1246 loops[1].push(torus.evaluate(phi - delta, v));
1247 }
1248 Some(
1249 loops
1250 .into_iter()
1251 .map(|mut run| {
1252 run.push(run[0]);
1253 run
1254 })
1255 .collect(),
1256 )
1257}
1258
1259#[must_use]
1272pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1273 let c = torus.center();
1274 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1275 let big_r = torus.major_radius();
1276 let small_r = torus.minor_radius();
1277
1278 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1280 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1281 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1282 let (c0, c1) = (za.dot(o), za.dot(dir));
1283
1284 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1286 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1287 let g0 = a0.mul_add(
1288 a0,
1289 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1290 );
1291
1292 let four_rr = 4.0 * big_r * big_r;
1294 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1295 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1296 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1297
1298 let e4 = g2 * g2;
1300 let e3 = 2.0 * g2 * g1;
1301 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1302 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1303 let e0 = g0.mul_add(g0, -h0);
1304
1305 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1306 let impl_f = |t: f64| -> f64 {
1308 let p = origin + dir * t;
1309 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1310 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1311 (a.hypot(b) - big_r).hypot(cc) - small_r
1312 };
1313 for t in &mut roots {
1314 let eps = 1e-7;
1315 let f = impl_f(*t);
1316 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1317 if df.abs() > 1e-12 {
1318 *t -= f / df;
1319 }
1320 }
1321 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1322 roots
1323}
1324
1325fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1328 if c4.abs() < 1e-14 {
1330 return real_roots_cubic(c3, c2, c1, c0);
1331 }
1332 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1334 let eval = |z: Complex| -> Complex {
1335 let mut acc = Complex::new(1.0, 0.0);
1337 acc = acc * z + Complex::new(a, 0.0);
1338 acc = acc * z + Complex::new(b, 0.0);
1339 acc = acc * z + Complex::new(c, 0.0);
1340 acc * z + Complex::new(d, 0.0)
1341 };
1342 let seed = Complex::new(0.4, 0.9);
1344 let mut r = [
1345 Complex::new(1.0, 0.0),
1346 seed,
1347 seed * seed,
1348 seed * seed * seed,
1349 ];
1350 for _ in 0..100 {
1351 let mut max_step = 0.0_f64;
1352 for i in 0..4 {
1353 let mut denom = Complex::new(1.0, 0.0);
1354 for j in 0..4 {
1355 if i != j {
1356 denom = denom * (r[i] - r[j]);
1357 }
1358 }
1359 if denom.norm() < 1e-300 {
1360 continue;
1361 }
1362 let step = eval(r[i]) / denom;
1363 r[i] = r[i] - step;
1364 max_step = max_step.max(step.norm());
1365 }
1366 if max_step < 1e-14 {
1367 break;
1368 }
1369 }
1370 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1377 let mut out: Vec<f64> = Vec::new();
1378 for z in r {
1379 if z.im.abs() >= 1e-7 {
1380 continue;
1381 }
1382 let x = z.re;
1383 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1386 if p_real(x).abs() > 1e-6 * scale {
1387 continue;
1388 }
1389 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1390 continue;
1391 }
1392 out.push(x);
1393 }
1394 out
1395}
1396
1397fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1399 if a.abs() < 1e-14 {
1400 return real_roots_quadratic(b, c, d);
1401 }
1402 let (b, c, d) = (b / a, c / a, d / a);
1404 let p = c - b * b / 3.0;
1405 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1406 let shift = -b / 3.0;
1407 let disc = q * q / 4.0 + p * p * p / 27.0;
1408 if disc > 1e-14 {
1409 let sq = disc.sqrt();
1410 let u = (-q / 2.0 + sq).cbrt();
1411 let v = (-q / 2.0 - sq).cbrt();
1412 vec![u + v + shift]
1413 } else if disc < -1e-14 {
1414 let m = 2.0 * (-p / 3.0).sqrt();
1416 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1417 (0..3)
1418 .map(|k| {
1419 m.mul_add(
1420 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1421 shift,
1422 )
1423 })
1424 .collect()
1425 } else {
1426 let u = (-q / 2.0).cbrt();
1428 vec![2.0 * u + shift, -u + shift]
1429 }
1430}
1431
1432fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1434 if a.abs() < 1e-14 {
1435 if b.abs() < 1e-14 {
1436 return Vec::new();
1437 }
1438 return vec![-c / b];
1439 }
1440 let disc = b * b - 4.0 * a * c;
1441 if disc < 0.0 {
1442 Vec::new()
1443 } else {
1444 let sq = disc.sqrt();
1445 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1446 }
1447}
1448
1449#[derive(Clone, Copy)]
1451struct Complex {
1452 re: f64,
1453 im: f64,
1454}
1455
1456impl Complex {
1457 const fn new(re: f64, im: f64) -> Self {
1458 Self { re, im }
1459 }
1460 fn norm(self) -> f64 {
1461 self.re.hypot(self.im)
1462 }
1463}
1464
1465impl std::ops::Add for Complex {
1466 type Output = Self;
1467 fn add(self, o: Self) -> Self {
1468 Self::new(self.re + o.re, self.im + o.im)
1469 }
1470}
1471
1472impl std::ops::Sub for Complex {
1473 type Output = Self;
1474 fn sub(self, o: Self) -> Self {
1475 Self::new(self.re - o.re, self.im - o.im)
1476 }
1477}
1478
1479impl std::ops::Mul for Complex {
1480 type Output = Self;
1481 fn mul(self, o: Self) -> Self {
1482 Self::new(
1483 self.re.mul_add(o.re, -(self.im * o.im)),
1484 self.re.mul_add(o.im, self.im * o.re),
1485 )
1486 }
1487}
1488
1489impl std::ops::Div for Complex {
1490 type Output = Self;
1491 fn div(self, o: Self) -> Self {
1492 let den = o.re.mul_add(o.re, o.im * o.im);
1493 Self::new(
1494 self.re.mul_add(o.re, self.im * o.im) / den,
1495 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1496 )
1497 }
1498}
1499
1500fn build_curves_from_points(
1504 points_3d: &[Point3],
1505 ipoints: Vec<IntersectionPoint>,
1506) -> Result<Vec<IntersectionCurve>, MathError> {
1507 if points_3d.len() < 2 {
1508 return Ok(vec![]);
1509 }
1510
1511 let degree = 3.min(points_3d.len() - 1);
1512 let curve = interpolate(points_3d, degree)?;
1513 Ok(vec![IntersectionCurve {
1514 curve,
1515 points: ipoints,
1516 }])
1517}
1518
1519#[allow(
1531 clippy::cast_precision_loss,
1532 clippy::too_many_lines,
1533 clippy::similar_names,
1534 clippy::unnecessary_wraps,
1535 clippy::type_complexity
1536)]
1537pub fn intersect_analytic_analytic(
1538 a: AnalyticSurface<'_>,
1539 b: AnalyticSurface<'_>,
1540 grid_res: usize,
1541) -> Result<Vec<IntersectionCurve>, MathError> {
1542 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1543}
1544
1545pub fn intersect_analytic_analytic_bounded(
1556 a: AnalyticSurface<'_>,
1557 b: AnalyticSurface<'_>,
1558 grid_res: usize,
1559 v_range_hint_a: Option<(f64, f64)>,
1560 v_range_hint_b: Option<(f64, f64)>,
1561) -> Result<Vec<IntersectionCurve>, MathError> {
1562 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1565 return Ok(result);
1566 }
1567
1568 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1569 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1570 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1571 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1572
1573 let diag_a = {
1575 let p00 = surf_a(u_range_a.0, v_range_a.0);
1576 let p11 = surf_a(u_range_a.1, v_range_a.1);
1577 (p00 - p11).length()
1578 };
1579 let diag_b = {
1580 let p00 = surf_b(u_range_b.0, v_range_b.0);
1581 let p11 = surf_b(u_range_b.1, v_range_b.1);
1582 (p00 - p11).length()
1583 };
1584 let char_size = diag_a.min(diag_b).max(0.1);
1585
1586 #[allow(clippy::type_complexity)]
1590 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1591 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1595 let mut min_dist = f64::INFINITY;
1596
1597 #[allow(clippy::cast_precision_loss)]
1598 for ia in 0..grid_res {
1599 for ja in 0..grid_res {
1600 let ua =
1601 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1602 let va =
1603 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1604
1605 let pa = surf_a(ua, va);
1606
1607 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1609 let pb = surf_b(ub, vb);
1610 let dist = (pa - pb).length();
1611 min_dist = min_dist.min(dist);
1612
1613 if dist < seed_threshold {
1614 let mid = Point3::new(
1619 (pa.x() + pb.x()) * 0.5,
1620 (pa.y() + pb.y()) * 0.5,
1621 (pa.z() + pb.z()) * 0.5,
1622 );
1623 seeds.push((mid, (ua, va), (ub, vb)));
1624 }
1625 }
1626 }
1627
1628 let reject_dist = (char_size / grid_res as f64) * 3.0;
1637 if min_dist > reject_dist {
1638 return Ok(vec![]);
1639 }
1640
1641 if seeds.is_empty() {
1642 return Ok(vec![]);
1643 }
1644
1645 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1649 let dedup_radius = march_step * 10.0;
1650 let mut unique_seeds = Vec::new();
1651 for seed in &seeds {
1652 let dominated = unique_seeds
1653 .iter()
1654 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1655 if !dominated {
1656 unique_seeds.push(*seed);
1657 }
1658 }
1659
1660 let mut curves = Vec::new();
1662 let mut used_seeds = vec![false; unique_seeds.len()];
1663
1664 for si in 0..unique_seeds.len() {
1665 if used_seeds[si] {
1666 continue;
1667 }
1668 used_seeds[si] = true;
1669
1670 let march_result = march_analytic_intersection(
1671 &a,
1672 &b,
1673 surf_a.as_ref(),
1674 norm_a.as_ref(),
1675 surf_b.as_ref(),
1676 norm_b.as_ref(),
1677 unique_seeds[si].0,
1678 u_range_a,
1679 v_range_a,
1680 u_range_b,
1681 v_range_b,
1682 march_step,
1683 is_u_periodic(&a),
1684 is_u_periodic(&b),
1685 );
1686
1687 if march_result.len() >= 2 {
1688 for (sj, other) in unique_seeds.iter().enumerate() {
1689 if !used_seeds[sj]
1690 && march_result
1691 .iter()
1692 .any(|p| (*p - other.0).length() < dedup_radius)
1693 {
1694 used_seeds[sj] = true;
1695 }
1696 }
1697
1698 let ipts: Vec<IntersectionPoint> = march_result
1699 .iter()
1700 .map(|&pt| IntersectionPoint {
1701 point: pt,
1702 param1: (0.0, 0.0),
1703 param2: (0.0, 0.0),
1704 })
1705 .collect();
1706
1707 let degree = 3.min(march_result.len() - 1);
1708 if let Ok(curve) = interpolate(&march_result, degree) {
1709 curves.push(IntersectionCurve {
1710 curve,
1711 points: ipts,
1712 });
1713 }
1714 }
1715 }
1716
1717 Ok(curves)
1718}
1719
1720#[allow(clippy::too_many_lines)]
1734fn try_algebraic_intersection(
1735 a: &AnalyticSurface<'_>,
1736 b: &AnalyticSurface<'_>,
1737 v_range_a: Option<(f64, f64)>,
1738 v_range_b: Option<(f64, f64)>,
1739) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1740 match (a, b) {
1741 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1742 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1743 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1744 ),
1745 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1746 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1747 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1748 ),
1749 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1750 algebraic_sphere_sphere(s1, s2).map(Some)
1751 }
1752 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1753 let axis_dot = c1.axis().dot(c2.axis()).abs();
1754 if axis_dot > 1.0 - 1e-10 {
1755 let delta = c2.origin() - c1.origin();
1757 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1758 let along = delta_vec.dot(c1.axis());
1759 let perp = (delta_vec - c1.axis() * along).length();
1760 if perp < 1e-8 {
1761 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1764 return Ok(None); }
1766 return Ok(Some(vec![])); }
1768 }
1769 algebraic_cylinder_cylinder(c1, c2)
1771 }
1772 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1774 algebraic_sphere_cylinder(s, c, true)
1775 }
1776 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1777 algebraic_sphere_cylinder(s, c, false)
1778 }
1779 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1780 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1781 Ok(ruling_cone_sphere(cone, sphere, true))
1782 }
1783 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1784 Ok(ruling_cone_sphere(cone, sphere, false))
1785 }
1786 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1787 Ok(parallel_axis_torus_cylinder(t, c, true)
1788 .or_else(|| ruling_torus_cylinder(t, c, true)))
1789 }
1790 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1791 Ok(parallel_axis_torus_cylinder(t, c, false)
1792 .or_else(|| ruling_torus_cylinder(t, c, false)))
1793 }
1794 _ => Ok(None),
1795 }
1796}
1797
1798fn parallel_axis_torus_cylinder(
1805 torus: &ToroidalSurface,
1806 cyl: &CylindricalSurface,
1807 torus_first: bool,
1808) -> Option<Vec<IntersectionCurve>> {
1809 let axis = torus.z_axis();
1810 let along = cyl.axis().dot(axis);
1811 if along.abs() < 1.0 - 1e-10 {
1812 return None;
1813 }
1814 let offset = cyl.origin() - torus.center();
1815 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1816 return None;
1817 }
1818 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1819 let roots = |u: f64| {
1820 let q = cyl.evaluate(u, 0.0) - torus.center();
1821 let height = q.dot(axis);
1822 let rho = (q - axis * height).length();
1823 let reach = minor * minor - (rho - major) * (rho - major);
1824 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1825 };
1826 let samples = ruling_samples(cyl, &roots);
1827 let loops = if samples.iter().all(Option::is_some) {
1828 closed_ruling_loops(&samples)
1829 } else {
1830 partial_ruling_loops(cyl, &roots, &samples)
1831 };
1832 if loops.is_empty() {
1833 return None;
1834 }
1835 Some(fit_ruling_loops(&loops, |p| {
1836 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1837 }))
1838}
1839
1840fn meridian_crossings(
1846 first: (f64, f64, f64),
1847 second: (f64, f64, f64),
1848 scale: f64,
1849) -> Option<Vec<(f64, f64)>> {
1850 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1851 let (dx, dz) = (x2 - x1, z2 - z1);
1852 let dist = dx.hypot(dz);
1853 let slack = 1e-9 * scale;
1854 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1855 return None;
1856 }
1857 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1858 return Some(Vec::new());
1859 }
1860 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1861 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1862 let (ux, uz) = (dx / dist, dz / dist);
1863 let mut crossings = Vec::with_capacity(2);
1864 for side in [1.0, -1.0] {
1865 let rho = x1 + along * ux - side * across * uz;
1866 if rho <= slack {
1867 return None;
1868 }
1869 crossings.push((rho, z1 + along * uz + side * across * ux));
1870 }
1871 Some(crossings)
1872}
1873
1874fn circles_about_axis(
1876 base: Point3,
1877 axis: Vec3,
1878 crossings: &[(f64, f64)],
1879) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1880 crossings
1881 .iter()
1882 .map(|&(rho, z)| {
1883 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1884 })
1885 .collect()
1886}
1887
1888pub fn exact_torus_torus(
1899 first: &ToroidalSurface,
1900 second: &ToroidalSurface,
1901) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1902 let axis = first.z_axis();
1903 let scale = first.major_radius() + second.major_radius();
1904 let offset = second.center() - first.center();
1905 if first.minor_radius() >= first.major_radius()
1907 || second.minor_radius() >= second.major_radius()
1908 || axis.cross(second.z_axis()).length() > 1e-9
1909 || offset.cross(axis).length() > 1e-9 * scale
1910 {
1911 return Ok(None);
1912 }
1913 let Some(crossings) = meridian_crossings(
1914 (first.major_radius(), 0.0, first.minor_radius()),
1915 (
1916 second.major_radius(),
1917 offset.dot(axis),
1918 second.minor_radius(),
1919 ),
1920 scale,
1921 ) else {
1922 return Ok(None);
1923 };
1924 circles_about_axis(first.center(), axis, &crossings).map(Some)
1925}
1926
1927pub fn exact_cylinder_torus(
1939 cylinder: &CylindricalSurface,
1940 torus: &ToroidalSurface,
1941) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1942 let axis = torus.z_axis();
1943 let scale = torus.major_radius() + cylinder.radius();
1944 let offset = cylinder.origin() - torus.center();
1945 if torus.minor_radius() >= torus.major_radius()
1947 || axis.cross(cylinder.axis()).length() > 1e-9
1948 || offset.cross(axis).length() > 1e-9 * scale
1949 {
1950 return Ok(None);
1951 }
1952 let gap = cylinder.radius() - torus.major_radius();
1953 let small = torus.minor_radius();
1954 if (gap.abs() - small).abs() < 1e-9 * scale {
1955 return Ok(None);
1956 }
1957 if gap.abs() > small {
1958 return Ok(Some(Vec::new()));
1959 }
1960 let height = small.mul_add(small, -(gap * gap)).sqrt();
1961 circles_about_axis(
1962 torus.center(),
1963 axis,
1964 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
1965 )
1966 .map(Some)
1967}
1968
1969pub fn exact_sphere_torus(
1982 sphere: &SphericalSurface,
1983 torus: &ToroidalSurface,
1984) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1985 let axis = torus.z_axis();
1986 let scale = torus.major_radius() + sphere.radius();
1987 let offset = sphere.center() - torus.center();
1988 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
1990 return Ok(None);
1991 }
1992 let Some(crossings) = meridian_crossings(
1993 (0.0, offset.dot(axis), sphere.radius()),
1994 (torus.major_radius(), 0.0, torus.minor_radius()),
1995 scale,
1996 ) else {
1997 return Ok(None);
1998 };
1999 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2000}
2001
2002pub fn exact_cone_cone(
2027 c1: &ConicalSurface,
2028 c2: &ConicalSurface,
2029) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2030 let axis = c1.axis();
2031 let axis2 = c2.axis();
2032
2033 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2035 return Ok(None); }
2037 let apex1 = c1.apex();
2038 let apex2 = c2.apex();
2039 let delta = apex2 - apex1;
2040 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2041 let along = delta_v.dot(axis);
2042 if (delta_v - axis * along).length() > 1e-8 {
2043 return offset_parallel_cone_cone(c1, c2);
2044 }
2045
2046 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2047 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2048 return Ok(None); }
2050 let m1 = c1.half_angle().cos() / s1;
2051 let m2 = c2.half_angle().cos() / s2;
2052 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2053 let d2 = along; let denom = m1 - m2 * sigma;
2056 if denom.abs() < 1e-12 {
2057 if sigma > 0.0 && d2.abs() < 1e-9 {
2060 return Ok(None);
2061 }
2062 return Ok(Some(vec![]));
2063 }
2064
2065 let t_star = (-m2 * sigma * d2) / denom;
2066 let radius = m1 * t_star;
2067 if radius < 1e-12 {
2068 return Ok(Some(vec![])); }
2070
2071 let center = Point3::new(
2072 apex1.x() + axis.x() * t_star,
2073 apex1.y() + axis.y() * t_star,
2074 apex1.z() + axis.z() * t_star,
2075 );
2076 let circle = Circle3D::new(center, axis, radius)?;
2077 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2078}
2079
2080fn offset_parallel_cone_cone(
2091 c1: &ConicalSurface,
2092 c2: &ConicalSurface,
2093) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2094 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2095 return Ok(None); }
2097 let t1 = c1.half_angle().tan();
2098 let t2 = c2.half_angle().tan();
2099 if !t1.is_finite() || !t2.is_finite() {
2100 return Ok(None);
2101 }
2102 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2103 return Ok(None);
2104 }
2105
2106 let w = c1.axis();
2107 let apex1 = c1.apex();
2108 let apex2 = c2.apex();
2109 let delta = apex2 - apex1;
2110 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2111 let s = delta_v.dot(w);
2112 let tm = 0.5 * (t1 + t2);
2113 let k = 1.0 + tm * tm;
2114
2115 let n = (delta_v - w * (k * s)) * 2.0;
2119 let n_len = n.length();
2120 if n_len < 1e-12 {
2121 return Ok(None);
2122 }
2123 let n_hat = n * (1.0 / n_len);
2124 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2125
2126 let axis2 = c2.axis();
2132 let scale = 1.0 + delta_v.length();
2133 let mut out = Vec::new();
2134 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2135 let samples: Vec<Point3> = match &curve {
2136 ExactIntersectionCurve::Circle(c) => (0..4)
2137 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2138 .collect(),
2139 ExactIntersectionCurve::Ellipse(e) => (0..4)
2140 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2141 .collect(),
2142 ExactIntersectionCurve::Points(_) => return Ok(None),
2143 };
2144 let on_real_nappe = |p: &Point3| {
2145 let rel = *p - apex2;
2146 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2147 };
2148 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2149 match hits {
2150 0 => {}
2151 4 => out.push(curve),
2152 _ => return Ok(None),
2153 }
2154 }
2155 Ok(Some(out))
2156}
2157
2158pub fn exact_cone_cylinder(
2178 cone: &ConicalSurface,
2179 cyl: &CylindricalSurface,
2180) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2181 let axis = cone.axis();
2182 let cyl_axis = cyl.axis();
2183
2184 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2186 return Ok(None);
2187 }
2188 let apex = cone.apex();
2189 let delta = apex - cyl.origin();
2190 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2191 let along = delta_v.dot(cyl_axis);
2192 if (delta_v - cyl_axis * along).length() > 1e-8 {
2193 return Ok(None);
2194 }
2195
2196 let s = cone.half_angle().sin();
2197 if s.abs() < 1e-12 {
2198 return Ok(None); }
2200 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2202 return Ok(None); }
2204
2205 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2207 return Ok(Some(vec![])); }
2209 let center = Point3::new(
2210 apex.x() + axis.x() * t_star,
2211 apex.y() + axis.y() * t_star,
2212 apex.z() + axis.z() * t_star,
2213 );
2214 let circle = Circle3D::new(center, axis, cyl.radius())?;
2215 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2216}
2217
2218fn algebraic_cone_cone(
2227 c1: &ConicalSurface,
2228 c2: &ConicalSurface,
2229) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2230 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2231 return Ok(None);
2232 };
2233 let mut curves = Vec::new();
2234 for exact in exacts {
2235 let n_samples = 33;
2236 let mut positions = Vec::with_capacity(n_samples);
2237 let mut points = Vec::with_capacity(n_samples);
2238 #[allow(clippy::cast_precision_loss)]
2239 for i in 0..n_samples {
2240 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2241 let pt = match &exact {
2242 ExactIntersectionCurve::Circle(circle) => {
2243 crate::traits::ParametricCurve::evaluate(circle, theta)
2244 }
2245 ExactIntersectionCurve::Ellipse(ellipse) => {
2246 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2247 }
2248 ExactIntersectionCurve::Points(_) => break,
2249 };
2250 positions.push(pt);
2251 points.push(IntersectionPoint {
2252 point: pt,
2253 param1: (0.0, 0.0),
2254 param2: (0.0, 0.0),
2255 });
2256 }
2257 if positions.is_empty() {
2258 continue;
2259 }
2260 let degree = 3.min(positions.len() - 1);
2261 let curve = interpolate(&positions, degree)?;
2262 curves.push(IntersectionCurve { curve, points });
2263 }
2264 Ok(Some(curves))
2265}
2266
2267pub fn exact_sphere_cylinder(
2287 sphere: &SphericalSurface,
2288 cyl: &CylindricalSurface,
2289) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2290 let sc = sphere.center();
2291 let r_sphere = sphere.radius();
2292 let co = cyl.origin();
2293 let axis = cyl.axis();
2294 let r_cyl = cyl.radius();
2295
2296 let delta = sc - co;
2298 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2299 let along = delta_vec.dot(axis);
2300 let perp_vec = delta_vec - axis * along;
2301 let d_perp = perp_vec.length();
2302
2303 if d_perp > 1e-7 {
2306 return Ok(None);
2307 }
2308
2309 if r_cyl > r_sphere + 1e-10 {
2312 return Ok(Some(vec![]));
2313 }
2314 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2315 if z_sq < 0.0 {
2316 return Ok(Some(vec![]));
2317 }
2318 let z = z_sq.sqrt();
2319
2320 let center_axis_pt = Point3::new(
2323 co.x() + axis.x() * along,
2324 co.y() + axis.y() * along,
2325 co.z() + axis.z() * along,
2326 );
2327
2328 let mut circles = Vec::new();
2329 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2330 for &z_offset in offsets {
2331 let center = Point3::new(
2332 center_axis_pt.x() + axis.x() * z_offset,
2333 center_axis_pt.y() + axis.y() * z_offset,
2334 center_axis_pt.z() + axis.z() * z_offset,
2335 );
2336 let circle = Circle3D::new(center, axis, r_cyl)?;
2337 circles.push(ExactIntersectionCurve::Circle(circle));
2338 }
2339 Ok(Some(circles))
2340}
2341
2342pub fn exact_cone_sphere(
2360 cone: &ConicalSurface,
2361 sphere: &SphericalSurface,
2362) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2363 let offset = cone.apex() - sphere.center();
2364 let along = offset.dot(cone.axis());
2365 if (offset - cone.axis() * along).length() > 1e-7 {
2366 return Ok(None);
2367 }
2368 let lin_tol = Tolerance::new().linear;
2369 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2370 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2371 let b = 2.0 * sin_a * along;
2372 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2373 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2376 if disc < -noise {
2377 return Ok(Some(vec![]));
2378 }
2379 let roots: &[f64] = if far - near < lin_tol {
2380 &[far]
2381 } else {
2382 &[near, far]
2383 };
2384 let mut circles = Vec::new();
2385 for &v in roots {
2386 if v * cos_a > lin_tol {
2387 let centre = cone.apex() + cone.axis() * (v * sin_a);
2388 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2389 circles.push(ExactIntersectionCurve::Circle(circle));
2390 }
2391 }
2392 Ok(Some(circles))
2393}
2394
2395fn algebraic_sphere_cylinder(
2404 sphere: &SphericalSurface,
2405 cyl: &CylindricalSurface,
2406 sphere_first: bool,
2407) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2408 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2409 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2410 };
2411
2412 let mut curves = Vec::new();
2413 for exact in exacts {
2414 let ExactIntersectionCurve::Circle(circle) = exact else {
2415 continue;
2416 };
2417 let n_samples = 33;
2418 let mut points = Vec::with_capacity(n_samples);
2419 let mut positions = Vec::with_capacity(n_samples);
2420 #[allow(clippy::cast_precision_loss)]
2421 for i in 0..n_samples {
2422 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2423 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2424 positions.push(pt);
2425 let (param1, param2) = in_order(
2426 sphere.project_point(pt),
2427 cyl.project_point(pt),
2428 sphere_first,
2429 );
2430 points.push(IntersectionPoint {
2431 point: pt,
2432 param1,
2433 param2,
2434 });
2435 }
2436 let degree = 3.min(positions.len() - 1);
2437 let curve = interpolate(&positions, degree)?;
2438 curves.push(IntersectionCurve { curve, points });
2439 }
2440
2441 Ok(Some(curves))
2442}
2443
2444fn off_axis_sphere_cylinder(
2453 sphere: &SphericalSurface,
2454 cyl: &CylindricalSurface,
2455 sphere_first: bool,
2456) -> Option<Vec<IntersectionCurve>> {
2457 let (centre, radius) = (sphere.center(), sphere.radius());
2458 let axis = cyl.axis();
2459 let offset = centre - cyl.origin();
2460 let axis_distance = (offset - axis * offset.dot(axis)).length();
2461 let lin_tol = Tolerance::new().linear;
2462 if axis_distance > radius + cyl.radius() + lin_tol
2463 || axis_distance + radius < cyl.radius() - lin_tol
2464 {
2465 return Some(Vec::new());
2466 }
2467 let roots = |u: f64| {
2468 let q = cyl.evaluate(u, 0.0) - centre;
2469 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2470 };
2471 let samples = ruling_samples(cyl, &roots);
2472 let loops = if samples.iter().all(Option::is_some) {
2473 closed_ruling_loops(&samples)
2474 } else {
2475 partial_ruling_loops(cyl, &roots, &samples)
2476 };
2477 if loops.is_empty() {
2478 return None;
2479 }
2480 Some(fit_ruling_loops(&loops, |p| {
2481 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2482 }))
2483}
2484
2485const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2488 if a_first { (a, b) } else { (b, a) }
2489}
2490
2491#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2505fn algebraic_cylinder_cylinder(
2506 c1: &CylindricalSurface,
2507 c2: &CylindricalSurface,
2508) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2509 let alpha = c1.axis().dot(c2.axis());
2510 let a_coeff = 1.0 - alpha * alpha;
2511
2512 if a_coeff.abs() < 1e-12 {
2514 return Ok(None);
2515 }
2516
2517 let r1 = c1.radius();
2518 let r2 = c2.radius();
2519 let o1 = c1.origin();
2520 let o2 = c2.origin();
2521 let a1 = c1.axis();
2522 let a2 = c2.axis();
2523
2524 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2527 let cross = a1.cross(a2);
2528 let cross_len = cross.length();
2529 if cross_len > 1e-12 {
2530 let axis_dist = delta.dot(cross).abs() / cross_len;
2531 if axis_dist > r1 + r2 + Tolerance::new().linear {
2532 return Ok(Some(vec![])); }
2534 }
2535
2536 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2542 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2543 let alpha = sweep.axis().dot(a);
2544 let quad = 1.0 - alpha * alpha;
2545 let (axis, sweep) = (sweep.axis(), sweep.clone());
2546 move |u: f64| {
2547 let q = sweep.evaluate(u, 0.0) - o;
2548 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2549 let b = 2.0 * (q_a1 - alpha * q_a2);
2550 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2551 ruling_quadratic(quad, b, c)
2552 }
2553 };
2554 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2555 let samples1 = ruling_samples(c1, &roots1);
2556 let loops = if samples1.iter().all(Option::is_some) {
2557 closed_ruling_loops(&samples1)
2558 } else {
2559 let samples2 = ruling_samples(c2, &roots2);
2560 if samples2.iter().all(Option::is_some) {
2561 closed_ruling_loops(&samples2)
2562 } else if samples1.iter().any(Option::is_some) {
2563 partial_ruling_loops(c1, &roots1, &samples1)
2564 } else {
2565 partial_ruling_loops(c2, &roots2, &samples2)
2566 }
2567 };
2568 if loops.is_empty() {
2569 return Ok(None);
2570 }
2571 Ok(Some(fit_ruling_loops(&loops, |p| {
2572 (c1.project_point(p), c2.project_point(p))
2573 })))
2574}
2575
2576fn ruling_cone_cylinder(
2583 cone: &ConicalSurface,
2584 cyl: &CylindricalSurface,
2585 cone_first: bool,
2586) -> Option<Vec<IntersectionCurve>> {
2587 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2588 if sin_t < 1e-12 || cos_t < 1e-12 {
2589 return None;
2590 }
2591 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2592 let s = 1.0 / (sin_t * sin_t);
2593 let alpha = w.dot(d);
2594 let quad = 1.0 - s * alpha * alpha;
2595 if quad.abs() < 1e-9 {
2596 return None;
2597 }
2598 let roots = |u: f64| {
2599 let delta = cyl.evaluate(u, 0.0) - apex;
2600 let (dd, dw) = (delta.dot(d), delta.dot(w));
2601 let b = 2.0 * (dw - s * dd * alpha);
2602 let c = delta.dot(delta) - s * dd * dd;
2603 ruling_quadratic(quad, b, c)
2604 };
2605 let lin_tol = Tolerance::new().linear;
2606 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2607 #[allow(clippy::cast_precision_loss)]
2608 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2609 let (disc, vp, vm) = roots(u);
2610 disc >= -lin_tol
2611 && [vp, vm]
2612 .iter()
2613 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2614 });
2615 if far_nappe {
2616 return None;
2617 }
2618 let samples = ruling_samples(cyl, &roots);
2619 let scan = WINDOW_SCAN * RULING_SAMPLES;
2624 #[allow(clippy::cast_precision_loss)]
2627 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2628 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2629 let mut k = start;
2630 while k < start + scan {
2631 if !meets(k) {
2632 k += 1;
2633 continue;
2634 }
2635 let first = k;
2636 while k < start + scan && meets(k) {
2637 k += 1;
2638 }
2639 let covered = (first..k)
2640 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2641 .count();
2642 if covered < WINDOW_MIN_SAMPLES {
2643 return None;
2644 }
2645 }
2646 }
2647 let loops = if samples.iter().all(Option::is_some) {
2648 closed_ruling_loops(&samples)
2649 } else {
2650 partial_ruling_loops(cyl, &roots, &samples)
2651 };
2652 if loops.is_empty() {
2653 return None;
2654 }
2655 Some(fit_ruling_loops(&loops, |p| {
2656 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2657 }))
2658}
2659
2660fn ruling_torus_cylinder(
2670 torus: &ToroidalSurface,
2671 cyl: &CylindricalSurface,
2672 torus_first: bool,
2673) -> Option<Vec<IntersectionCurve>> {
2674 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2675 || torus.minor_radius() >= torus.major_radius()
2676 {
2677 return None;
2678 }
2679 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2680 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2681 let count = rows[0].len();
2682 let scan = WINDOW_SCAN * RULING_SAMPLES;
2683 #[allow(clippy::cast_precision_loss)]
2684 if count == 0
2685 || count % 2 == 1
2686 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2687 {
2688 return None;
2689 }
2690 let loops: Vec<Vec<Point3>> = (0..count)
2691 .map(|j| {
2692 let mut pts: Vec<Point3> = rows
2693 .iter()
2694 .enumerate()
2695 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2696 .collect();
2697 pts.push(pts[0]);
2698 pts
2699 })
2700 .collect();
2701 Some(fit_ruling_loops(&loops, |p| {
2702 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2703 }))
2704}
2705
2706fn ruling_cone_sphere(
2715 cone: &ConicalSurface,
2716 sphere: &SphericalSurface,
2717 cone_first: bool,
2718) -> Option<Vec<IntersectionCurve>> {
2719 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2720 let offset = apex - centre;
2721 let lin_tol = Tolerance::new().linear;
2722 let along = offset.dot(cone.axis());
2723 let across = (offset - cone.axis() * along).length();
2724 if across < lin_tol {
2725 return None;
2726 }
2727 let k = offset.dot(offset) - radius * radius;
2733 if radius - offset.length() > lin_tol {
2734 let exit = |u: f64| {
2735 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2736 let root = h.mul_add(h, -k).sqrt();
2737 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2738 };
2739 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2744 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2745 .collect();
2746 for _ in 0..10 {
2747 let mut refined = Vec::with_capacity(2 * samples.len());
2748 for pair in samples.windows(2) {
2749 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2750 refined.push(pair[0]);
2751 let um = 0.5 * (u0 + u1);
2752 let pm = exit(um);
2753 let chord = (p1 - p0).length();
2754 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2755 refined.push((um, pm));
2756 }
2757 }
2758 refined.extend(samples.last().copied());
2759 if refined.len() == samples.len() {
2760 break;
2761 }
2762 samples = refined;
2763 }
2764 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2765 if let Some(last) = pts.last_mut() {
2766 *last = samples[0].1;
2767 }
2768 return Some(fit_ruling_loops(&[pts], |p| {
2769 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2770 }));
2771 }
2772 let crossing = |h: f64| {
2776 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2777 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2778 };
2779 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2784 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2785 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2786 {
2787 return window_cone_sphere(cone, sphere, cone_first);
2788 }
2789 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2790 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2791 .collect::<Option<_>>()?;
2792 let loops: Vec<Vec<Point3>> = [0, 1]
2793 .iter()
2794 .map(|&j| {
2795 let mut pts: Vec<Point3> = rows
2796 .iter()
2797 .enumerate()
2798 .map(|(i, &(near, far))| {
2799 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2800 })
2801 .collect();
2802 pts.push(pts[0]);
2803 pts
2804 })
2805 .collect();
2806 Some(fit_ruling_loops(&loops, |p| {
2807 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2808 }))
2809}
2810
2811fn window_cone_sphere(
2826 cone: &ConicalSurface,
2827 sphere: &SphericalSurface,
2828 cone_first: bool,
2829) -> Option<Vec<IntersectionCurve>> {
2830 let offset = cone.apex() - sphere.center();
2831 let lin_tol = Tolerance::new().linear;
2832 if offset.length() - sphere.radius() <= lin_tol {
2833 return None;
2834 }
2835 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2836 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2837 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2838 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2839 if a < lin_tol {
2840 return None;
2841 }
2842 let reach = (-k.sqrt() - c) / a;
2843 if reach <= -1.0 {
2844 return Some(Vec::new());
2845 }
2846 if reach >= 1.0 {
2847 return None;
2848 }
2849 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2850 let half = std::f64::consts::PI - half;
2851 let n = RULING_SAMPLES;
2852 let mut pts: Vec<Point3> = (0..n)
2853 .map(|i| {
2854 #[allow(clippy::cast_precision_loss)]
2855 let theta = TAU * i as f64 / n as f64;
2856 let u = half.mul_add(-theta.cos(), mid);
2857 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2858 let split = h.mul_add(h, -k).max(0.0).sqrt();
2859 cone.evaluate(u, -h - split.copysign(theta.sin()))
2860 })
2861 .collect();
2862 pts.push(pts[0]);
2863 Some(fit_ruling_loops(&[pts], |p| {
2864 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2865 }))
2866}
2867
2868const WINDOW_SCAN: usize = 16;
2871const WINDOW_MIN_SAMPLES: usize = 8;
2872
2873const RULING_SAMPLES: usize = 128;
2877
2878#[allow(clippy::cast_precision_loss)]
2879fn ruling_u(i: usize) -> f64 {
2880 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2881}
2882
2883fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2885 let disc = b * b - 4.0 * quad * c;
2886 let root = disc.max(0.0).sqrt();
2887 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2888}
2889
2890fn ruling_samples(
2894 sweep: &CylindricalSurface,
2895 roots: &impl Fn(f64) -> (f64, f64, f64),
2896) -> Vec<Option<(Point3, Point3)>> {
2897 let lin_tol = Tolerance::new().linear;
2898 (0..RULING_SAMPLES)
2899 .map(|i| {
2900 let u = ruling_u(i);
2901 let (disc, vp, vm) = roots(u);
2902 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2903 })
2904 .collect()
2905}
2906
2907fn closed_ruling_loops(samples: &[Option<(Point3, Point3)>]) -> Vec<Vec<Point3>> {
2909 let mut plus: Vec<Point3> = samples.iter().flatten().map(|s| s.0).collect();
2910 let mut minus: Vec<Point3> = samples.iter().flatten().map(|s| s.1).collect();
2911 plus.push(plus[0]);
2912 minus.push(minus[0]);
2913 vec![plus, minus]
2914}
2915
2916fn partial_ruling_loops(
2921 sweep: &CylindricalSurface,
2922 roots: &impl Fn(f64) -> (f64, f64, f64),
2923 samples: &[Option<(Point3, Point3)>],
2924) -> Vec<Vec<Point3>> {
2925 let branch_point = |inside: usize, outside: usize| -> Point3 {
2926 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
2927 if (hi - lo).abs() > std::f64::consts::PI {
2928 hi += if hi < lo { TAU } else { -TAU };
2929 }
2930 for _ in 0..60 {
2931 let mid = 0.5 * (lo + hi);
2932 if roots(mid).0 >= 0.0 {
2933 lo = mid;
2934 } else {
2935 hi = mid;
2936 }
2937 }
2938 let (_, vp, vm) = roots(lo);
2939 sweep.evaluate(lo, 0.5 * (vp + vm))
2940 };
2941 let Some(first_gap) = samples.iter().position(Option::is_none) else {
2942 return Vec::new();
2943 };
2944 let mut loops = Vec::new();
2945 let mut k = 0;
2946 while k < RULING_SAMPLES {
2947 let i = (first_gap + k) % RULING_SAMPLES;
2948 if samples[i].is_none() {
2949 k += 1;
2950 continue;
2951 }
2952 let start = i;
2953 let mut run = Vec::new();
2954 while k < RULING_SAMPLES {
2955 let j = (first_gap + k) % RULING_SAMPLES;
2956 let Some(pair) = samples[j] else { break };
2957 run.push(pair);
2958 k += 1;
2959 }
2960 let end = (start + run.len() - 1) % RULING_SAMPLES;
2961 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
2962 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
2963 let mut pts = vec![head];
2964 pts.extend(run.iter().map(|p| p.0));
2965 pts.push(tail);
2966 pts.extend(run.iter().rev().map(|p| p.1));
2967 pts.push(head);
2968 loops.push(pts);
2969 }
2970 loops
2971}
2972
2973fn fit_ruling_loops(
2976 loops: &[Vec<Point3>],
2977 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
2978) -> Vec<IntersectionCurve> {
2979 let mut curves = Vec::new();
2980 for pts in loops {
2981 if pts.len() < 4 {
2982 continue;
2983 }
2984 let ipts: Vec<IntersectionPoint> = pts
2985 .iter()
2986 .map(|&p| {
2987 let (param1, param2) = params(p);
2988 IntersectionPoint {
2989 point: p,
2990 param1,
2991 param2,
2992 }
2993 })
2994 .collect();
2995 let degree = 3.min(pts.len() - 1);
2996 if let Ok(curve) = interpolate(pts, degree) {
2997 curves.push(IntersectionCurve {
2998 curve,
2999 points: ipts,
3000 });
3001 }
3002 }
3003 curves
3004}
3005
3006#[allow(clippy::unnecessary_wraps)]
3032fn algebraic_parallel_cone_cylinder(
3033 cone: &ConicalSurface,
3034 cyl: &CylindricalSurface,
3035 v_range_cone: Option<(f64, f64)>,
3036 v_range_cyl: Option<(f64, f64)>,
3037) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3038 let axis = cone.axis();
3039 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3040 return Ok(None); }
3042
3043 let apex = cone.apex();
3044 let delta = cyl.origin() - apex;
3045 let along = delta.dot(axis);
3046 let perp = delta - axis * along;
3047 let d = perp.length();
3048 if d < 1e-9 {
3049 return Ok(None); }
3051
3052 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3053 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3054
3055 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3056 if cos_t < 1e-12 || sin_t < 1e-12 {
3057 return Ok(None);
3058 }
3059 let r = cyl.radius();
3060
3061 let mut v_min = (d - r).abs() / cos_t;
3063 let mut v_max = (d + r) / cos_t;
3064 if v_max <= v_min {
3065 return Ok(Some(vec![]));
3066 }
3067
3068 let mut lo = v_min;
3074 let mut hi = v_max;
3075 if let Some((a, b)) = v_range_cone {
3080 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3081 lo = lo.max(a);
3082 hi = hi.min(b);
3083 }
3084 if let Some((a, b)) = v_range_cyl {
3085 let flip = cyl.axis().dot(axis);
3088 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3089 let (a, b) = (to_cone_v(a), to_cone_v(b));
3090 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3091 lo = lo.max(a);
3092 hi = hi.min(b);
3093 }
3094 let (turn_lo, turn_hi) = (v_min, v_max);
3095 v_min = lo.max(v_min);
3096 v_max = hi.min(v_max);
3097 if v_max - v_min <= 1e-12 {
3098 return Ok(Some(vec![]));
3099 }
3100 let slack = Tolerance::new().linear;
3108 #[allow(clippy::cast_precision_loss)]
3109 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3110 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3111 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3112 .map(|i| {
3113 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3114 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3115 let off = foot - apex;
3116 let across = off - axis * off.dot(axis);
3117 apex + across + axis * (across.length() * sin_t / cos_t)
3118 })
3119 .collect();
3120 pts.push(pts[0]);
3121 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3122 (cone.project_point(p), cyl.project_point(p))
3123 })));
3124 }
3125
3126 let n_samples = 128;
3127 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3128 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3129 #[allow(clippy::cast_precision_loss)]
3130 for i in 0..=n_samples {
3131 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3132 let rho = v * cos_t;
3133 if rho < 1e-12 {
3134 if (d - r).abs() < 1e-12 {
3142 let apex = cone.evaluate(phi0, v);
3143 plus.push(apex);
3144 minus.push(apex);
3145 }
3146 continue;
3147 }
3148 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3149 let alpha = cos_alpha.acos();
3150 plus.push(cone.evaluate(phi0 + alpha, v));
3151 minus.push(cone.evaluate(phi0 - alpha, v));
3152 }
3153
3154 let mut curves = Vec::new();
3155 for pts in [&plus, &minus] {
3156 if pts.len() < 4 {
3159 continue;
3160 }
3161 let ipts: Vec<IntersectionPoint> = pts
3162 .iter()
3163 .map(|&p| IntersectionPoint {
3164 point: p,
3165 param1: cone.project_point(p),
3166 param2: cyl.project_point(p),
3167 })
3168 .collect();
3169 let degree = 3.min(pts.len() - 1);
3170 match interpolate(pts, degree) {
3171 Ok(curve) => curves.push(IntersectionCurve {
3172 curve,
3173 points: ipts,
3174 }),
3175 Err(_) => return Ok(None),
3180 }
3181 }
3182
3183 Ok(Some(curves))
3184}
3185
3186fn algebraic_sphere_sphere(
3194 s1: &SphericalSurface,
3195 s2: &SphericalSurface,
3196) -> Result<Vec<IntersectionCurve>, MathError> {
3197 let c1 = s1.center();
3198 let c2 = s2.center();
3199 let r1 = s1.radius();
3200 let r2 = s2.radius();
3201
3202 let delta = c2 - c1;
3203 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3204 let d = d_sq.sqrt();
3205
3206 if d < 1e-12 {
3207 return Ok(vec![]);
3209 }
3210
3211 if d > r1 + r2 + 1e-10 {
3213 return Ok(vec![]); }
3215 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3216 return Ok(vec![]); }
3218
3219 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3221
3222 let r_circle_sq = r1 * r1 - d1 * d1;
3224 if r_circle_sq < 0.0 {
3225 if r_circle_sq > -1e-10 {
3227 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3229 let tangent_pt = Point3::new(
3230 c1.x() + axis.x() * d1,
3231 c1.y() + axis.y() * d1,
3232 c1.z() + axis.z() * d1,
3233 );
3234 let ipt = IntersectionPoint {
3235 point: tangent_pt,
3236 param1: (0.0, 0.0),
3237 param2: (0.0, 0.0),
3238 };
3239 return Ok(vec![IntersectionCurve {
3241 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3242 points: vec![ipt],
3243 }]);
3244 }
3245 return Ok(vec![]);
3246 }
3247
3248 let r_circle = r_circle_sq.sqrt();
3249 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3250 let center = Point3::new(
3251 c1.x() + axis.x() * d1,
3252 c1.y() + axis.y() * d1,
3253 c1.z() + axis.z() * d1,
3254 );
3255
3256 let basis = Frame3::from_normal(center, axis)?;
3258 let u_dir = basis.x;
3259 let v_dir = basis.y;
3260
3261 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3264 let mut positions = Vec::with_capacity(n_samples);
3265 #[allow(clippy::cast_precision_loss)]
3266 for i in 0..n_samples {
3267 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3268 let (sin_t, cos_t) = theta.sin_cos();
3269 let pt = Point3::new(
3270 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3271 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3272 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3273 );
3274 positions.push(pt);
3275 points.push(IntersectionPoint {
3276 point: pt,
3277 param1: (0.0, 0.0),
3278 param2: (0.0, 0.0),
3279 });
3280 }
3281
3282 let degree = 3.min(positions.len() - 1);
3283 let curve = interpolate(&positions, degree)?;
3284
3285 Ok(vec![IntersectionCurve { curve, points }])
3286}
3287
3288#[allow(clippy::too_many_arguments)]
3294fn correct_to_intersection(
3295 a: &AnalyticSurface<'_>,
3296 b: &AnalyticSurface<'_>,
3297 surf_a: &dyn Fn(f64, f64) -> Point3,
3298 norm_a: &dyn Fn(f64, f64) -> Vec3,
3299 surf_b: &dyn Fn(f64, f64) -> Point3,
3300 norm_b: &dyn Fn(f64, f64) -> Vec3,
3301 point: Point3,
3302 u_range_a: (f64, f64),
3303 v_range_a: (f64, f64),
3304 u_range_b: (f64, f64),
3305 v_range_b: (f64, f64),
3306 max_iters: usize,
3307) -> Point3 {
3308 let mut p = point;
3309 for _ in 0..max_iters {
3310 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3311 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3312 let pa = surf_a(ua, va);
3313 let pb = surf_b(ub, vb);
3314 let na = norm_a(ua, va);
3315 let nb = norm_b(ub, vb);
3316 let pv = Vec3::new(p.x(), p.y(), p.z());
3317
3318 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3319 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3320
3321 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3322 break;
3323 }
3324
3325 let t = na.cross(nb);
3326 let t_len = t.length();
3327 if t_len < 1e-10 {
3328 return Point3::new(
3330 (pa.x() + pb.x()) * 0.5,
3331 (pa.y() + pb.y()) * 0.5,
3332 (pa.z() + pb.z()) * 0.5,
3333 );
3334 }
3335 let t_hat = t * (1.0 / t_len);
3336
3337 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3339 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3340 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3341 if det.abs() < 1e-15 {
3342 return Point3::new(
3343 (pa.x() + pb.x()) * 0.5,
3344 (pa.y() + pb.y()) * 0.5,
3345 (pa.z() + pb.z()) * 0.5,
3346 );
3347 }
3348 let inv = 1.0 / det;
3349 let dx = inv
3351 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3352 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3353 let dy = inv
3354 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3355 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3356 let dz = inv
3357 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3358 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3359 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3360
3361 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3364 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3365 let pc_a = surf_a(uc, vc);
3366 let pc_b = surf_b(ud, vd);
3367 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3368 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3369 .dot(norm_a(uc, vc))
3370 .abs();
3371 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3372 .dot(norm_b(ud, vd))
3373 .abs();
3374 if da_new > da.abs() && db_new > db.abs() {
3375 return p;
3376 }
3377
3378 p = candidate;
3379 }
3380 p
3381}
3382
3383#[allow(clippy::too_many_arguments)]
3389fn march_analytic_intersection(
3390 a: &AnalyticSurface<'_>,
3391 b: &AnalyticSurface<'_>,
3392 surf_a: &dyn Fn(f64, f64) -> Point3,
3393 norm_a: &dyn Fn(f64, f64) -> Vec3,
3394 surf_b: &dyn Fn(f64, f64) -> Point3,
3395 norm_b: &dyn Fn(f64, f64) -> Vec3,
3396 seed: Point3,
3397 u_range_a: (f64, f64),
3398 v_range_a: (f64, f64),
3399 u_range_b: (f64, f64),
3400 v_range_b: (f64, f64),
3401 initial_step: f64,
3402 u_periodic_a: bool,
3403 u_periodic_b: bool,
3404) -> Vec<Point3> {
3405 let max_steps = 500;
3406 let h_min = 1e-6;
3407 let h_max = initial_step * 4.0;
3408 let closure_dist = initial_step * 5.0;
3412 let max_angle = 10.0_f64.to_radians();
3414 let min_angle = 2.0_f64.to_radians();
3415
3416 let mut forward = Vec::new();
3418 let mut backward = Vec::new();
3420
3421 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3422 let mut current = seed;
3423 let mut h = initial_step;
3424 let mut prev_tangent: Option<Vec3> = None;
3425
3426 for _ in 0..max_steps {
3427 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3428 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3429
3430 let na = norm_a(ua, va);
3431 let nb = norm_b(ub, vb);
3432
3433 let tangent = na.cross(nb);
3434 let t_len = tangent.length();
3435 if t_len < 1e-10 {
3436 break;
3437 }
3438 let t_dir = tangent * (direction / t_len);
3439
3440 if let Some(prev_t) = prev_tangent {
3442 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3443 let angle = cos_angle.acos();
3444 if angle > max_angle && h > h_min {
3445 h = (h * 0.5).max(h_min);
3446 } else if angle < min_angle {
3447 h = (h * 2.0).min(h_max);
3448 }
3449 }
3450 prev_tangent = Some(t_dir);
3451
3452 let next = Point3::new(
3453 h.mul_add(t_dir.x(), current.x()),
3454 h.mul_add(t_dir.y(), current.y()),
3455 h.mul_add(t_dir.z(), current.z()),
3456 );
3457
3458 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3459 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3460
3461 let pa = surf_a(ua2, va2);
3462 let pb = surf_b(ub2, vb2);
3463 let mid = Point3::new(
3464 (pa.x() + pb.x()) * 0.5,
3465 (pa.y() + pb.y()) * 0.5,
3466 (pa.z() + pb.z()) * 0.5,
3467 );
3468 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3469 || va2 <= v_range_a.0
3470 || va2 >= v_range_a.1;
3471 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3472 || vb2 <= v_range_b.0
3473 || vb2 >= v_range_b.1;
3474
3475 if out_a || out_b {
3476 break;
3477 }
3478
3479 let dist_to_seed = (mid - seed).length();
3483 if points.len() > 10 && dist_to_seed < closure_dist {
3484 points.push(seed);
3485 break;
3486 }
3487
3488 points.push(mid);
3489 current = mid;
3490 }
3491 }
3492
3493 backward.reverse();
3495 let mut result = backward;
3496 result.push(seed);
3497 result.append(&mut forward);
3498
3499 for pt in &mut result {
3501 *pt = correct_to_intersection(
3502 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3503 5,
3504 );
3505 }
3506
3507 result
3508}
3509
3510fn project_analytic(
3514 surface: &AnalyticSurface<'_>,
3515 point: Point3,
3516 u_range: (f64, f64),
3517 v_range: (f64, f64),
3518) -> (f64, f64) {
3519 match surface {
3520 AnalyticSurface::Cylinder(cyl) => {
3521 let (u, v) = cyl.project_point(point);
3522 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3523 }
3524 AnalyticSurface::Sphere(sphere) => {
3525 let (u, v) = sphere.project_point(point);
3526 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3527 }
3528 AnalyticSurface::Cone(cone) => {
3529 let (u, v) = cone.project_point(point);
3530 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3531 }
3532 AnalyticSurface::Torus(torus) => {
3533 let (u, v) = torus.project_point(point);
3534 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3535 }
3536 }
3537}
3538
3539fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3543 matches!(
3544 surface,
3545 AnalyticSurface::Cylinder(_)
3546 | AnalyticSurface::Cone(_)
3547 | AnalyticSurface::Sphere(_)
3548 | AnalyticSurface::Torus(_)
3549 )
3550}
3551
3552#[allow(clippy::type_complexity)]
3554fn surface_closures<'a>(
3555 surface: &'a AnalyticSurface<'a>,
3556) -> (
3557 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3558 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3559 (f64, f64),
3560 (f64, f64),
3561) {
3562 match surface {
3563 AnalyticSurface::Cylinder(cyl) => (
3564 Box::new(|u, v| cyl.evaluate(u, v)),
3565 Box::new(|u, v| cyl.normal(u, v)),
3566 (0.0, TAU),
3567 (-1.0, 1.0),
3568 ),
3569 AnalyticSurface::Cone(cone) => (
3570 Box::new(|u, v| cone.evaluate(u, v)),
3571 Box::new(|u, v| cone.normal(u, v)),
3572 (0.0, TAU),
3573 (0.01, 2.0),
3574 ),
3575 AnalyticSurface::Sphere(sphere) => (
3576 Box::new(|u, v| sphere.evaluate(u, v)),
3577 Box::new(|u, v| sphere.normal(u, v)),
3578 (0.0, TAU),
3579 (-FRAC_PI_2, FRAC_PI_2),
3580 ),
3581 AnalyticSurface::Torus(torus) => (
3582 Box::new(|u, v| torus.evaluate(u, v)),
3583 Box::new(|u, v| torus.normal(u, v)),
3584 (0.0, TAU),
3585 (0.0, TAU),
3586 ),
3587 }
3588}
3589
3590#[cfg(test)]
3591#[allow(clippy::unwrap_used, clippy::expect_used)]
3592mod tests {
3593 use super::*;
3594 use crate::tolerance::Tolerance;
3595
3596 #[test]
3600 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3601 let half_angle = 1.1_f64;
3602 let cone = ConicalSurface::new(
3603 Point3::new(0.0, 0.0, 0.0),
3604 Vec3::new(0.0, 0.0, 1.0),
3605 half_angle,
3606 )
3607 .unwrap();
3608 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3609 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3610 let chains =
3611 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3612 .unwrap();
3613 let chain = chains
3614 .iter()
3615 .find_map(|c| match c {
3616 ExactIntersectionCurve::Points(chain) => Some(chain),
3617 _ => None,
3618 })
3619 .expect("a parabola or hyperbola section is sampled");
3620 let (from, to) = (chain[2], chain[chain.len() - 3]);
3621 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3622 .unwrap()
3623 .expect("an exact arc");
3624 let (t0, t1) = arc.domain();
3625 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3626 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3627 for i in 0..=200 {
3628 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3629 let w = q - Point3::new(0.0, 0.0, 0.0);
3630 let off_plane = (normal.dot(w) - d).abs();
3631 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3632 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3633 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3634 }
3635 }
3636 }
3637
3638 #[test]
3642 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3643 let half_angle = 1.1_f64;
3644 let cone = ConicalSurface::new(
3645 Point3::new(0.0, 0.0, 0.0),
3646 Vec3::new(0.0, 0.0, 1.0),
3647 half_angle,
3648 )
3649 .unwrap();
3650 for shortfall in [1e-10, 3e-10, 8e-10] {
3651 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3652 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3653 let chains =
3654 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3655 .unwrap();
3656 let Some(chain) = chains.iter().find_map(|c| match c {
3657 ExactIntersectionCurve::Points(chain) => Some(chain),
3658 _ => None,
3659 }) else {
3660 continue;
3661 };
3662 let (from, to) = (chain[2], chain[chain.len() - 3]);
3663 assert!(
3664 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3665 .unwrap()
3666 .is_none(),
3667 "coincident ends"
3668 );
3669 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3670 continue;
3671 };
3672 let (t0, t1) = arc.domain();
3673 for i in 0..=200 {
3674 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3675 - Point3::new(0.0, 0.0, 0.0);
3676 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3677 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3678 }
3679 }
3680 }
3681
3682 #[test]
3683 fn plane_cylinder_perpendicular() {
3684 let cyl =
3685 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3686 .unwrap();
3687
3688 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3690 assert!(!curves.is_empty(), "should find intersection curve");
3691 assert!(
3692 curves[0].points.len() > 10,
3693 "should have many sample points"
3694 );
3695
3696 let tol = Tolerance::loose();
3697 for pt in &curves[0].points {
3698 assert!(
3699 tol.approx_eq(pt.point.z(), 3.0),
3700 "z should be ~3.0, got {}",
3701 pt.point.z()
3702 );
3703 let r = pt.point.x().hypot(pt.point.y());
3704 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3705 }
3706 }
3707
3708 #[test]
3709 fn plane_sphere_equator() {
3710 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3711
3712 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3713 assert!(!curves.is_empty());
3714
3715 let tol = Tolerance::loose();
3716 for pt in &curves[0].points {
3717 assert!(
3718 tol.approx_eq(pt.point.z(), 0.0),
3719 "z should be ~0, got {}",
3720 pt.point.z()
3721 );
3722 let r = pt.point.x().hypot(pt.point.y());
3723 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3724 }
3725 }
3726
3727 #[test]
3728 fn plane_sphere_no_intersection() {
3729 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3730
3731 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3732 assert!(curves.is_empty());
3733 }
3734
3735 #[test]
3736 fn plane_cone_cross_section() {
3737 let cone = ConicalSurface::new(
3738 Point3::new(0.0, 0.0, 0.0),
3739 Vec3::new(0.0, 0.0, 1.0),
3740 std::f64::consts::FRAC_PI_4,
3741 )
3742 .unwrap();
3743
3744 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3745 assert!(!curves.is_empty(), "should find intersection with cone");
3746 }
3747
3748 #[test]
3755 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3756 let c1 = ConicalSurface::new(
3757 Point3::new(
3758 -16.999_999_999_999_975,
3759 -16.999_999_999_999_975,
3760 5.849_999_999_999_951,
3761 ),
3762 Vec3::new(0.0, 0.0, -1.0),
3763 0.785_398_163_397_433_5,
3764 )
3765 .unwrap();
3766 let c2 = ConicalSurface::new(
3767 Point3::new(
3768 -16.750_000_000_000_036,
3769 -16.750_000_000_000_018,
3770 0.749_999_999_999_881,
3771 ),
3772 Vec3::new(0.0, 0.0, 1.0),
3773 0.785_398_163_397_467_6,
3774 )
3775 .unwrap();
3776
3777 let curves = exact_cone_cone(&c1, &c2)
3778 .unwrap()
3779 .expect("offset parallel equal-angle cones must take the radical-plane path");
3780 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3781 assert!(
3782 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3783 "expected an ellipse section, got {:?}",
3784 curves[0]
3785 );
3786 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3787 return;
3788 };
3789
3790 for i in 0..16 {
3794 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3795 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3796 let rel = p - cone.apex();
3797 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3798 let axial = rel_v.dot(cone.axis());
3799 let radial = (rel_v - cone.axis() * axial).length();
3800 assert!(
3801 axial > 0.0,
3802 "{label}: sample on phantom nappe (axial {axial})"
3803 );
3804 let expect = cone.half_angle().tan() * axial;
3805 assert!(
3806 (radial - expect).abs() < 1e-9,
3807 "{label}: sample off surface by {}",
3808 (radial - expect).abs()
3809 );
3810 }
3811 }
3812 }
3813
3814 #[test]
3818 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3819 let c1 = ConicalSurface::new(
3820 Point3::new(0.0, 0.0, 5.0),
3821 Vec3::new(0.0, 0.0, -1.0),
3822 std::f64::consts::FRAC_PI_4,
3823 )
3824 .unwrap();
3825 let c2 = ConicalSurface::new(
3826 Point3::new(0.25, 0.25, 20.0),
3827 Vec3::new(0.0, 0.0, 1.0),
3828 std::f64::consts::FRAC_PI_4,
3829 )
3830 .unwrap();
3831 let curves = exact_cone_cone(&c1, &c2)
3832 .unwrap()
3833 .expect("radical-plane path");
3834 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3835 }
3836
3837 #[test]
3840 fn offset_parallel_cones_with_unequal_angles_defer() {
3841 let c1 = ConicalSurface::new(
3842 Point3::new(0.0, 0.0, 5.0),
3843 Vec3::new(0.0, 0.0, -1.0),
3844 std::f64::consts::FRAC_PI_4,
3845 )
3846 .unwrap();
3847 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
3848 .unwrap();
3849 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
3850 }
3851
3852 #[test]
3853 fn coaxial_cones_cross_at_single_circle() {
3854 let outer = ConicalSurface::new(
3859 Point3::new(0.0, 0.0, 50.0),
3860 Vec3::new(0.0, 0.0, -1.0),
3861 5.0_f64.atan(),
3862 )
3863 .unwrap();
3864 let inner = ConicalSurface::new(
3865 Point3::new(0.0, 0.0, 90.0),
3866 Vec3::new(0.0, 0.0, -1.0),
3867 10.0_f64.atan(),
3868 )
3869 .unwrap();
3870
3871 let curves = intersect_analytic_analytic_bounded(
3872 AnalyticSurface::Cone(&outer),
3873 AnalyticSurface::Cone(&inner),
3874 32,
3875 None,
3876 None,
3877 )
3878 .unwrap();
3879
3880 assert_eq!(
3881 curves.len(),
3882 1,
3883 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
3884 curves.len()
3885 );
3886 for p in &curves[0].points {
3887 let r = p.point.x().hypot(p.point.y());
3888 assert!(
3889 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
3890 "intersection point off the expected z=10,r=8 circle: {:?}",
3891 p.point
3892 );
3893 }
3894 }
3895
3896 #[test]
3897 fn plane_torus_cross_section() {
3898 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
3899
3900 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3901 assert!(
3902 !curves.is_empty(),
3903 "should find intersection curves with torus"
3904 );
3905 }
3906
3907 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
3910 let rho = p.x().hypot(p.y());
3911 ((rho - major).hypot(p.z())) - minor
3912 }
3913
3914 #[test]
3920 fn oblique_cone_cylinder_traces_curves_on_both() {
3921 use crate::traits::ParametricCurve;
3922 let cone = ConicalSurface::new(
3926 Point3::new(0.0, 0.0, 3.0),
3927 Vec3::new(0.0, 0.0, -1.0),
3928 2.0_f64.atan(),
3929 )
3930 .unwrap();
3931 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
3932 let cyl =
3933 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
3934 .unwrap();
3935 for cone_first in [true, false] {
3936 let (a, b) = if cone_first {
3937 (
3938 AnalyticSurface::Cone(&cone),
3939 AnalyticSurface::Cylinder(&cyl),
3940 )
3941 } else {
3942 (
3943 AnalyticSurface::Cylinder(&cyl),
3944 AnalyticSurface::Cone(&cone),
3945 )
3946 };
3947 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
3948 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
3949 for c in &curves {
3950 let (t0, t1) = c.curve.domain();
3951 for k in 0..=64 {
3952 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
3953 let p = ParametricCurve::evaluate(&c.curve, t);
3954 let rod = (p.x() - x0).hypot(p.z() - 1.0);
3957 assert!(
3958 (rod - 0.6).abs() < 1e-4,
3959 "x0 {x0}: off the rod by {}",
3960 rod - 0.6
3961 );
3962 let cone_r = p.x().hypot(p.y());
3963 assert!(
3964 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
3965 "x0 {x0}: off the cone at {p:?}"
3966 );
3967 }
3968 }
3969 }
3970 }
3971 }
3972
3973 #[test]
3974 fn a_rod_through_a_rings_tube_traces_four_loops() {
3975 use crate::traits::ParametricCurve;
3976 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
3977 let rod =
3980 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
3981 .unwrap();
3982 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
3983 assert_eq!(curves.len(), 4);
3984 for c in &curves {
3985 let (t0, t1) = c.curve.domain();
3986 for k in 0..=64 {
3987 let p =
3988 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
3989 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
3990 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
3991 assert!(
3992 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
3993 "off by {on_rod}, {on_ring}"
3994 );
3995 }
3996 }
3997 let high =
3999 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4000 .unwrap();
4001 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4002 let grazing =
4004 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4005 .unwrap();
4006 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4007 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4009 let thin =
4010 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4011 .unwrap();
4012 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4013 }
4014
4015 #[test]
4016 fn a_pin_through_a_ball_traces_two_loops() {
4017 use crate::traits::ParametricCurve;
4018 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4019 let half = 0.08_f64.atan();
4022 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4023 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4024 for cone_first in [true, false] {
4025 let (a, b) = if cone_first {
4026 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4027 } else {
4028 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4029 };
4030 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4031 assert_eq!(curves.len(), 2, "entry and exit loops");
4032 for c in &curves {
4033 let (t0, t1) = c.curve.domain();
4034 for k in 0..=64 {
4035 let p = ParametricCurve::evaluate(
4036 &c.curve,
4037 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4038 );
4039 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4040 let axial = apex.z() - p.z();
4041 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4042 assert!(
4043 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4044 "off by {on_ball}, {on_pin}"
4045 );
4046 }
4047 }
4048 }
4049 let coaxial =
4054 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4055 .unwrap();
4056 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4057 let aside = ConicalSurface::new(
4058 Point3::new(2.8, 0.0, 10.0),
4059 Vec3::new(0.0, 0.0, -1.0),
4060 FRAC_PI_2 - half,
4061 )
4062 .unwrap();
4063 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4064 let holding = ConicalSurface::new(
4065 Point3::new(1.0, 0.5, 1.0),
4066 Vec3::new(0.0, 0.0, -1.0),
4067 FRAC_PI_2 - half,
4068 )
4069 .unwrap();
4070 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4071 let away = ConicalSurface::new(
4072 Point3::new(1.0, 0.5, 10.0),
4073 Vec3::new(0.0, 0.0, 1.0),
4074 FRAC_PI_2 - half,
4075 )
4076 .unwrap();
4077 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4078 let step = TAU / 2048.0;
4082 let grazed =
4083 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4084 .unwrap();
4085 let wide =
4086 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4087 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4088 }
4089
4090 #[test]
4091 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4092 use crate::traits::ParametricCurve;
4093 let cone = ConicalSurface::new(
4095 Point3::new(0.0, 0.0, 3.0),
4096 Vec3::new(0.0, 0.0, -1.0),
4097 2.0_f64.atan(),
4098 )
4099 .unwrap();
4100 for (centre, radius) in [
4101 (Point3::new(1.0, 0.8, 1.2), 1.1),
4102 (Point3::new(1.5, 0.0, 0.0), 0.8),
4103 ] {
4104 let ball = SphericalSurface::new(centre, radius).unwrap();
4105 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4106 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4107 let (t0, t1) = curves[0].curve.domain();
4108 for k in 0..=64 {
4109 let p = ParametricCurve::evaluate(
4110 &curves[0].curve,
4111 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4112 );
4113 let on_ball = (p - centre).length() - radius;
4114 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4115 assert!(
4116 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4117 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4118 );
4119 }
4120 }
4121 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4123 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4124 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4125 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4126 }
4127
4128 #[test]
4129 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4130 use crate::traits::ParametricCurve;
4131 let cone = ConicalSurface::new(
4132 Point3::new(0.0, 0.0, 3.0),
4133 Vec3::new(0.0, 0.0, -1.0),
4134 2.0_f64.atan(),
4135 )
4136 .unwrap();
4137 for (centre, radius) in [
4140 (Point3::new(0.5, 0.0, 2.5), 2.0),
4141 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4142 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4143 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4144 ] {
4145 let ball = SphericalSurface::new(centre, radius).unwrap();
4146 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4147 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4148 let (t0, t1) = curves[0].curve.domain();
4149 for k in 0..=4096 {
4150 let p = ParametricCurve::evaluate(
4151 &curves[0].curve,
4152 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4153 );
4154 let on_ball = (p - centre).length() - radius;
4155 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4156 assert!(
4157 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4158 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4159 );
4160 }
4161 }
4162 }
4163
4164 #[test]
4165 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4166 let t = 2.0_f64.atan();
4167 let cone =
4168 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4169 let through_apex =
4171 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4172 .unwrap();
4173 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4174 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4176 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4177 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4178 let pin =
4181 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4182 let tube =
4183 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4184 .unwrap();
4185 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4186 }
4187
4188 #[test]
4189 fn parallel_cone_cylinder_gives_two_exact_branches() {
4190 use crate::traits::ParametricCurve;
4191 let cone = ConicalSurface::new(
4192 Point3::new(-5.45, -36.55, -4.85),
4193 Vec3::new(0.0, 0.0, 1.0),
4194 std::f64::consts::FRAC_PI_4,
4195 )
4196 .unwrap();
4197 let cyl = CylindricalSurface::new(
4198 Point3::new(-8.0, -34.0, -5.0),
4199 Vec3::new(0.0, 0.0, 1.0),
4200 4.45,
4201 )
4202 .unwrap();
4203 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4205 let curves = intersect_analytic_analytic_bounded(
4206 AnalyticSurface::Cone(&cone),
4207 AnalyticSurface::Cylinder(&cyl),
4208 32,
4209 Some(v_hint),
4210 Some((0.0, 2.5)),
4211 )
4212 .unwrap();
4213
4214 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4215 for c in &curves {
4216 let (t0, t1) = c.curve.domain();
4217 for k in 0..=32 {
4218 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4219 let p = ParametricCurve::evaluate(&c.curve, t);
4220 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4222 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4223 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4225 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4226 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4228 }
4229 }
4230 }
4231
4232 #[test]
4233 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4234 use crate::traits::ParametricCurve;
4235 let cone = ConicalSurface::new(
4237 Point3::new(0.0, 0.0, 3.0),
4238 Vec3::new(0.0, 0.0, -1.0),
4239 2.0_f64.atan(),
4240 )
4241 .unwrap();
4242 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4244 let rod =
4245 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4246 .unwrap();
4247 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4248 .unwrap()
4249 .unwrap();
4250 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4251 let (t0, t1) = curves[0].curve.domain();
4252 let (first, last) = (
4253 ParametricCurve::evaluate(&curves[0].curve, t0),
4254 ParametricCurve::evaluate(&curves[0].curve, t1),
4255 );
4256 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4257 for k in 0..=64 {
4258 let p = ParametricCurve::evaluate(
4259 &curves[0].curve,
4260 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4261 );
4262 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4263 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4264 assert!(
4265 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4266 "({x}, {y}): off by {on_rod}, {on_cone}"
4267 );
4268 }
4269 }
4270 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4273 let rod =
4274 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4275 .unwrap();
4276 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4277 .unwrap()
4278 .unwrap();
4279 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4280 }
4281 }
4282
4283 #[test]
4286 fn coaxial_cone_cylinder_defers_to_other_paths() {
4287 let cone = ConicalSurface::new(
4288 Point3::new(0.0, 0.0, 0.0),
4289 Vec3::new(0.0, 0.0, 1.0),
4290 std::f64::consts::FRAC_PI_4,
4291 )
4292 .unwrap();
4293 let cyl =
4294 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4295 .unwrap();
4296 assert!(
4297 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4298 .unwrap()
4299 .is_none()
4300 );
4301 }
4302
4303 #[test]
4304 fn oblique_cone_cylinder_defers_to_other_paths() {
4305 let cone = ConicalSurface::new(
4306 Point3::new(0.0, 0.0, 0.0),
4307 Vec3::new(0.0, 0.0, 1.0),
4308 std::f64::consts::FRAC_PI_4,
4309 )
4310 .unwrap();
4311 let cyl =
4312 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4313 .unwrap();
4314 assert!(
4315 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4316 .unwrap()
4317 .is_none()
4318 );
4319 }
4320
4321 #[test]
4322 fn plane_torus_lobe_closes_and_stays_on_surface() {
4323 use crate::traits::ParametricCurve;
4324 let (major, minor) = (10.0, 3.0);
4325 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4326
4327 for (n, d) in [
4331 (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), ] {
4335 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4336 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4337 for c in &curves {
4338 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4339 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4340 assert!(
4341 (p0 - p1).length() < 1e-7,
4342 "lobe not closed: gap={} (n={n:?} d={d})",
4343 (p0 - p1).length()
4344 );
4345 for k in 0..=64 {
4347 let t = f64::from(k) / 64.0;
4348 let p = ParametricCurve::evaluate(&c.curve, t);
4349 assert!(
4350 torus_implicit(p, major, minor).abs() < 1e-2,
4351 "off-surface point {p:?} implicit={}",
4352 torus_implicit(p, major, minor)
4353 );
4354 }
4355 }
4356 }
4357 }
4358
4359 #[test]
4360 fn plane_torus_inner_tangent_figure_eight_stays_open() {
4361 use crate::traits::ParametricCurve;
4362 let (major, minor) = (10.0, 3.0);
4363 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4364
4365 let curves =
4371 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4372 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4373 let max_gap = curves
4374 .iter()
4375 .map(|c| {
4376 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4377 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4378 (p0 - p1).length()
4379 })
4380 .fold(0.0_f64, f64::max);
4381 assert!(
4382 max_gap > 1e-2,
4383 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4384 );
4385 }
4386
4387 #[test]
4388 fn line_torus_box_edge_crossing_is_exact() {
4389 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4392 let ts = intersect_line_torus(
4393 &torus,
4394 Point3::new(6.0, -4.0, -5.0),
4395 Vec3::new(0.0, 0.0, 1.0),
4396 );
4397 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4399 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4400 let rho = 6.0_f64.hypot(4.0);
4401 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4402 assert!(
4403 (zs[0] - (-z_exp)).abs() < 1e-9,
4404 "z0={} exp={}",
4405 zs[0],
4406 -z_exp
4407 );
4408 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4409 for &t in &ts {
4411 let p = Point3::new(6.0, -4.0, -5.0 + t);
4412 let rho = p.x().hypot(p.y());
4413 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4414 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4415 }
4416 }
4417
4418 #[test]
4419 fn line_torus_miss_and_tangent() {
4420 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4421 let miss = intersect_line_torus(
4423 &torus,
4424 Point3::new(20.0, 0.0, 0.0),
4425 Vec3::new(0.0, 0.0, 1.0),
4426 );
4427 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4428 let axis =
4430 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4431 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4432 }
4433
4434 #[test]
4435 fn dispatch_via_analytic_surface() {
4436 let cyl =
4437 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4438 .unwrap();
4439 let curves = intersect_plane_analytic(
4440 AnalyticSurface::Cylinder(&cyl),
4441 Vec3::new(0.0, 0.0, 1.0),
4442 0.0,
4443 )
4444 .unwrap();
4445 assert!(!curves.is_empty());
4446 }
4447
4448 #[test]
4449 fn perpendicular_cylinders_intersect() {
4450 let cyl_z =
4451 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4452 .unwrap();
4453 let cyl_x =
4454 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4455 .unwrap();
4456
4457 let curves = intersect_analytic_analytic(
4458 AnalyticSurface::Cylinder(&cyl_z),
4459 AnalyticSurface::Cylinder(&cyl_x),
4460 16,
4461 )
4462 .unwrap();
4463
4464 assert!(
4465 !curves.is_empty(),
4466 "perpendicular cylinders should intersect"
4467 );
4468
4469 for c in &curves {
4470 assert!(
4471 c.points.len() >= 2,
4472 "intersection curve should have >= 2 points, got {}",
4473 c.points.len()
4474 );
4475 }
4476 }
4477
4478 #[test]
4481 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4482 let cyl_z =
4483 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4484 .unwrap();
4485 let cyl_x =
4486 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4487 .unwrap();
4488 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4489 .unwrap()
4490 .unwrap();
4491 assert_eq!(curves.len(), 1);
4492 let curve = &curves[0].curve;
4493 let (t0, t1) = curve.domain();
4494 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4495 let off = |p: Point3| {
4496 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4497 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4498 on_z.max(on_x)
4499 };
4500 let worst = (0..=400)
4501 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4502 .fold(0.0, f64::max);
4503 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4504 }
4505
4506 #[test]
4510 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4511 let cyl_z =
4512 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4513 .unwrap();
4514 let cyl_x =
4515 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4516 .unwrap();
4517 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4518 .unwrap()
4519 .expect("the thin cylinder's sweep finds the loop");
4520 assert_eq!(curves.len(), 1);
4521 }
4522
4523 #[test]
4524 fn sphere_cylinder_intersect() {
4525 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4526 let cyl =
4527 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4528 .unwrap();
4529
4530 let curves = intersect_analytic_analytic(
4531 AnalyticSurface::Sphere(&sphere),
4532 AnalyticSurface::Cylinder(&cyl),
4533 16,
4534 )
4535 .unwrap();
4536
4537 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4541 }
4542
4543 #[test]
4544 fn exact_sphere_cylinder_coaxial_two_circles() {
4545 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4548 let cyl =
4549 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4550 .unwrap();
4551 let circles = exact_sphere_cylinder(&sphere, &cyl)
4552 .unwrap()
4553 .expect("coaxial case returns Some");
4554 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4555 let mut zs: Vec<f64> = circles
4556 .iter()
4557 .filter_map(|c| match c {
4558 ExactIntersectionCurve::Circle(circle) => {
4559 assert!(
4560 (circle.radius() - 3.0).abs() < 1e-9,
4561 "rim radius == cyl radius"
4562 );
4563 Some(circle.center().z())
4564 }
4565 _ => None,
4566 })
4567 .collect();
4568 assert_eq!(zs.len(), 2, "both sections must be exact circles");
4569 zs.sort_by(f64::total_cmp);
4570 let z = 27.0_f64.sqrt();
4571 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4572 }
4573
4574 #[test]
4575 fn exact_sphere_cylinder_non_coaxial_defers() {
4576 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4578 let cyl =
4579 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4580 .unwrap();
4581 assert!(
4582 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4583 "non-coaxial sphere/cylinder defers to the marcher"
4584 );
4585 }
4586
4587 #[test]
4588 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4589 let cone = ConicalSurface::new(
4591 Point3::new(0.0, 0.0, 3.0),
4592 Vec3::new(0.0, 0.0, -1.0),
4593 2.0_f64.atan(),
4594 )
4595 .unwrap();
4596 for (height, radius, count) in [
4597 (0.0, 2.0, 2), (2.5, 1.3, 1), (2.5, 0.5, 1), (0.0, 1.0, 0), (5.0, 1.0, 0), ] {
4603 let centre = Point3::new(0.0, 0.0, height);
4604 let ball = SphericalSurface::new(centre, radius).unwrap();
4605 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4606 let circles = circles_of(&curves);
4607 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4608 for circle in circles {
4609 for k in 0..16 {
4610 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4611 let on_ball = (p - centre).length() - radius;
4612 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4613 assert!(
4614 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4615 "ball at {height}: off by {on_ball}, {on_cone}"
4616 );
4617 }
4618 }
4619 }
4620 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4621 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4622 let wide =
4625 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4626 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4627 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4628 let circles = circles_of(&curves);
4629 assert_eq!(circles.len(), 1, "the touch");
4630 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4631 assert!(
4632 (circles[0].radius() - touch).abs() < 1e-3,
4633 "{}",
4634 circles[0].radius()
4635 );
4636 }
4637
4638 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
4640 curves
4641 .iter()
4642 .filter_map(|c| match c {
4643 ExactIntersectionCurve::Circle(circle) => Some(circle),
4644 _ => None,
4645 })
4646 .collect()
4647 }
4648
4649 fn worst_off(
4652 circles: &[&Circle3D],
4653 torus: &ToroidalSurface,
4654 other: impl Fn(Point3) -> f64,
4655 ) -> f64 {
4656 let mut worst = 0.0_f64;
4657 for circle in circles {
4658 for k in 0..16 {
4659 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4660 let q = p - torus.center();
4661 let along = q.dot(torus.z_axis());
4662 let rho = (q - torus.z_axis() * along).length();
4663 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
4664 worst = worst.max(off).max(other(p).abs());
4665 }
4666 }
4667 worst
4668 }
4669
4670 #[test]
4671 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
4672 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4673 for height in [0.0, 1.0] {
4674 let centre = Point3::new(0.0, 0.0, height);
4675 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
4676 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
4677 let circles = circles_of(&curves);
4678 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
4679 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
4680 assert!(worst < 1e-9, "height {height}: {worst}");
4681 }
4682 }
4683
4684 #[test]
4685 fn exact_sphere_torus_misses_touches_and_defers() {
4686 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4687 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
4688 assert!(
4689 exact_sphere_torus(&ball(0.0, 1.0), &torus)
4690 .unwrap()
4691 .unwrap()
4692 .is_empty(),
4693 "a small ball in the hole misses"
4694 );
4695 assert!(
4696 exact_sphere_torus(&ball(0.0, 2.5), &torus)
4697 .unwrap()
4698 .is_none(),
4699 "a ball touching the inner equator defers"
4700 );
4701 assert!(
4702 exact_sphere_torus(&ball(1.0, 3.0), &torus)
4703 .unwrap()
4704 .is_none(),
4705 "a ball off the axis defers"
4706 );
4707 let spindle = ToroidalSurface::with_axis_and_ref_dir(
4708 Point3::new(0.0, 0.0, 0.0),
4709 1.0,
4710 2.0,
4711 Vec3::new(0.0, 0.0, 1.0),
4712 Vec3::new(1.0, 0.0, 0.0),
4713 )
4714 .unwrap();
4715 assert!(
4716 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
4717 .unwrap()
4718 .is_none()
4719 );
4720 }
4721
4722 #[test]
4723 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
4724 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4725 let z = Vec3::new(0.0, 0.0, 1.0);
4726 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
4727 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
4728 let circles = circles_of(&curves);
4729 assert_eq!((curves.len(), circles.len()), (2, 2));
4730 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
4731 assert!(worst < 1e-9, "{worst}");
4732 assert!(
4733 exact_cylinder_torus(&rod(2.0), &torus)
4734 .unwrap()
4735 .unwrap()
4736 .is_empty(),
4737 "a rod clear in the hole misses"
4738 );
4739 assert!(
4740 exact_cylinder_torus(&rod(5.5), &torus).unwrap().is_none(),
4741 "a wall touching the outer equator defers"
4742 );
4743 let tilted =
4744 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
4745 .unwrap();
4746 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
4747 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
4748 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
4749 let spindle = ToroidalSurface::with_axis_and_ref_dir(
4750 Point3::new(0.0, 0.0, 0.0),
4751 1.0,
4752 2.0,
4753 z,
4754 Vec3::new(1.0, 0.0, 0.0),
4755 )
4756 .unwrap();
4757 assert!(
4758 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
4759 "a spindle torus's inner lemon also meets the rod"
4760 );
4761 }
4762
4763 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
4766 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4767 let cyl =
4768 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
4769 .unwrap();
4770 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
4771 .unwrap()
4772 .unwrap();
4773 let mut worst: f64 = 0.0;
4774 for c in &curves {
4775 for ip in &c.points {
4776 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
4777 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
4778 worst = worst
4779 .max((on_sphere - ip.point).length())
4780 .max((on_cylinder - ip.point).length());
4781 }
4782 let (t0, t1) = c.curve.domain();
4783 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
4784 for k in 0..=400 {
4785 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
4786 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
4787 let on_cylinder = ((p.x() - cylinder_origin.x())
4788 .hypot(p.y() - cylinder_origin.y())
4789 - cylinder_radius)
4790 .abs();
4791 worst = worst.max(on_sphere).max(on_cylinder);
4792 }
4793 }
4794 (curves.len(), worst)
4795 }
4796
4797 #[test]
4800 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
4801 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
4802 assert_eq!(count, 2);
4803 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
4804 }
4805
4806 #[test]
4808 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
4809 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
4810 assert_eq!(count, 1);
4811 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
4812 }
4813
4814 #[test]
4815 fn disjoint_cylinders_no_intersection() {
4816 let cyl_a =
4817 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4818 .unwrap();
4819 let cyl_b =
4820 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4821 .unwrap();
4822
4823 let curves = intersect_analytic_analytic(
4824 AnalyticSurface::Cylinder(&cyl_a),
4825 AnalyticSurface::Cylinder(&cyl_b),
4826 16,
4827 )
4828 .unwrap();
4829
4830 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
4831 }
4832
4833 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
4837 use crate::traits::ParametricCurve;
4838 match curve {
4839 ExactIntersectionCurve::Circle(c) => (0..=64)
4840 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
4841 .collect(),
4842 ExactIntersectionCurve::Ellipse(e) => (0..=64)
4843 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
4844 .collect(),
4845 ExactIntersectionCurve::Points(pts) => pts.clone(),
4846 }
4847 }
4848
4849 fn assert_on_plane_and_cone(
4852 curves: &[ExactIntersectionCurve],
4853 cone: &ConicalSurface,
4854 n: Vec3,
4855 d: f64,
4856 z_bound: (f64, f64),
4857 ) {
4858 assert!(!curves.is_empty(), "expected at least one section curve");
4859 let mut total = 0;
4860 for curve in curves {
4861 for p in collect_points(curve) {
4862 total += 1;
4863 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
4864 assert!(
4865 plane_err < 1e-9,
4866 "point off plane by {plane_err:.2e}: {p:?}"
4867 );
4868 let (u, v) = cone.project_point(p);
4869 let q = cone.evaluate(u, v);
4870 let cone_err =
4871 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
4872 .sqrt();
4873 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
4874 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
4875 assert!(
4876 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
4877 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
4878 p.z()
4879 );
4880 }
4881 }
4882 assert!(total >= 8, "too few section points ({total})");
4883 }
4884
4885 #[test]
4886 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
4887 let cone = ConicalSurface::new(
4891 Point3::new(0.0, 0.0, 0.0),
4892 Vec3::new(0.0, 0.0, 1.0),
4893 std::f64::consts::FRAC_PI_4,
4894 )
4895 .unwrap();
4896 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
4897 let d = n.z() * 5.0;
4899 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4900 assert!(
4901 curves
4902 .iter()
4903 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
4904 "oblique steep plane × cone must yield an exact Ellipse"
4905 );
4906 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
4908 }
4909
4910 #[test]
4911 fn oblique_plane_cone_wrong_nappe_is_empty() {
4912 let cone = ConicalSurface::new(
4916 Point3::new(0.0, 0.0, 0.0),
4917 Vec3::new(0.0, 0.0, 1.0),
4918 std::f64::consts::FRAC_PI_4,
4919 )
4920 .unwrap();
4921 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
4922 let d = n.z() * -5.0;
4923 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4924 assert!(
4925 curves.is_empty(),
4926 "plane on the phantom-nappe side must yield no real curve, got {}",
4927 curves.len()
4928 );
4929 }
4930
4931 #[test]
4932 fn oblique_plane_cone_parabola_on_both_single_branch() {
4933 let cone = ConicalSurface::new(
4936 Point3::new(0.0, 0.0, 0.0),
4937 Vec3::new(0.0, 0.0, 1.0),
4938 std::f64::consts::FRAC_PI_4,
4939 )
4940 .unwrap();
4941 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
4942 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4944 assert_eq!(
4945 curves.len(),
4946 1,
4947 "a parabola is a single branch, got {}",
4948 curves.len()
4949 );
4950 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
4952 }
4953
4954 #[test]
4955 fn oblique_plane_cone_hyperbola_real_nappe_only() {
4956 let cone = ConicalSurface::new(
4964 Point3::new(-59.0, -59.0, 15.85),
4965 Vec3::new(0.0, 0.0, -1.0),
4966 std::f64::consts::FRAC_PI_4,
4967 )
4968 .unwrap();
4969 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
4970 let d = -58.360_56;
4971 let cos_theta = n.dot(cone.axis()).abs();
4972 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
4973 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4974 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
4977 for c in &curves {
4979 assert!(
4980 matches!(c, ExactIntersectionCurve::Points(_)),
4981 "hyperbola must be sampled Points, not a closed conic"
4982 );
4983 }
4984 }
4985}