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 Ok(plane_torus_loops(torus, normal, d, 128)
817 .into_iter()
818 .map(|run| run.into_iter().map(|p| p.point).collect())
819 .collect())
820}
821
822#[allow(clippy::cast_precision_loss)]
832pub fn intersect_plane_cylinder(
833 cyl: &CylindricalSurface,
834 normal: Vec3,
835 d: f64,
836) -> Result<Vec<IntersectionCurve>, MathError> {
837 let n_samples = 64_usize;
838 let mut points_3d = Vec::new();
839 let mut ipoints = Vec::new();
840
841 for i in 0..=n_samples {
842 let u = TAU * (i as f64) / (n_samples as f64);
843 let base = cyl.evaluate(u, 0.0);
846 let n_dot_axis = normal.dot(cyl.axis());
847 let n_dot_base = dot_np(normal, base);
848
849 if n_dot_axis.abs() < 1e-12 {
850 if (n_dot_base - d).abs() < 1e-6 {
852 let pt = base;
853 points_3d.push(pt);
854 ipoints.push(IntersectionPoint {
855 point: pt,
856 param1: (u, 0.0),
857 param2: (0.0, 0.0),
858 });
859 }
860 } else {
861 let v = (d - n_dot_base) / n_dot_axis;
862 if v.abs() <= 100.0 {
864 let pt = cyl.evaluate(u, v);
865 points_3d.push(pt);
866 ipoints.push(IntersectionPoint {
867 point: pt,
868 param1: (u, v),
869 param2: (0.0, 0.0),
870 });
871 }
872 }
873 }
874
875 build_curves_from_points(&points_3d, ipoints)
876}
877
878#[allow(clippy::cast_precision_loss)]
887pub fn intersect_plane_sphere(
888 sphere: &SphericalSurface,
889 normal: Vec3,
890 d: f64,
891) -> Result<Vec<IntersectionCurve>, MathError> {
892 let h = dot_np(normal, sphere.center()) - d;
893 let r = sphere.radius();
894
895 if h.abs() > r - 1e-10 {
897 return Ok(vec![]);
898 }
899
900 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
901 let circle_center = Point3::new(
902 h.mul_add(-normal.x(), sphere.center().x()),
903 h.mul_add(-normal.y(), sphere.center().y()),
904 h.mul_add(-normal.z(), sphere.center().z()),
905 );
906
907 let basis = Frame3::from_normal(circle_center, normal)?;
909 let u_dir = basis.x;
910 let v_dir = basis.y;
911
912 let n_samples = 64_usize;
913 let mut points_3d = Vec::new();
914 let mut ipoints = Vec::new();
915
916 for i in 0..=n_samples {
917 let theta = TAU * (i as f64) / (n_samples as f64);
918 let (sin_t, cos_t) = theta.sin_cos();
919 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
920 points_3d.push(pt);
921 ipoints.push(IntersectionPoint {
922 point: pt,
923 param1: (theta, 0.0),
924 param2: (0.0, 0.0),
925 });
926 }
927
928 build_curves_from_points(&points_3d, ipoints)
929}
930
931#[allow(clippy::cast_precision_loss)]
940pub fn intersect_plane_cone(
941 cone: &ConicalSurface,
942 normal: Vec3,
943 d: f64,
944) -> Result<Vec<IntersectionCurve>, MathError> {
945 let n_samples = 64_usize;
946 let mut points_3d = Vec::new();
947 let mut ipoints = Vec::new();
948
949 for i in 0..n_samples {
950 let u = TAU * (i as f64) / (n_samples as f64);
951 let apex = cone.apex();
954 let n_dot_apex = dot_np(normal, apex);
955 let p1 = cone.evaluate(u, 1.0);
957 let dir = p1 - apex;
958 let n_dot_dir = normal.dot(dir);
959
960 if n_dot_dir.abs() < 1e-12 {
961 continue;
962 }
963
964 let v = (d - n_dot_apex) / n_dot_dir;
965 if v.abs() > 1e-10 && v.abs() < 100.0 {
967 let pt = cone.evaluate(u, v);
968 points_3d.push(pt);
969 ipoints.push(IntersectionPoint {
970 point: pt,
971 param1: (u, v),
972 param2: (0.0, 0.0),
973 });
974 }
975 }
976
977 build_curves_from_points(&points_3d, ipoints)
978}
979
980#[allow(clippy::unnecessary_wraps)]
992pub fn intersect_plane_torus(
993 torus: &ToroidalSurface,
994 normal: Vec3,
995 d: f64,
996) -> Result<Vec<IntersectionCurve>, MathError> {
997 let mut curves = Vec::new();
1001 for ipts in plane_torus_loops(torus, normal, d, 128) {
1002 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1003 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1004 curves.push(IntersectionCurve {
1005 curve,
1006 points: ipts,
1007 });
1008 }
1009 }
1010
1011 Ok(curves)
1012}
1013
1014const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1017
1018#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1040fn plane_torus_loops(
1041 torus: &ToroidalSurface,
1042 normal: Vec3,
1043 d: f64,
1044 n_v: usize,
1045) -> Vec<Vec<IntersectionPoint>> {
1046 let big_r = torus.major_radius();
1047 let small_r = torus.minor_radius();
1048 let a = normal.dot(torus.x_axis());
1049 let b = normal.dot(torus.y_axis());
1050 let c = normal.dot(torus.z_axis());
1051 let s = a.hypot(b);
1052 let phi = b.atan2(a);
1053 let d_local = d - dot_np(normal, torus.center());
1054 let point = |u: f64, v: f64| IntersectionPoint {
1055 point: torus.evaluate(u, v),
1056 param1: (u, v.rem_euclid(TAU)),
1057 param2: (0.0, 0.0),
1058 };
1059 let closed = |mut run: Vec<IntersectionPoint>| {
1060 run.push(run[0]);
1061 run
1062 };
1063
1064 if s < 1e-12 {
1066 if c.abs() < 1e-12 {
1067 return Vec::new();
1068 }
1069 let sin_v = d_local / (small_r * c);
1070 if sin_v.abs() > 1.0 + 1e-9 {
1071 return Vec::new();
1072 }
1073 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1074 let v1 = std::f64::consts::PI - v0;
1075 let mut vs = vec![v0];
1076 if (v1 - v0).abs() > 1e-9 {
1078 vs.push(v1);
1079 }
1080 return vs
1081 .into_iter()
1082 .map(|v| {
1083 closed(
1084 (0..n_v)
1085 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1086 .collect(),
1087 )
1088 })
1089 .collect();
1090 }
1091
1092 let step = TAU / (n_v as f64);
1095 let v_off = step * 0.5;
1096 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1098 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1099 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1100 let scan: Vec<f64> = (0..n_v).map(|i| (i as f64).mul_add(step, v_off)).collect();
1101 let touches = |lo: f64, hi: f64| {
1104 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1105 let (mut lo, mut hi) = (lo, hi);
1106 for _ in 0..80 {
1107 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1108 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1109 hi = m2;
1110 } else {
1111 lo = m1;
1112 }
1113 }
1114 1.0 - rhs_at(f64::midpoint(lo, hi)).abs() < 1e-12
1115 };
1116 let turn = |v_in: f64, v_out: f64| {
1118 let (mut lo, mut hi) = (v_in, v_out);
1119 for _ in 0..60 {
1120 let mid = f64::midpoint(lo, hi);
1121 if inside(mid) {
1122 lo = mid;
1123 } else {
1124 hi = mid;
1125 }
1126 }
1127 lo
1128 };
1129 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1130 if in_scan.iter().all(|&x| x) {
1131 let touching = scan.iter().any(|&v| touches(v, v + step));
1132 return [1.0, -1.0]
1133 .into_iter()
1134 .map(|sign| {
1135 let run: Vec<IntersectionPoint> = scan.iter().map(|&v| branch(v, sign)).collect();
1136 if touching { run } else { closed(run) }
1137 })
1138 .collect();
1139 }
1140 let Some(first) = (0..n_v).find(|&i| in_scan[i] && !in_scan[(i + n_v - 1) % n_v]) else {
1141 return Vec::new();
1142 };
1143 let mut loops = Vec::new();
1144 let mut k = 0;
1145 while k < n_v {
1146 let i = (first + k) % n_v;
1147 if !in_scan[i] {
1148 k += 1;
1149 continue;
1150 }
1151 let len = (0..n_v - k).take_while(|&j| in_scan[(i + j) % n_v]).count();
1153 let v_a = scan[i];
1154 let v_b = ((len - 1) as f64).mul_add(step, v_a);
1155 let run_v = |j: usize| (j as f64).mul_add(step, v_a);
1156 let (t_lo, t_hi) = (turn(v_a, v_a - step), turn(v_b, v_b + step));
1157 let touching = (0..len - 1).any(|j| touches(run_v(j), run_v(j + 1)));
1158 let (u_lo, u_hi) = (0..len)
1166 .map(run_v)
1167 .chain([t_lo, t_hi])
1168 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1169 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1170 (lo.min(u), hi.max(u))
1171 });
1172 let m = (len as f64)
1173 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1174 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1175 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1176 .ceil();
1177 let at = |k: f64| {
1178 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1179 (t_hi - t_lo).mul_add(f, t_lo)
1180 };
1181 let steps = m as usize;
1182 let mut pts: Vec<IntersectionPoint> =
1183 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1184 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1185 loops.push(if touching { pts } else { closed(pts) });
1186 k += len;
1187 }
1188 loops
1189}
1190
1191#[allow(clippy::cast_precision_loss)]
1200fn plane_torus_winding_loops(
1201 torus: &ToroidalSurface,
1202 normal: Vec3,
1203 d: f64,
1204 n_v: usize,
1205) -> Option<Vec<Vec<Point3>>> {
1206 let big_r = torus.major_radius();
1207 let small_r = torus.minor_radius();
1208 let a = normal.dot(torus.x_axis());
1209 let b = normal.dot(torus.y_axis());
1210 let c = normal.dot(torus.z_axis());
1211 let s = a.hypot(b);
1212 if s < 1e-12 * normal.length() || small_r >= big_r {
1213 return None;
1214 }
1215 let phi = b.atan2(a);
1216 let d_local = d - dot_np(normal, torus.center());
1217 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1218 let dense = 8 * n_v;
1219 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1220 return None;
1221 }
1222 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1223 for i in 0..n_v {
1224 let v = TAU * i as f64 / n_v as f64;
1225 let delta = rhs(v).acos();
1226 loops[0].push(torus.evaluate(phi + delta, v));
1227 loops[1].push(torus.evaluate(phi - delta, v));
1228 }
1229 Some(
1230 loops
1231 .into_iter()
1232 .map(|mut run| {
1233 run.push(run[0]);
1234 run
1235 })
1236 .collect(),
1237 )
1238}
1239
1240#[must_use]
1253pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1254 let c = torus.center();
1255 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1256 let big_r = torus.major_radius();
1257 let small_r = torus.minor_radius();
1258
1259 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1261 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1262 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1263 let (c0, c1) = (za.dot(o), za.dot(dir));
1264
1265 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1267 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1268 let g0 = a0.mul_add(
1269 a0,
1270 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1271 );
1272
1273 let four_rr = 4.0 * big_r * big_r;
1275 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1276 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1277 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1278
1279 let e4 = g2 * g2;
1281 let e3 = 2.0 * g2 * g1;
1282 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1283 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1284 let e0 = g0.mul_add(g0, -h0);
1285
1286 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1287 let impl_f = |t: f64| -> f64 {
1289 let p = origin + dir * t;
1290 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1291 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1292 (a.hypot(b) - big_r).hypot(cc) - small_r
1293 };
1294 for t in &mut roots {
1295 let eps = 1e-7;
1296 let f = impl_f(*t);
1297 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1298 if df.abs() > 1e-12 {
1299 *t -= f / df;
1300 }
1301 }
1302 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1303 roots
1304}
1305
1306fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1309 if c4.abs() < 1e-14 {
1311 return real_roots_cubic(c3, c2, c1, c0);
1312 }
1313 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1315 let eval = |z: Complex| -> Complex {
1316 let mut acc = Complex::new(1.0, 0.0);
1318 acc = acc * z + Complex::new(a, 0.0);
1319 acc = acc * z + Complex::new(b, 0.0);
1320 acc = acc * z + Complex::new(c, 0.0);
1321 acc * z + Complex::new(d, 0.0)
1322 };
1323 let seed = Complex::new(0.4, 0.9);
1325 let mut r = [
1326 Complex::new(1.0, 0.0),
1327 seed,
1328 seed * seed,
1329 seed * seed * seed,
1330 ];
1331 for _ in 0..100 {
1332 let mut max_step = 0.0_f64;
1333 for i in 0..4 {
1334 let mut denom = Complex::new(1.0, 0.0);
1335 for j in 0..4 {
1336 if i != j {
1337 denom = denom * (r[i] - r[j]);
1338 }
1339 }
1340 if denom.norm() < 1e-300 {
1341 continue;
1342 }
1343 let step = eval(r[i]) / denom;
1344 r[i] = r[i] - step;
1345 max_step = max_step.max(step.norm());
1346 }
1347 if max_step < 1e-14 {
1348 break;
1349 }
1350 }
1351 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1358 let mut out: Vec<f64> = Vec::new();
1359 for z in r {
1360 if z.im.abs() >= 1e-7 {
1361 continue;
1362 }
1363 let x = z.re;
1364 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1367 if p_real(x).abs() > 1e-6 * scale {
1368 continue;
1369 }
1370 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1371 continue;
1372 }
1373 out.push(x);
1374 }
1375 out
1376}
1377
1378fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1380 if a.abs() < 1e-14 {
1381 return real_roots_quadratic(b, c, d);
1382 }
1383 let (b, c, d) = (b / a, c / a, d / a);
1385 let p = c - b * b / 3.0;
1386 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1387 let shift = -b / 3.0;
1388 let disc = q * q / 4.0 + p * p * p / 27.0;
1389 if disc > 1e-14 {
1390 let sq = disc.sqrt();
1391 let u = (-q / 2.0 + sq).cbrt();
1392 let v = (-q / 2.0 - sq).cbrt();
1393 vec![u + v + shift]
1394 } else if disc < -1e-14 {
1395 let m = 2.0 * (-p / 3.0).sqrt();
1397 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1398 (0..3)
1399 .map(|k| {
1400 m.mul_add(
1401 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1402 shift,
1403 )
1404 })
1405 .collect()
1406 } else {
1407 let u = (-q / 2.0).cbrt();
1409 vec![2.0 * u + shift, -u + shift]
1410 }
1411}
1412
1413fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1415 if a.abs() < 1e-14 {
1416 if b.abs() < 1e-14 {
1417 return Vec::new();
1418 }
1419 return vec![-c / b];
1420 }
1421 let disc = b * b - 4.0 * a * c;
1422 if disc < 0.0 {
1423 Vec::new()
1424 } else {
1425 let sq = disc.sqrt();
1426 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1427 }
1428}
1429
1430#[derive(Clone, Copy)]
1432struct Complex {
1433 re: f64,
1434 im: f64,
1435}
1436
1437impl Complex {
1438 const fn new(re: f64, im: f64) -> Self {
1439 Self { re, im }
1440 }
1441 fn norm(self) -> f64 {
1442 self.re.hypot(self.im)
1443 }
1444}
1445
1446impl std::ops::Add for Complex {
1447 type Output = Self;
1448 fn add(self, o: Self) -> Self {
1449 Self::new(self.re + o.re, self.im + o.im)
1450 }
1451}
1452
1453impl std::ops::Sub for Complex {
1454 type Output = Self;
1455 fn sub(self, o: Self) -> Self {
1456 Self::new(self.re - o.re, self.im - o.im)
1457 }
1458}
1459
1460impl std::ops::Mul for Complex {
1461 type Output = Self;
1462 fn mul(self, o: Self) -> Self {
1463 Self::new(
1464 self.re.mul_add(o.re, -(self.im * o.im)),
1465 self.re.mul_add(o.im, self.im * o.re),
1466 )
1467 }
1468}
1469
1470impl std::ops::Div for Complex {
1471 type Output = Self;
1472 fn div(self, o: Self) -> Self {
1473 let den = o.re.mul_add(o.re, o.im * o.im);
1474 Self::new(
1475 self.re.mul_add(o.re, self.im * o.im) / den,
1476 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1477 )
1478 }
1479}
1480
1481fn build_curves_from_points(
1485 points_3d: &[Point3],
1486 ipoints: Vec<IntersectionPoint>,
1487) -> Result<Vec<IntersectionCurve>, MathError> {
1488 if points_3d.len() < 2 {
1489 return Ok(vec![]);
1490 }
1491
1492 let degree = 3.min(points_3d.len() - 1);
1493 let curve = interpolate(points_3d, degree)?;
1494 Ok(vec![IntersectionCurve {
1495 curve,
1496 points: ipoints,
1497 }])
1498}
1499
1500#[allow(
1512 clippy::cast_precision_loss,
1513 clippy::too_many_lines,
1514 clippy::similar_names,
1515 clippy::unnecessary_wraps,
1516 clippy::type_complexity
1517)]
1518pub fn intersect_analytic_analytic(
1519 a: AnalyticSurface<'_>,
1520 b: AnalyticSurface<'_>,
1521 grid_res: usize,
1522) -> Result<Vec<IntersectionCurve>, MathError> {
1523 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1524}
1525
1526pub fn intersect_analytic_analytic_bounded(
1537 a: AnalyticSurface<'_>,
1538 b: AnalyticSurface<'_>,
1539 grid_res: usize,
1540 v_range_hint_a: Option<(f64, f64)>,
1541 v_range_hint_b: Option<(f64, f64)>,
1542) -> Result<Vec<IntersectionCurve>, MathError> {
1543 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1546 return Ok(result);
1547 }
1548
1549 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1550 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1551 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1552 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1553
1554 let diag_a = {
1556 let p00 = surf_a(u_range_a.0, v_range_a.0);
1557 let p11 = surf_a(u_range_a.1, v_range_a.1);
1558 (p00 - p11).length()
1559 };
1560 let diag_b = {
1561 let p00 = surf_b(u_range_b.0, v_range_b.0);
1562 let p11 = surf_b(u_range_b.1, v_range_b.1);
1563 (p00 - p11).length()
1564 };
1565 let char_size = diag_a.min(diag_b).max(0.1);
1566
1567 #[allow(clippy::type_complexity)]
1571 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1572 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1576 let mut min_dist = f64::INFINITY;
1577
1578 #[allow(clippy::cast_precision_loss)]
1579 for ia in 0..grid_res {
1580 for ja in 0..grid_res {
1581 let ua =
1582 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1583 let va =
1584 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1585
1586 let pa = surf_a(ua, va);
1587
1588 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1590 let pb = surf_b(ub, vb);
1591 let dist = (pa - pb).length();
1592 min_dist = min_dist.min(dist);
1593
1594 if dist < seed_threshold {
1595 let mid = Point3::new(
1600 (pa.x() + pb.x()) * 0.5,
1601 (pa.y() + pb.y()) * 0.5,
1602 (pa.z() + pb.z()) * 0.5,
1603 );
1604 seeds.push((mid, (ua, va), (ub, vb)));
1605 }
1606 }
1607 }
1608
1609 let reject_dist = (char_size / grid_res as f64) * 3.0;
1618 if min_dist > reject_dist {
1619 return Ok(vec![]);
1620 }
1621
1622 if seeds.is_empty() {
1623 return Ok(vec![]);
1624 }
1625
1626 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1630 let dedup_radius = march_step * 10.0;
1631 let mut unique_seeds = Vec::new();
1632 for seed in &seeds {
1633 let dominated = unique_seeds
1634 .iter()
1635 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1636 if !dominated {
1637 unique_seeds.push(*seed);
1638 }
1639 }
1640
1641 let mut curves = Vec::new();
1643 let mut used_seeds = vec![false; unique_seeds.len()];
1644
1645 for si in 0..unique_seeds.len() {
1646 if used_seeds[si] {
1647 continue;
1648 }
1649 used_seeds[si] = true;
1650
1651 let march_result = march_analytic_intersection(
1652 &a,
1653 &b,
1654 surf_a.as_ref(),
1655 norm_a.as_ref(),
1656 surf_b.as_ref(),
1657 norm_b.as_ref(),
1658 unique_seeds[si].0,
1659 u_range_a,
1660 v_range_a,
1661 u_range_b,
1662 v_range_b,
1663 march_step,
1664 is_u_periodic(&a),
1665 is_u_periodic(&b),
1666 );
1667
1668 if march_result.len() >= 2 {
1669 for (sj, other) in unique_seeds.iter().enumerate() {
1670 if !used_seeds[sj]
1671 && march_result
1672 .iter()
1673 .any(|p| (*p - other.0).length() < dedup_radius)
1674 {
1675 used_seeds[sj] = true;
1676 }
1677 }
1678
1679 let ipts: Vec<IntersectionPoint> = march_result
1680 .iter()
1681 .map(|&pt| IntersectionPoint {
1682 point: pt,
1683 param1: (0.0, 0.0),
1684 param2: (0.0, 0.0),
1685 })
1686 .collect();
1687
1688 let degree = 3.min(march_result.len() - 1);
1689 if let Ok(curve) = interpolate(&march_result, degree) {
1690 curves.push(IntersectionCurve {
1691 curve,
1692 points: ipts,
1693 });
1694 }
1695 }
1696 }
1697
1698 Ok(curves)
1699}
1700
1701#[allow(clippy::too_many_lines)]
1715fn try_algebraic_intersection(
1716 a: &AnalyticSurface<'_>,
1717 b: &AnalyticSurface<'_>,
1718 v_range_a: Option<(f64, f64)>,
1719 v_range_b: Option<(f64, f64)>,
1720) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1721 match (a, b) {
1722 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1723 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1724 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1725 ),
1726 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1727 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1728 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1729 ),
1730 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1731 algebraic_sphere_sphere(s1, s2).map(Some)
1732 }
1733 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1734 let axis_dot = c1.axis().dot(c2.axis()).abs();
1735 if axis_dot > 1.0 - 1e-10 {
1736 let delta = c2.origin() - c1.origin();
1738 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1739 let along = delta_vec.dot(c1.axis());
1740 let perp = (delta_vec - c1.axis() * along).length();
1741 if perp < 1e-8 {
1742 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1745 return Ok(None); }
1747 return Ok(Some(vec![])); }
1749 }
1750 algebraic_cylinder_cylinder(c1, c2)
1752 }
1753 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1755 algebraic_sphere_cylinder(s, c, true)
1756 }
1757 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1758 algebraic_sphere_cylinder(s, c, false)
1759 }
1760 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1761 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1762 Ok(ruling_cone_sphere(cone, sphere, true))
1763 }
1764 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1765 Ok(ruling_cone_sphere(cone, sphere, false))
1766 }
1767 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1768 Ok(parallel_axis_torus_cylinder(t, c, true)
1769 .or_else(|| ruling_torus_cylinder(t, c, true)))
1770 }
1771 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1772 Ok(parallel_axis_torus_cylinder(t, c, false)
1773 .or_else(|| ruling_torus_cylinder(t, c, false)))
1774 }
1775 _ => Ok(None),
1776 }
1777}
1778
1779fn parallel_axis_torus_cylinder(
1786 torus: &ToroidalSurface,
1787 cyl: &CylindricalSurface,
1788 torus_first: bool,
1789) -> Option<Vec<IntersectionCurve>> {
1790 let axis = torus.z_axis();
1791 let along = cyl.axis().dot(axis);
1792 if along.abs() < 1.0 - 1e-10 {
1793 return None;
1794 }
1795 let offset = cyl.origin() - torus.center();
1796 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1797 return None;
1798 }
1799 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1800 let roots = |u: f64| {
1801 let q = cyl.evaluate(u, 0.0) - torus.center();
1802 let height = q.dot(axis);
1803 let rho = (q - axis * height).length();
1804 let reach = minor * minor - (rho - major) * (rho - major);
1805 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1806 };
1807 let samples = ruling_samples(cyl, &roots);
1808 let loops = if samples.iter().all(Option::is_some) {
1809 closed_ruling_loops(&samples)
1810 } else {
1811 partial_ruling_loops(cyl, &roots, &samples)
1812 };
1813 if loops.is_empty() {
1814 return None;
1815 }
1816 Some(fit_ruling_loops(&loops, |p| {
1817 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1818 }))
1819}
1820
1821fn meridian_crossings(
1827 first: (f64, f64, f64),
1828 second: (f64, f64, f64),
1829 scale: f64,
1830) -> Option<Vec<(f64, f64)>> {
1831 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1832 let (dx, dz) = (x2 - x1, z2 - z1);
1833 let dist = dx.hypot(dz);
1834 let slack = 1e-9 * scale;
1835 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1836 return None;
1837 }
1838 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1839 return Some(Vec::new());
1840 }
1841 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1842 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1843 let (ux, uz) = (dx / dist, dz / dist);
1844 let mut crossings = Vec::with_capacity(2);
1845 for side in [1.0, -1.0] {
1846 let rho = x1 + along * ux - side * across * uz;
1847 if rho <= slack {
1848 return None;
1849 }
1850 crossings.push((rho, z1 + along * uz + side * across * ux));
1851 }
1852 Some(crossings)
1853}
1854
1855fn circles_about_axis(
1857 base: Point3,
1858 axis: Vec3,
1859 crossings: &[(f64, f64)],
1860) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1861 crossings
1862 .iter()
1863 .map(|&(rho, z)| {
1864 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1865 })
1866 .collect()
1867}
1868
1869pub fn exact_torus_torus(
1880 first: &ToroidalSurface,
1881 second: &ToroidalSurface,
1882) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1883 let axis = first.z_axis();
1884 let scale = first.major_radius() + second.major_radius();
1885 let offset = second.center() - first.center();
1886 if first.minor_radius() >= first.major_radius()
1888 || second.minor_radius() >= second.major_radius()
1889 || axis.cross(second.z_axis()).length() > 1e-9
1890 || offset.cross(axis).length() > 1e-9 * scale
1891 {
1892 return Ok(None);
1893 }
1894 let Some(crossings) = meridian_crossings(
1895 (first.major_radius(), 0.0, first.minor_radius()),
1896 (
1897 second.major_radius(),
1898 offset.dot(axis),
1899 second.minor_radius(),
1900 ),
1901 scale,
1902 ) else {
1903 return Ok(None);
1904 };
1905 circles_about_axis(first.center(), axis, &crossings).map(Some)
1906}
1907
1908pub fn exact_cylinder_torus(
1920 cylinder: &CylindricalSurface,
1921 torus: &ToroidalSurface,
1922) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1923 let axis = torus.z_axis();
1924 let scale = torus.major_radius() + cylinder.radius();
1925 let offset = cylinder.origin() - torus.center();
1926 if torus.minor_radius() >= torus.major_radius()
1928 || axis.cross(cylinder.axis()).length() > 1e-9
1929 || offset.cross(axis).length() > 1e-9 * scale
1930 {
1931 return Ok(None);
1932 }
1933 let gap = cylinder.radius() - torus.major_radius();
1934 let small = torus.minor_radius();
1935 if (gap.abs() - small).abs() < 1e-9 * scale {
1936 return Ok(None);
1937 }
1938 if gap.abs() > small {
1939 return Ok(Some(Vec::new()));
1940 }
1941 let height = small.mul_add(small, -(gap * gap)).sqrt();
1942 circles_about_axis(
1943 torus.center(),
1944 axis,
1945 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
1946 )
1947 .map(Some)
1948}
1949
1950pub fn exact_sphere_torus(
1963 sphere: &SphericalSurface,
1964 torus: &ToroidalSurface,
1965) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1966 let axis = torus.z_axis();
1967 let scale = torus.major_radius() + sphere.radius();
1968 let offset = sphere.center() - torus.center();
1969 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
1971 return Ok(None);
1972 }
1973 let Some(crossings) = meridian_crossings(
1974 (0.0, offset.dot(axis), sphere.radius()),
1975 (torus.major_radius(), 0.0, torus.minor_radius()),
1976 scale,
1977 ) else {
1978 return Ok(None);
1979 };
1980 circles_about_axis(torus.center(), axis, &crossings).map(Some)
1981}
1982
1983pub fn exact_cone_cone(
2008 c1: &ConicalSurface,
2009 c2: &ConicalSurface,
2010) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2011 let axis = c1.axis();
2012 let axis2 = c2.axis();
2013
2014 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2016 return Ok(None); }
2018 let apex1 = c1.apex();
2019 let apex2 = c2.apex();
2020 let delta = apex2 - apex1;
2021 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2022 let along = delta_v.dot(axis);
2023 if (delta_v - axis * along).length() > 1e-8 {
2024 return offset_parallel_cone_cone(c1, c2);
2025 }
2026
2027 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2028 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2029 return Ok(None); }
2031 let m1 = c1.half_angle().cos() / s1;
2032 let m2 = c2.half_angle().cos() / s2;
2033 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2034 let d2 = along; let denom = m1 - m2 * sigma;
2037 if denom.abs() < 1e-12 {
2038 if sigma > 0.0 && d2.abs() < 1e-9 {
2041 return Ok(None);
2042 }
2043 return Ok(Some(vec![]));
2044 }
2045
2046 let t_star = (-m2 * sigma * d2) / denom;
2047 let radius = m1 * t_star;
2048 if radius < 1e-12 {
2049 return Ok(Some(vec![])); }
2051
2052 let center = Point3::new(
2053 apex1.x() + axis.x() * t_star,
2054 apex1.y() + axis.y() * t_star,
2055 apex1.z() + axis.z() * t_star,
2056 );
2057 let circle = Circle3D::new(center, axis, radius)?;
2058 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2059}
2060
2061fn offset_parallel_cone_cone(
2072 c1: &ConicalSurface,
2073 c2: &ConicalSurface,
2074) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2075 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2076 return Ok(None); }
2078 let t1 = c1.half_angle().tan();
2079 let t2 = c2.half_angle().tan();
2080 if !t1.is_finite() || !t2.is_finite() {
2081 return Ok(None);
2082 }
2083 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2084 return Ok(None);
2085 }
2086
2087 let w = c1.axis();
2088 let apex1 = c1.apex();
2089 let apex2 = c2.apex();
2090 let delta = apex2 - apex1;
2091 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2092 let s = delta_v.dot(w);
2093 let tm = 0.5 * (t1 + t2);
2094 let k = 1.0 + tm * tm;
2095
2096 let n = (delta_v - w * (k * s)) * 2.0;
2100 let n_len = n.length();
2101 if n_len < 1e-12 {
2102 return Ok(None);
2103 }
2104 let n_hat = n * (1.0 / n_len);
2105 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2106
2107 let axis2 = c2.axis();
2113 let scale = 1.0 + delta_v.length();
2114 let mut out = Vec::new();
2115 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2116 let samples: Vec<Point3> = match &curve {
2117 ExactIntersectionCurve::Circle(c) => (0..4)
2118 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2119 .collect(),
2120 ExactIntersectionCurve::Ellipse(e) => (0..4)
2121 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2122 .collect(),
2123 ExactIntersectionCurve::Points(_) => return Ok(None),
2124 };
2125 let on_real_nappe = |p: &Point3| {
2126 let rel = *p - apex2;
2127 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2128 };
2129 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2130 match hits {
2131 0 => {}
2132 4 => out.push(curve),
2133 _ => return Ok(None),
2134 }
2135 }
2136 Ok(Some(out))
2137}
2138
2139pub fn exact_cone_cylinder(
2159 cone: &ConicalSurface,
2160 cyl: &CylindricalSurface,
2161) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2162 let axis = cone.axis();
2163 let cyl_axis = cyl.axis();
2164
2165 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2167 return Ok(None);
2168 }
2169 let apex = cone.apex();
2170 let delta = apex - cyl.origin();
2171 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2172 let along = delta_v.dot(cyl_axis);
2173 if (delta_v - cyl_axis * along).length() > 1e-8 {
2174 return Ok(None);
2175 }
2176
2177 let s = cone.half_angle().sin();
2178 if s.abs() < 1e-12 {
2179 return Ok(None); }
2181 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2183 return Ok(None); }
2185
2186 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2188 return Ok(Some(vec![])); }
2190 let center = Point3::new(
2191 apex.x() + axis.x() * t_star,
2192 apex.y() + axis.y() * t_star,
2193 apex.z() + axis.z() * t_star,
2194 );
2195 let circle = Circle3D::new(center, axis, cyl.radius())?;
2196 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2197}
2198
2199fn algebraic_cone_cone(
2208 c1: &ConicalSurface,
2209 c2: &ConicalSurface,
2210) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2211 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2212 return Ok(None);
2213 };
2214 let mut curves = Vec::new();
2215 for exact in exacts {
2216 let n_samples = 33;
2217 let mut positions = Vec::with_capacity(n_samples);
2218 let mut points = Vec::with_capacity(n_samples);
2219 #[allow(clippy::cast_precision_loss)]
2220 for i in 0..n_samples {
2221 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2222 let pt = match &exact {
2223 ExactIntersectionCurve::Circle(circle) => {
2224 crate::traits::ParametricCurve::evaluate(circle, theta)
2225 }
2226 ExactIntersectionCurve::Ellipse(ellipse) => {
2227 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2228 }
2229 ExactIntersectionCurve::Points(_) => break,
2230 };
2231 positions.push(pt);
2232 points.push(IntersectionPoint {
2233 point: pt,
2234 param1: (0.0, 0.0),
2235 param2: (0.0, 0.0),
2236 });
2237 }
2238 if positions.is_empty() {
2239 continue;
2240 }
2241 let degree = 3.min(positions.len() - 1);
2242 let curve = interpolate(&positions, degree)?;
2243 curves.push(IntersectionCurve { curve, points });
2244 }
2245 Ok(Some(curves))
2246}
2247
2248pub fn exact_sphere_cylinder(
2268 sphere: &SphericalSurface,
2269 cyl: &CylindricalSurface,
2270) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2271 let sc = sphere.center();
2272 let r_sphere = sphere.radius();
2273 let co = cyl.origin();
2274 let axis = cyl.axis();
2275 let r_cyl = cyl.radius();
2276
2277 let delta = sc - co;
2279 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2280 let along = delta_vec.dot(axis);
2281 let perp_vec = delta_vec - axis * along;
2282 let d_perp = perp_vec.length();
2283
2284 if d_perp > 1e-7 {
2287 return Ok(None);
2288 }
2289
2290 if r_cyl > r_sphere + 1e-10 {
2293 return Ok(Some(vec![]));
2294 }
2295 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2296 if z_sq < 0.0 {
2297 return Ok(Some(vec![]));
2298 }
2299 let z = z_sq.sqrt();
2300
2301 let center_axis_pt = Point3::new(
2304 co.x() + axis.x() * along,
2305 co.y() + axis.y() * along,
2306 co.z() + axis.z() * along,
2307 );
2308
2309 let mut circles = Vec::new();
2310 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2311 for &z_offset in offsets {
2312 let center = Point3::new(
2313 center_axis_pt.x() + axis.x() * z_offset,
2314 center_axis_pt.y() + axis.y() * z_offset,
2315 center_axis_pt.z() + axis.z() * z_offset,
2316 );
2317 let circle = Circle3D::new(center, axis, r_cyl)?;
2318 circles.push(ExactIntersectionCurve::Circle(circle));
2319 }
2320 Ok(Some(circles))
2321}
2322
2323pub fn exact_cone_sphere(
2341 cone: &ConicalSurface,
2342 sphere: &SphericalSurface,
2343) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2344 let offset = cone.apex() - sphere.center();
2345 let along = offset.dot(cone.axis());
2346 if (offset - cone.axis() * along).length() > 1e-7 {
2347 return Ok(None);
2348 }
2349 let lin_tol = Tolerance::new().linear;
2350 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2351 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2352 let b = 2.0 * sin_a * along;
2353 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2354 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2357 if disc < -noise {
2358 return Ok(Some(vec![]));
2359 }
2360 let roots: &[f64] = if far - near < lin_tol {
2361 &[far]
2362 } else {
2363 &[near, far]
2364 };
2365 let mut circles = Vec::new();
2366 for &v in roots {
2367 if v * cos_a > lin_tol {
2368 let centre = cone.apex() + cone.axis() * (v * sin_a);
2369 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2370 circles.push(ExactIntersectionCurve::Circle(circle));
2371 }
2372 }
2373 Ok(Some(circles))
2374}
2375
2376fn algebraic_sphere_cylinder(
2385 sphere: &SphericalSurface,
2386 cyl: &CylindricalSurface,
2387 sphere_first: bool,
2388) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2389 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2390 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2391 };
2392
2393 let mut curves = Vec::new();
2394 for exact in exacts {
2395 let ExactIntersectionCurve::Circle(circle) = exact else {
2396 continue;
2397 };
2398 let n_samples = 33;
2399 let mut points = Vec::with_capacity(n_samples);
2400 let mut positions = Vec::with_capacity(n_samples);
2401 #[allow(clippy::cast_precision_loss)]
2402 for i in 0..n_samples {
2403 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2404 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2405 positions.push(pt);
2406 let (param1, param2) = in_order(
2407 sphere.project_point(pt),
2408 cyl.project_point(pt),
2409 sphere_first,
2410 );
2411 points.push(IntersectionPoint {
2412 point: pt,
2413 param1,
2414 param2,
2415 });
2416 }
2417 let degree = 3.min(positions.len() - 1);
2418 let curve = interpolate(&positions, degree)?;
2419 curves.push(IntersectionCurve { curve, points });
2420 }
2421
2422 Ok(Some(curves))
2423}
2424
2425fn off_axis_sphere_cylinder(
2434 sphere: &SphericalSurface,
2435 cyl: &CylindricalSurface,
2436 sphere_first: bool,
2437) -> Option<Vec<IntersectionCurve>> {
2438 let (centre, radius) = (sphere.center(), sphere.radius());
2439 let axis = cyl.axis();
2440 let offset = centre - cyl.origin();
2441 let axis_distance = (offset - axis * offset.dot(axis)).length();
2442 let lin_tol = Tolerance::new().linear;
2443 if axis_distance > radius + cyl.radius() + lin_tol
2444 || axis_distance + radius < cyl.radius() - lin_tol
2445 {
2446 return Some(Vec::new());
2447 }
2448 let roots = |u: f64| {
2449 let q = cyl.evaluate(u, 0.0) - centre;
2450 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2451 };
2452 let samples = ruling_samples(cyl, &roots);
2453 let loops = if samples.iter().all(Option::is_some) {
2454 closed_ruling_loops(&samples)
2455 } else {
2456 partial_ruling_loops(cyl, &roots, &samples)
2457 };
2458 if loops.is_empty() {
2459 return None;
2460 }
2461 Some(fit_ruling_loops(&loops, |p| {
2462 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2463 }))
2464}
2465
2466const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2469 if a_first { (a, b) } else { (b, a) }
2470}
2471
2472#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2486fn algebraic_cylinder_cylinder(
2487 c1: &CylindricalSurface,
2488 c2: &CylindricalSurface,
2489) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2490 let alpha = c1.axis().dot(c2.axis());
2491 let a_coeff = 1.0 - alpha * alpha;
2492
2493 if a_coeff.abs() < 1e-12 {
2495 return Ok(None);
2496 }
2497
2498 let r1 = c1.radius();
2499 let r2 = c2.radius();
2500 let o1 = c1.origin();
2501 let o2 = c2.origin();
2502 let a1 = c1.axis();
2503 let a2 = c2.axis();
2504
2505 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2508 let cross = a1.cross(a2);
2509 let cross_len = cross.length();
2510 if cross_len > 1e-12 {
2511 let axis_dist = delta.dot(cross).abs() / cross_len;
2512 if axis_dist > r1 + r2 + Tolerance::new().linear {
2513 return Ok(Some(vec![])); }
2515 }
2516
2517 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2523 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2524 let alpha = sweep.axis().dot(a);
2525 let quad = 1.0 - alpha * alpha;
2526 let (axis, sweep) = (sweep.axis(), sweep.clone());
2527 move |u: f64| {
2528 let q = sweep.evaluate(u, 0.0) - o;
2529 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2530 let b = 2.0 * (q_a1 - alpha * q_a2);
2531 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2532 ruling_quadratic(quad, b, c)
2533 }
2534 };
2535 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2536 let samples1 = ruling_samples(c1, &roots1);
2537 let loops = if samples1.iter().all(Option::is_some) {
2538 closed_ruling_loops(&samples1)
2539 } else {
2540 let samples2 = ruling_samples(c2, &roots2);
2541 if samples2.iter().all(Option::is_some) {
2542 closed_ruling_loops(&samples2)
2543 } else if samples1.iter().any(Option::is_some) {
2544 partial_ruling_loops(c1, &roots1, &samples1)
2545 } else {
2546 partial_ruling_loops(c2, &roots2, &samples2)
2547 }
2548 };
2549 if loops.is_empty() {
2550 return Ok(None);
2551 }
2552 Ok(Some(fit_ruling_loops(&loops, |p| {
2553 (c1.project_point(p), c2.project_point(p))
2554 })))
2555}
2556
2557fn ruling_cone_cylinder(
2564 cone: &ConicalSurface,
2565 cyl: &CylindricalSurface,
2566 cone_first: bool,
2567) -> Option<Vec<IntersectionCurve>> {
2568 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2569 if sin_t < 1e-12 || cos_t < 1e-12 {
2570 return None;
2571 }
2572 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2573 let s = 1.0 / (sin_t * sin_t);
2574 let alpha = w.dot(d);
2575 let quad = 1.0 - s * alpha * alpha;
2576 if quad.abs() < 1e-9 {
2577 return None;
2578 }
2579 let roots = |u: f64| {
2580 let delta = cyl.evaluate(u, 0.0) - apex;
2581 let (dd, dw) = (delta.dot(d), delta.dot(w));
2582 let b = 2.0 * (dw - s * dd * alpha);
2583 let c = delta.dot(delta) - s * dd * dd;
2584 ruling_quadratic(quad, b, c)
2585 };
2586 let lin_tol = Tolerance::new().linear;
2587 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2588 #[allow(clippy::cast_precision_loss)]
2589 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2590 let (disc, vp, vm) = roots(u);
2591 disc >= -lin_tol
2592 && [vp, vm]
2593 .iter()
2594 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2595 });
2596 if far_nappe {
2597 return None;
2598 }
2599 let samples = ruling_samples(cyl, &roots);
2600 let scan = WINDOW_SCAN * RULING_SAMPLES;
2605 #[allow(clippy::cast_precision_loss)]
2608 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2609 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2610 let mut k = start;
2611 while k < start + scan {
2612 if !meets(k) {
2613 k += 1;
2614 continue;
2615 }
2616 let first = k;
2617 while k < start + scan && meets(k) {
2618 k += 1;
2619 }
2620 let covered = (first..k)
2621 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2622 .count();
2623 if covered < WINDOW_MIN_SAMPLES {
2624 return None;
2625 }
2626 }
2627 }
2628 let loops = if samples.iter().all(Option::is_some) {
2629 closed_ruling_loops(&samples)
2630 } else {
2631 partial_ruling_loops(cyl, &roots, &samples)
2632 };
2633 if loops.is_empty() {
2634 return None;
2635 }
2636 Some(fit_ruling_loops(&loops, |p| {
2637 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2638 }))
2639}
2640
2641fn ruling_torus_cylinder(
2651 torus: &ToroidalSurface,
2652 cyl: &CylindricalSurface,
2653 torus_first: bool,
2654) -> Option<Vec<IntersectionCurve>> {
2655 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2656 || torus.minor_radius() >= torus.major_radius()
2657 {
2658 return None;
2659 }
2660 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2661 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2662 let count = rows[0].len();
2663 let scan = WINDOW_SCAN * RULING_SAMPLES;
2664 #[allow(clippy::cast_precision_loss)]
2665 if count == 0
2666 || count % 2 == 1
2667 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2668 {
2669 return None;
2670 }
2671 let loops: Vec<Vec<Point3>> = (0..count)
2672 .map(|j| {
2673 let mut pts: Vec<Point3> = rows
2674 .iter()
2675 .enumerate()
2676 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2677 .collect();
2678 pts.push(pts[0]);
2679 pts
2680 })
2681 .collect();
2682 Some(fit_ruling_loops(&loops, |p| {
2683 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2684 }))
2685}
2686
2687fn ruling_cone_sphere(
2696 cone: &ConicalSurface,
2697 sphere: &SphericalSurface,
2698 cone_first: bool,
2699) -> Option<Vec<IntersectionCurve>> {
2700 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2701 let offset = apex - centre;
2702 let lin_tol = Tolerance::new().linear;
2703 let along = offset.dot(cone.axis());
2704 let across = (offset - cone.axis() * along).length();
2705 if across < lin_tol {
2706 return None;
2707 }
2708 let k = offset.dot(offset) - radius * radius;
2714 if radius - offset.length() > lin_tol {
2715 let exit = |u: f64| {
2716 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2717 let root = h.mul_add(h, -k).sqrt();
2718 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2719 };
2720 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2725 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2726 .collect();
2727 for _ in 0..10 {
2728 let mut refined = Vec::with_capacity(2 * samples.len());
2729 for pair in samples.windows(2) {
2730 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2731 refined.push(pair[0]);
2732 let um = 0.5 * (u0 + u1);
2733 let pm = exit(um);
2734 let chord = (p1 - p0).length();
2735 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2736 refined.push((um, pm));
2737 }
2738 }
2739 refined.extend(samples.last().copied());
2740 if refined.len() == samples.len() {
2741 break;
2742 }
2743 samples = refined;
2744 }
2745 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2746 if let Some(last) = pts.last_mut() {
2747 *last = samples[0].1;
2748 }
2749 return Some(fit_ruling_loops(&[pts], |p| {
2750 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2751 }));
2752 }
2753 let crossing = |h: f64| {
2757 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2758 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2759 };
2760 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2765 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2766 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2767 {
2768 return window_cone_sphere(cone, sphere, cone_first);
2769 }
2770 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2771 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2772 .collect::<Option<_>>()?;
2773 let loops: Vec<Vec<Point3>> = [0, 1]
2774 .iter()
2775 .map(|&j| {
2776 let mut pts: Vec<Point3> = rows
2777 .iter()
2778 .enumerate()
2779 .map(|(i, &(near, far))| {
2780 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2781 })
2782 .collect();
2783 pts.push(pts[0]);
2784 pts
2785 })
2786 .collect();
2787 Some(fit_ruling_loops(&loops, |p| {
2788 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2789 }))
2790}
2791
2792fn window_cone_sphere(
2807 cone: &ConicalSurface,
2808 sphere: &SphericalSurface,
2809 cone_first: bool,
2810) -> Option<Vec<IntersectionCurve>> {
2811 let offset = cone.apex() - sphere.center();
2812 let lin_tol = Tolerance::new().linear;
2813 if offset.length() - sphere.radius() <= lin_tol {
2814 return None;
2815 }
2816 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2817 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2818 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2819 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2820 if a < lin_tol {
2821 return None;
2822 }
2823 let reach = (-k.sqrt() - c) / a;
2824 if reach <= -1.0 {
2825 return Some(Vec::new());
2826 }
2827 if reach >= 1.0 {
2828 return None;
2829 }
2830 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2831 let half = std::f64::consts::PI - half;
2832 let n = RULING_SAMPLES;
2833 let mut pts: Vec<Point3> = (0..n)
2834 .map(|i| {
2835 #[allow(clippy::cast_precision_loss)]
2836 let theta = TAU * i as f64 / n as f64;
2837 let u = half.mul_add(-theta.cos(), mid);
2838 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2839 let split = h.mul_add(h, -k).max(0.0).sqrt();
2840 cone.evaluate(u, -h - split.copysign(theta.sin()))
2841 })
2842 .collect();
2843 pts.push(pts[0]);
2844 Some(fit_ruling_loops(&[pts], |p| {
2845 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2846 }))
2847}
2848
2849const WINDOW_SCAN: usize = 16;
2852const WINDOW_MIN_SAMPLES: usize = 8;
2853
2854const RULING_SAMPLES: usize = 128;
2858
2859#[allow(clippy::cast_precision_loss)]
2860fn ruling_u(i: usize) -> f64 {
2861 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2862}
2863
2864fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2866 let disc = b * b - 4.0 * quad * c;
2867 let root = disc.max(0.0).sqrt();
2868 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2869}
2870
2871fn ruling_samples(
2875 sweep: &CylindricalSurface,
2876 roots: &impl Fn(f64) -> (f64, f64, f64),
2877) -> Vec<Option<(Point3, Point3)>> {
2878 let lin_tol = Tolerance::new().linear;
2879 (0..RULING_SAMPLES)
2880 .map(|i| {
2881 let u = ruling_u(i);
2882 let (disc, vp, vm) = roots(u);
2883 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2884 })
2885 .collect()
2886}
2887
2888fn closed_ruling_loops(samples: &[Option<(Point3, Point3)>]) -> Vec<Vec<Point3>> {
2890 let mut plus: Vec<Point3> = samples.iter().flatten().map(|s| s.0).collect();
2891 let mut minus: Vec<Point3> = samples.iter().flatten().map(|s| s.1).collect();
2892 plus.push(plus[0]);
2893 minus.push(minus[0]);
2894 vec![plus, minus]
2895}
2896
2897fn partial_ruling_loops(
2902 sweep: &CylindricalSurface,
2903 roots: &impl Fn(f64) -> (f64, f64, f64),
2904 samples: &[Option<(Point3, Point3)>],
2905) -> Vec<Vec<Point3>> {
2906 let branch_point = |inside: usize, outside: usize| -> Point3 {
2907 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
2908 if (hi - lo).abs() > std::f64::consts::PI {
2909 hi += if hi < lo { TAU } else { -TAU };
2910 }
2911 for _ in 0..60 {
2912 let mid = 0.5 * (lo + hi);
2913 if roots(mid).0 >= 0.0 {
2914 lo = mid;
2915 } else {
2916 hi = mid;
2917 }
2918 }
2919 let (_, vp, vm) = roots(lo);
2920 sweep.evaluate(lo, 0.5 * (vp + vm))
2921 };
2922 let Some(first_gap) = samples.iter().position(Option::is_none) else {
2923 return Vec::new();
2924 };
2925 let mut loops = Vec::new();
2926 let mut k = 0;
2927 while k < RULING_SAMPLES {
2928 let i = (first_gap + k) % RULING_SAMPLES;
2929 if samples[i].is_none() {
2930 k += 1;
2931 continue;
2932 }
2933 let start = i;
2934 let mut run = Vec::new();
2935 while k < RULING_SAMPLES {
2936 let j = (first_gap + k) % RULING_SAMPLES;
2937 let Some(pair) = samples[j] else { break };
2938 run.push(pair);
2939 k += 1;
2940 }
2941 let end = (start + run.len() - 1) % RULING_SAMPLES;
2942 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
2943 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
2944 let mut pts = vec![head];
2945 pts.extend(run.iter().map(|p| p.0));
2946 pts.push(tail);
2947 pts.extend(run.iter().rev().map(|p| p.1));
2948 pts.push(head);
2949 loops.push(pts);
2950 }
2951 loops
2952}
2953
2954fn fit_ruling_loops(
2957 loops: &[Vec<Point3>],
2958 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
2959) -> Vec<IntersectionCurve> {
2960 let mut curves = Vec::new();
2961 for pts in loops {
2962 if pts.len() < 4 {
2963 continue;
2964 }
2965 let ipts: Vec<IntersectionPoint> = pts
2966 .iter()
2967 .map(|&p| {
2968 let (param1, param2) = params(p);
2969 IntersectionPoint {
2970 point: p,
2971 param1,
2972 param2,
2973 }
2974 })
2975 .collect();
2976 let degree = 3.min(pts.len() - 1);
2977 if let Ok(curve) = interpolate(pts, degree) {
2978 curves.push(IntersectionCurve {
2979 curve,
2980 points: ipts,
2981 });
2982 }
2983 }
2984 curves
2985}
2986
2987#[allow(clippy::unnecessary_wraps)]
3013fn algebraic_parallel_cone_cylinder(
3014 cone: &ConicalSurface,
3015 cyl: &CylindricalSurface,
3016 v_range_cone: Option<(f64, f64)>,
3017 v_range_cyl: Option<(f64, f64)>,
3018) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3019 let axis = cone.axis();
3020 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3021 return Ok(None); }
3023
3024 let apex = cone.apex();
3025 let delta = cyl.origin() - apex;
3026 let along = delta.dot(axis);
3027 let perp = delta - axis * along;
3028 let d = perp.length();
3029 if d < 1e-9 {
3030 return Ok(None); }
3032
3033 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3034 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3035
3036 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3037 if cos_t < 1e-12 || sin_t < 1e-12 {
3038 return Ok(None);
3039 }
3040 let r = cyl.radius();
3041
3042 let mut v_min = (d - r).abs() / cos_t;
3044 let mut v_max = (d + r) / cos_t;
3045 if v_max <= v_min {
3046 return Ok(Some(vec![]));
3047 }
3048
3049 let mut lo = v_min;
3055 let mut hi = v_max;
3056 if let Some((a, b)) = v_range_cone {
3061 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3062 lo = lo.max(a);
3063 hi = hi.min(b);
3064 }
3065 if let Some((a, b)) = v_range_cyl {
3066 let flip = cyl.axis().dot(axis);
3069 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3070 let (a, b) = (to_cone_v(a), to_cone_v(b));
3071 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3072 lo = lo.max(a);
3073 hi = hi.min(b);
3074 }
3075 let (turn_lo, turn_hi) = (v_min, v_max);
3076 v_min = lo.max(v_min);
3077 v_max = hi.min(v_max);
3078 if v_max - v_min <= 1e-12 {
3079 return Ok(Some(vec![]));
3080 }
3081 let slack = Tolerance::new().linear;
3089 #[allow(clippy::cast_precision_loss)]
3090 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3091 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3092 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3093 .map(|i| {
3094 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3095 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3096 let off = foot - apex;
3097 let across = off - axis * off.dot(axis);
3098 apex + across + axis * (across.length() * sin_t / cos_t)
3099 })
3100 .collect();
3101 pts.push(pts[0]);
3102 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3103 (cone.project_point(p), cyl.project_point(p))
3104 })));
3105 }
3106
3107 let n_samples = 128;
3108 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3109 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3110 #[allow(clippy::cast_precision_loss)]
3111 for i in 0..=n_samples {
3112 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3113 let rho = v * cos_t;
3114 if rho < 1e-12 {
3115 if (d - r).abs() < 1e-12 {
3123 let apex = cone.evaluate(phi0, v);
3124 plus.push(apex);
3125 minus.push(apex);
3126 }
3127 continue;
3128 }
3129 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3130 let alpha = cos_alpha.acos();
3131 plus.push(cone.evaluate(phi0 + alpha, v));
3132 minus.push(cone.evaluate(phi0 - alpha, v));
3133 }
3134
3135 let mut curves = Vec::new();
3136 for pts in [&plus, &minus] {
3137 if pts.len() < 4 {
3140 continue;
3141 }
3142 let ipts: Vec<IntersectionPoint> = pts
3143 .iter()
3144 .map(|&p| IntersectionPoint {
3145 point: p,
3146 param1: cone.project_point(p),
3147 param2: cyl.project_point(p),
3148 })
3149 .collect();
3150 let degree = 3.min(pts.len() - 1);
3151 match interpolate(pts, degree) {
3152 Ok(curve) => curves.push(IntersectionCurve {
3153 curve,
3154 points: ipts,
3155 }),
3156 Err(_) => return Ok(None),
3161 }
3162 }
3163
3164 Ok(Some(curves))
3165}
3166
3167fn algebraic_sphere_sphere(
3175 s1: &SphericalSurface,
3176 s2: &SphericalSurface,
3177) -> Result<Vec<IntersectionCurve>, MathError> {
3178 let c1 = s1.center();
3179 let c2 = s2.center();
3180 let r1 = s1.radius();
3181 let r2 = s2.radius();
3182
3183 let delta = c2 - c1;
3184 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3185 let d = d_sq.sqrt();
3186
3187 if d < 1e-12 {
3188 return Ok(vec![]);
3190 }
3191
3192 if d > r1 + r2 + 1e-10 {
3194 return Ok(vec![]); }
3196 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3197 return Ok(vec![]); }
3199
3200 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3202
3203 let r_circle_sq = r1 * r1 - d1 * d1;
3205 if r_circle_sq < 0.0 {
3206 if r_circle_sq > -1e-10 {
3208 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3210 let tangent_pt = Point3::new(
3211 c1.x() + axis.x() * d1,
3212 c1.y() + axis.y() * d1,
3213 c1.z() + axis.z() * d1,
3214 );
3215 let ipt = IntersectionPoint {
3216 point: tangent_pt,
3217 param1: (0.0, 0.0),
3218 param2: (0.0, 0.0),
3219 };
3220 return Ok(vec![IntersectionCurve {
3222 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3223 points: vec![ipt],
3224 }]);
3225 }
3226 return Ok(vec![]);
3227 }
3228
3229 let r_circle = r_circle_sq.sqrt();
3230 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3231 let center = Point3::new(
3232 c1.x() + axis.x() * d1,
3233 c1.y() + axis.y() * d1,
3234 c1.z() + axis.z() * d1,
3235 );
3236
3237 let basis = Frame3::from_normal(center, axis)?;
3239 let u_dir = basis.x;
3240 let v_dir = basis.y;
3241
3242 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3245 let mut positions = Vec::with_capacity(n_samples);
3246 #[allow(clippy::cast_precision_loss)]
3247 for i in 0..n_samples {
3248 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3249 let (sin_t, cos_t) = theta.sin_cos();
3250 let pt = Point3::new(
3251 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3252 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3253 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3254 );
3255 positions.push(pt);
3256 points.push(IntersectionPoint {
3257 point: pt,
3258 param1: (0.0, 0.0),
3259 param2: (0.0, 0.0),
3260 });
3261 }
3262
3263 let degree = 3.min(positions.len() - 1);
3264 let curve = interpolate(&positions, degree)?;
3265
3266 Ok(vec![IntersectionCurve { curve, points }])
3267}
3268
3269#[allow(clippy::too_many_arguments)]
3275fn correct_to_intersection(
3276 a: &AnalyticSurface<'_>,
3277 b: &AnalyticSurface<'_>,
3278 surf_a: &dyn Fn(f64, f64) -> Point3,
3279 norm_a: &dyn Fn(f64, f64) -> Vec3,
3280 surf_b: &dyn Fn(f64, f64) -> Point3,
3281 norm_b: &dyn Fn(f64, f64) -> Vec3,
3282 point: Point3,
3283 u_range_a: (f64, f64),
3284 v_range_a: (f64, f64),
3285 u_range_b: (f64, f64),
3286 v_range_b: (f64, f64),
3287 max_iters: usize,
3288) -> Point3 {
3289 let mut p = point;
3290 for _ in 0..max_iters {
3291 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3292 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3293 let pa = surf_a(ua, va);
3294 let pb = surf_b(ub, vb);
3295 let na = norm_a(ua, va);
3296 let nb = norm_b(ub, vb);
3297 let pv = Vec3::new(p.x(), p.y(), p.z());
3298
3299 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3300 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3301
3302 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3303 break;
3304 }
3305
3306 let t = na.cross(nb);
3307 let t_len = t.length();
3308 if t_len < 1e-10 {
3309 return Point3::new(
3311 (pa.x() + pb.x()) * 0.5,
3312 (pa.y() + pb.y()) * 0.5,
3313 (pa.z() + pb.z()) * 0.5,
3314 );
3315 }
3316 let t_hat = t * (1.0 / t_len);
3317
3318 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3320 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3321 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3322 if det.abs() < 1e-15 {
3323 return Point3::new(
3324 (pa.x() + pb.x()) * 0.5,
3325 (pa.y() + pb.y()) * 0.5,
3326 (pa.z() + pb.z()) * 0.5,
3327 );
3328 }
3329 let inv = 1.0 / det;
3330 let dx = inv
3332 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3333 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3334 let dy = inv
3335 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3336 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3337 let dz = inv
3338 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3339 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3340 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3341
3342 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3345 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3346 let pc_a = surf_a(uc, vc);
3347 let pc_b = surf_b(ud, vd);
3348 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3349 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3350 .dot(norm_a(uc, vc))
3351 .abs();
3352 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3353 .dot(norm_b(ud, vd))
3354 .abs();
3355 if da_new > da.abs() && db_new > db.abs() {
3356 return p;
3357 }
3358
3359 p = candidate;
3360 }
3361 p
3362}
3363
3364#[allow(clippy::too_many_arguments)]
3370fn march_analytic_intersection(
3371 a: &AnalyticSurface<'_>,
3372 b: &AnalyticSurface<'_>,
3373 surf_a: &dyn Fn(f64, f64) -> Point3,
3374 norm_a: &dyn Fn(f64, f64) -> Vec3,
3375 surf_b: &dyn Fn(f64, f64) -> Point3,
3376 norm_b: &dyn Fn(f64, f64) -> Vec3,
3377 seed: Point3,
3378 u_range_a: (f64, f64),
3379 v_range_a: (f64, f64),
3380 u_range_b: (f64, f64),
3381 v_range_b: (f64, f64),
3382 initial_step: f64,
3383 u_periodic_a: bool,
3384 u_periodic_b: bool,
3385) -> Vec<Point3> {
3386 let max_steps = 500;
3387 let h_min = 1e-6;
3388 let h_max = initial_step * 4.0;
3389 let closure_dist = initial_step * 5.0;
3393 let max_angle = 10.0_f64.to_radians();
3395 let min_angle = 2.0_f64.to_radians();
3396
3397 let mut forward = Vec::new();
3399 let mut backward = Vec::new();
3401
3402 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3403 let mut current = seed;
3404 let mut h = initial_step;
3405 let mut prev_tangent: Option<Vec3> = None;
3406
3407 for _ in 0..max_steps {
3408 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3409 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3410
3411 let na = norm_a(ua, va);
3412 let nb = norm_b(ub, vb);
3413
3414 let tangent = na.cross(nb);
3415 let t_len = tangent.length();
3416 if t_len < 1e-10 {
3417 break;
3418 }
3419 let t_dir = tangent * (direction / t_len);
3420
3421 if let Some(prev_t) = prev_tangent {
3423 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3424 let angle = cos_angle.acos();
3425 if angle > max_angle && h > h_min {
3426 h = (h * 0.5).max(h_min);
3427 } else if angle < min_angle {
3428 h = (h * 2.0).min(h_max);
3429 }
3430 }
3431 prev_tangent = Some(t_dir);
3432
3433 let next = Point3::new(
3434 h.mul_add(t_dir.x(), current.x()),
3435 h.mul_add(t_dir.y(), current.y()),
3436 h.mul_add(t_dir.z(), current.z()),
3437 );
3438
3439 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3440 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3441
3442 let pa = surf_a(ua2, va2);
3443 let pb = surf_b(ub2, vb2);
3444 let mid = Point3::new(
3445 (pa.x() + pb.x()) * 0.5,
3446 (pa.y() + pb.y()) * 0.5,
3447 (pa.z() + pb.z()) * 0.5,
3448 );
3449 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3450 || va2 <= v_range_a.0
3451 || va2 >= v_range_a.1;
3452 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3453 || vb2 <= v_range_b.0
3454 || vb2 >= v_range_b.1;
3455
3456 if out_a || out_b {
3457 break;
3458 }
3459
3460 let dist_to_seed = (mid - seed).length();
3464 if points.len() > 10 && dist_to_seed < closure_dist {
3465 points.push(seed);
3466 break;
3467 }
3468
3469 points.push(mid);
3470 current = mid;
3471 }
3472 }
3473
3474 backward.reverse();
3476 let mut result = backward;
3477 result.push(seed);
3478 result.append(&mut forward);
3479
3480 for pt in &mut result {
3482 *pt = correct_to_intersection(
3483 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3484 5,
3485 );
3486 }
3487
3488 result
3489}
3490
3491fn project_analytic(
3495 surface: &AnalyticSurface<'_>,
3496 point: Point3,
3497 u_range: (f64, f64),
3498 v_range: (f64, f64),
3499) -> (f64, f64) {
3500 match surface {
3501 AnalyticSurface::Cylinder(cyl) => {
3502 let (u, v) = cyl.project_point(point);
3503 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3504 }
3505 AnalyticSurface::Sphere(sphere) => {
3506 let (u, v) = sphere.project_point(point);
3507 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3508 }
3509 AnalyticSurface::Cone(cone) => {
3510 let (u, v) = cone.project_point(point);
3511 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3512 }
3513 AnalyticSurface::Torus(torus) => {
3514 let (u, v) = torus.project_point(point);
3515 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3516 }
3517 }
3518}
3519
3520fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3524 matches!(
3525 surface,
3526 AnalyticSurface::Cylinder(_)
3527 | AnalyticSurface::Cone(_)
3528 | AnalyticSurface::Sphere(_)
3529 | AnalyticSurface::Torus(_)
3530 )
3531}
3532
3533#[allow(clippy::type_complexity)]
3535fn surface_closures<'a>(
3536 surface: &'a AnalyticSurface<'a>,
3537) -> (
3538 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3539 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3540 (f64, f64),
3541 (f64, f64),
3542) {
3543 match surface {
3544 AnalyticSurface::Cylinder(cyl) => (
3545 Box::new(|u, v| cyl.evaluate(u, v)),
3546 Box::new(|u, v| cyl.normal(u, v)),
3547 (0.0, TAU),
3548 (-1.0, 1.0),
3549 ),
3550 AnalyticSurface::Cone(cone) => (
3551 Box::new(|u, v| cone.evaluate(u, v)),
3552 Box::new(|u, v| cone.normal(u, v)),
3553 (0.0, TAU),
3554 (0.01, 2.0),
3555 ),
3556 AnalyticSurface::Sphere(sphere) => (
3557 Box::new(|u, v| sphere.evaluate(u, v)),
3558 Box::new(|u, v| sphere.normal(u, v)),
3559 (0.0, TAU),
3560 (-FRAC_PI_2, FRAC_PI_2),
3561 ),
3562 AnalyticSurface::Torus(torus) => (
3563 Box::new(|u, v| torus.evaluate(u, v)),
3564 Box::new(|u, v| torus.normal(u, v)),
3565 (0.0, TAU),
3566 (0.0, TAU),
3567 ),
3568 }
3569}
3570
3571#[cfg(test)]
3572#[allow(clippy::unwrap_used, clippy::expect_used)]
3573mod tests {
3574 use super::*;
3575 use crate::tolerance::Tolerance;
3576
3577 #[test]
3581 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3582 let half_angle = 1.1_f64;
3583 let cone = ConicalSurface::new(
3584 Point3::new(0.0, 0.0, 0.0),
3585 Vec3::new(0.0, 0.0, 1.0),
3586 half_angle,
3587 )
3588 .unwrap();
3589 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3590 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3591 let chains =
3592 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3593 .unwrap();
3594 let chain = chains
3595 .iter()
3596 .find_map(|c| match c {
3597 ExactIntersectionCurve::Points(chain) => Some(chain),
3598 _ => None,
3599 })
3600 .expect("a parabola or hyperbola section is sampled");
3601 let (from, to) = (chain[2], chain[chain.len() - 3]);
3602 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3603 .unwrap()
3604 .expect("an exact arc");
3605 let (t0, t1) = arc.domain();
3606 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3607 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3608 for i in 0..=200 {
3609 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3610 let w = q - Point3::new(0.0, 0.0, 0.0);
3611 let off_plane = (normal.dot(w) - d).abs();
3612 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3613 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3614 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3615 }
3616 }
3617 }
3618
3619 #[test]
3623 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3624 let half_angle = 1.1_f64;
3625 let cone = ConicalSurface::new(
3626 Point3::new(0.0, 0.0, 0.0),
3627 Vec3::new(0.0, 0.0, 1.0),
3628 half_angle,
3629 )
3630 .unwrap();
3631 for shortfall in [1e-10, 3e-10, 8e-10] {
3632 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3633 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3634 let chains =
3635 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3636 .unwrap();
3637 let Some(chain) = chains.iter().find_map(|c| match c {
3638 ExactIntersectionCurve::Points(chain) => Some(chain),
3639 _ => None,
3640 }) else {
3641 continue;
3642 };
3643 let (from, to) = (chain[2], chain[chain.len() - 3]);
3644 assert!(
3645 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3646 .unwrap()
3647 .is_none(),
3648 "coincident ends"
3649 );
3650 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3651 continue;
3652 };
3653 let (t0, t1) = arc.domain();
3654 for i in 0..=200 {
3655 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3656 - Point3::new(0.0, 0.0, 0.0);
3657 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3658 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3659 }
3660 }
3661 }
3662
3663 #[test]
3664 fn plane_cylinder_perpendicular() {
3665 let cyl =
3666 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3667 .unwrap();
3668
3669 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3671 assert!(!curves.is_empty(), "should find intersection curve");
3672 assert!(
3673 curves[0].points.len() > 10,
3674 "should have many sample points"
3675 );
3676
3677 let tol = Tolerance::loose();
3678 for pt in &curves[0].points {
3679 assert!(
3680 tol.approx_eq(pt.point.z(), 3.0),
3681 "z should be ~3.0, got {}",
3682 pt.point.z()
3683 );
3684 let r = pt.point.x().hypot(pt.point.y());
3685 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3686 }
3687 }
3688
3689 #[test]
3690 fn plane_sphere_equator() {
3691 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3692
3693 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3694 assert!(!curves.is_empty());
3695
3696 let tol = Tolerance::loose();
3697 for pt in &curves[0].points {
3698 assert!(
3699 tol.approx_eq(pt.point.z(), 0.0),
3700 "z should be ~0, got {}",
3701 pt.point.z()
3702 );
3703 let r = pt.point.x().hypot(pt.point.y());
3704 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3705 }
3706 }
3707
3708 #[test]
3709 fn plane_sphere_no_intersection() {
3710 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3711
3712 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3713 assert!(curves.is_empty());
3714 }
3715
3716 #[test]
3717 fn plane_cone_cross_section() {
3718 let cone = ConicalSurface::new(
3719 Point3::new(0.0, 0.0, 0.0),
3720 Vec3::new(0.0, 0.0, 1.0),
3721 std::f64::consts::FRAC_PI_4,
3722 )
3723 .unwrap();
3724
3725 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3726 assert!(!curves.is_empty(), "should find intersection with cone");
3727 }
3728
3729 #[test]
3736 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3737 let c1 = ConicalSurface::new(
3738 Point3::new(
3739 -16.999_999_999_999_975,
3740 -16.999_999_999_999_975,
3741 5.849_999_999_999_951,
3742 ),
3743 Vec3::new(0.0, 0.0, -1.0),
3744 0.785_398_163_397_433_5,
3745 )
3746 .unwrap();
3747 let c2 = ConicalSurface::new(
3748 Point3::new(
3749 -16.750_000_000_000_036,
3750 -16.750_000_000_000_018,
3751 0.749_999_999_999_881,
3752 ),
3753 Vec3::new(0.0, 0.0, 1.0),
3754 0.785_398_163_397_467_6,
3755 )
3756 .unwrap();
3757
3758 let curves = exact_cone_cone(&c1, &c2)
3759 .unwrap()
3760 .expect("offset parallel equal-angle cones must take the radical-plane path");
3761 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3762 assert!(
3763 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3764 "expected an ellipse section, got {:?}",
3765 curves[0]
3766 );
3767 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3768 return;
3769 };
3770
3771 for i in 0..16 {
3775 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3776 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3777 let rel = p - cone.apex();
3778 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3779 let axial = rel_v.dot(cone.axis());
3780 let radial = (rel_v - cone.axis() * axial).length();
3781 assert!(
3782 axial > 0.0,
3783 "{label}: sample on phantom nappe (axial {axial})"
3784 );
3785 let expect = cone.half_angle().tan() * axial;
3786 assert!(
3787 (radial - expect).abs() < 1e-9,
3788 "{label}: sample off surface by {}",
3789 (radial - expect).abs()
3790 );
3791 }
3792 }
3793 }
3794
3795 #[test]
3799 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3800 let c1 = ConicalSurface::new(
3801 Point3::new(0.0, 0.0, 5.0),
3802 Vec3::new(0.0, 0.0, -1.0),
3803 std::f64::consts::FRAC_PI_4,
3804 )
3805 .unwrap();
3806 let c2 = ConicalSurface::new(
3807 Point3::new(0.25, 0.25, 20.0),
3808 Vec3::new(0.0, 0.0, 1.0),
3809 std::f64::consts::FRAC_PI_4,
3810 )
3811 .unwrap();
3812 let curves = exact_cone_cone(&c1, &c2)
3813 .unwrap()
3814 .expect("radical-plane path");
3815 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3816 }
3817
3818 #[test]
3821 fn offset_parallel_cones_with_unequal_angles_defer() {
3822 let c1 = ConicalSurface::new(
3823 Point3::new(0.0, 0.0, 5.0),
3824 Vec3::new(0.0, 0.0, -1.0),
3825 std::f64::consts::FRAC_PI_4,
3826 )
3827 .unwrap();
3828 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
3829 .unwrap();
3830 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
3831 }
3832
3833 #[test]
3834 fn coaxial_cones_cross_at_single_circle() {
3835 let outer = ConicalSurface::new(
3840 Point3::new(0.0, 0.0, 50.0),
3841 Vec3::new(0.0, 0.0, -1.0),
3842 5.0_f64.atan(),
3843 )
3844 .unwrap();
3845 let inner = ConicalSurface::new(
3846 Point3::new(0.0, 0.0, 90.0),
3847 Vec3::new(0.0, 0.0, -1.0),
3848 10.0_f64.atan(),
3849 )
3850 .unwrap();
3851
3852 let curves = intersect_analytic_analytic_bounded(
3853 AnalyticSurface::Cone(&outer),
3854 AnalyticSurface::Cone(&inner),
3855 32,
3856 None,
3857 None,
3858 )
3859 .unwrap();
3860
3861 assert_eq!(
3862 curves.len(),
3863 1,
3864 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
3865 curves.len()
3866 );
3867 for p in &curves[0].points {
3868 let r = p.point.x().hypot(p.point.y());
3869 assert!(
3870 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
3871 "intersection point off the expected z=10,r=8 circle: {:?}",
3872 p.point
3873 );
3874 }
3875 }
3876
3877 #[test]
3878 fn plane_torus_cross_section() {
3879 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
3880
3881 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3882 assert!(
3883 !curves.is_empty(),
3884 "should find intersection curves with torus"
3885 );
3886 }
3887
3888 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
3891 let rho = p.x().hypot(p.y());
3892 ((rho - major).hypot(p.z())) - minor
3893 }
3894
3895 #[test]
3901 fn oblique_cone_cylinder_traces_curves_on_both() {
3902 use crate::traits::ParametricCurve;
3903 let cone = ConicalSurface::new(
3907 Point3::new(0.0, 0.0, 3.0),
3908 Vec3::new(0.0, 0.0, -1.0),
3909 2.0_f64.atan(),
3910 )
3911 .unwrap();
3912 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
3913 let cyl =
3914 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
3915 .unwrap();
3916 for cone_first in [true, false] {
3917 let (a, b) = if cone_first {
3918 (
3919 AnalyticSurface::Cone(&cone),
3920 AnalyticSurface::Cylinder(&cyl),
3921 )
3922 } else {
3923 (
3924 AnalyticSurface::Cylinder(&cyl),
3925 AnalyticSurface::Cone(&cone),
3926 )
3927 };
3928 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
3929 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
3930 for c in &curves {
3931 let (t0, t1) = c.curve.domain();
3932 for k in 0..=64 {
3933 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
3934 let p = ParametricCurve::evaluate(&c.curve, t);
3935 let rod = (p.x() - x0).hypot(p.z() - 1.0);
3938 assert!(
3939 (rod - 0.6).abs() < 1e-4,
3940 "x0 {x0}: off the rod by {}",
3941 rod - 0.6
3942 );
3943 let cone_r = p.x().hypot(p.y());
3944 assert!(
3945 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
3946 "x0 {x0}: off the cone at {p:?}"
3947 );
3948 }
3949 }
3950 }
3951 }
3952 }
3953
3954 #[test]
3955 fn a_rod_through_a_rings_tube_traces_four_loops() {
3956 use crate::traits::ParametricCurve;
3957 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
3958 let rod =
3961 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
3962 .unwrap();
3963 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
3964 assert_eq!(curves.len(), 4);
3965 for c in &curves {
3966 let (t0, t1) = c.curve.domain();
3967 for k in 0..=64 {
3968 let p =
3969 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
3970 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
3971 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
3972 assert!(
3973 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
3974 "off by {on_rod}, {on_ring}"
3975 );
3976 }
3977 }
3978 let high =
3980 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
3981 .unwrap();
3982 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
3983 let grazing =
3985 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
3986 .unwrap();
3987 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
3988 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
3990 let thin =
3991 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
3992 .unwrap();
3993 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
3994 }
3995
3996 #[test]
3997 fn a_pin_through_a_ball_traces_two_loops() {
3998 use crate::traits::ParametricCurve;
3999 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4000 let half = 0.08_f64.atan();
4003 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4004 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4005 for cone_first in [true, false] {
4006 let (a, b) = if cone_first {
4007 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4008 } else {
4009 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4010 };
4011 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4012 assert_eq!(curves.len(), 2, "entry and exit loops");
4013 for c in &curves {
4014 let (t0, t1) = c.curve.domain();
4015 for k in 0..=64 {
4016 let p = ParametricCurve::evaluate(
4017 &c.curve,
4018 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4019 );
4020 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4021 let axial = apex.z() - p.z();
4022 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4023 assert!(
4024 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4025 "off by {on_ball}, {on_pin}"
4026 );
4027 }
4028 }
4029 }
4030 let coaxial =
4035 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4036 .unwrap();
4037 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4038 let aside = ConicalSurface::new(
4039 Point3::new(2.8, 0.0, 10.0),
4040 Vec3::new(0.0, 0.0, -1.0),
4041 FRAC_PI_2 - half,
4042 )
4043 .unwrap();
4044 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4045 let holding = ConicalSurface::new(
4046 Point3::new(1.0, 0.5, 1.0),
4047 Vec3::new(0.0, 0.0, -1.0),
4048 FRAC_PI_2 - half,
4049 )
4050 .unwrap();
4051 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4052 let away = ConicalSurface::new(
4053 Point3::new(1.0, 0.5, 10.0),
4054 Vec3::new(0.0, 0.0, 1.0),
4055 FRAC_PI_2 - half,
4056 )
4057 .unwrap();
4058 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4059 let step = TAU / 2048.0;
4063 let grazed =
4064 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4065 .unwrap();
4066 let wide =
4067 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4068 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4069 }
4070
4071 #[test]
4072 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4073 use crate::traits::ParametricCurve;
4074 let cone = ConicalSurface::new(
4076 Point3::new(0.0, 0.0, 3.0),
4077 Vec3::new(0.0, 0.0, -1.0),
4078 2.0_f64.atan(),
4079 )
4080 .unwrap();
4081 for (centre, radius) in [
4082 (Point3::new(1.0, 0.8, 1.2), 1.1),
4083 (Point3::new(1.5, 0.0, 0.0), 0.8),
4084 ] {
4085 let ball = SphericalSurface::new(centre, radius).unwrap();
4086 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4087 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4088 let (t0, t1) = curves[0].curve.domain();
4089 for k in 0..=64 {
4090 let p = ParametricCurve::evaluate(
4091 &curves[0].curve,
4092 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4093 );
4094 let on_ball = (p - centre).length() - radius;
4095 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4096 assert!(
4097 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4098 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4099 );
4100 }
4101 }
4102 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4104 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4105 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4106 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4107 }
4108
4109 #[test]
4110 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4111 use crate::traits::ParametricCurve;
4112 let cone = ConicalSurface::new(
4113 Point3::new(0.0, 0.0, 3.0),
4114 Vec3::new(0.0, 0.0, -1.0),
4115 2.0_f64.atan(),
4116 )
4117 .unwrap();
4118 for (centre, radius) in [
4121 (Point3::new(0.5, 0.0, 2.5), 2.0),
4122 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4123 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4124 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4125 ] {
4126 let ball = SphericalSurface::new(centre, radius).unwrap();
4127 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4128 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4129 let (t0, t1) = curves[0].curve.domain();
4130 for k in 0..=4096 {
4131 let p = ParametricCurve::evaluate(
4132 &curves[0].curve,
4133 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4134 );
4135 let on_ball = (p - centre).length() - radius;
4136 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4137 assert!(
4138 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4139 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4140 );
4141 }
4142 }
4143 }
4144
4145 #[test]
4146 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4147 let t = 2.0_f64.atan();
4148 let cone =
4149 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4150 let through_apex =
4152 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4153 .unwrap();
4154 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4155 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4157 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4158 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4159 let pin =
4162 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4163 let tube =
4164 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4165 .unwrap();
4166 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4167 }
4168
4169 #[test]
4170 fn parallel_cone_cylinder_gives_two_exact_branches() {
4171 use crate::traits::ParametricCurve;
4172 let cone = ConicalSurface::new(
4173 Point3::new(-5.45, -36.55, -4.85),
4174 Vec3::new(0.0, 0.0, 1.0),
4175 std::f64::consts::FRAC_PI_4,
4176 )
4177 .unwrap();
4178 let cyl = CylindricalSurface::new(
4179 Point3::new(-8.0, -34.0, -5.0),
4180 Vec3::new(0.0, 0.0, 1.0),
4181 4.45,
4182 )
4183 .unwrap();
4184 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4186 let curves = intersect_analytic_analytic_bounded(
4187 AnalyticSurface::Cone(&cone),
4188 AnalyticSurface::Cylinder(&cyl),
4189 32,
4190 Some(v_hint),
4191 Some((0.0, 2.5)),
4192 )
4193 .unwrap();
4194
4195 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4196 for c in &curves {
4197 let (t0, t1) = c.curve.domain();
4198 for k in 0..=32 {
4199 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4200 let p = ParametricCurve::evaluate(&c.curve, t);
4201 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4203 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4204 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4206 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4207 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4209 }
4210 }
4211 }
4212
4213 #[test]
4214 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4215 use crate::traits::ParametricCurve;
4216 let cone = ConicalSurface::new(
4218 Point3::new(0.0, 0.0, 3.0),
4219 Vec3::new(0.0, 0.0, -1.0),
4220 2.0_f64.atan(),
4221 )
4222 .unwrap();
4223 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4225 let rod =
4226 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4227 .unwrap();
4228 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4229 .unwrap()
4230 .unwrap();
4231 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4232 let (t0, t1) = curves[0].curve.domain();
4233 let (first, last) = (
4234 ParametricCurve::evaluate(&curves[0].curve, t0),
4235 ParametricCurve::evaluate(&curves[0].curve, t1),
4236 );
4237 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4238 for k in 0..=64 {
4239 let p = ParametricCurve::evaluate(
4240 &curves[0].curve,
4241 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4242 );
4243 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4244 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4245 assert!(
4246 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4247 "({x}, {y}): off by {on_rod}, {on_cone}"
4248 );
4249 }
4250 }
4251 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4254 let rod =
4255 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4256 .unwrap();
4257 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4258 .unwrap()
4259 .unwrap();
4260 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4261 }
4262 }
4263
4264 #[test]
4267 fn coaxial_cone_cylinder_defers_to_other_paths() {
4268 let cone = ConicalSurface::new(
4269 Point3::new(0.0, 0.0, 0.0),
4270 Vec3::new(0.0, 0.0, 1.0),
4271 std::f64::consts::FRAC_PI_4,
4272 )
4273 .unwrap();
4274 let cyl =
4275 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4276 .unwrap();
4277 assert!(
4278 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4279 .unwrap()
4280 .is_none()
4281 );
4282 }
4283
4284 #[test]
4285 fn oblique_cone_cylinder_defers_to_other_paths() {
4286 let cone = ConicalSurface::new(
4287 Point3::new(0.0, 0.0, 0.0),
4288 Vec3::new(0.0, 0.0, 1.0),
4289 std::f64::consts::FRAC_PI_4,
4290 )
4291 .unwrap();
4292 let cyl =
4293 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4294 .unwrap();
4295 assert!(
4296 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4297 .unwrap()
4298 .is_none()
4299 );
4300 }
4301
4302 #[test]
4303 fn plane_torus_lobe_closes_and_stays_on_surface() {
4304 use crate::traits::ParametricCurve;
4305 let (major, minor) = (10.0, 3.0);
4306 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4307
4308 for (n, d) in [
4312 (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), ] {
4316 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4317 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4318 for c in &curves {
4319 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4320 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4321 assert!(
4322 (p0 - p1).length() < 1e-7,
4323 "lobe not closed: gap={} (n={n:?} d={d})",
4324 (p0 - p1).length()
4325 );
4326 for k in 0..=64 {
4328 let t = f64::from(k) / 64.0;
4329 let p = ParametricCurve::evaluate(&c.curve, t);
4330 assert!(
4331 torus_implicit(p, major, minor).abs() < 1e-2,
4332 "off-surface point {p:?} implicit={}",
4333 torus_implicit(p, major, minor)
4334 );
4335 }
4336 }
4337 }
4338 }
4339
4340 #[test]
4341 fn plane_torus_inner_tangent_figure_eight_stays_open() {
4342 use crate::traits::ParametricCurve;
4343 let (major, minor) = (10.0, 3.0);
4344 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4345
4346 let curves =
4351 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4352 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4353 let max_gap = curves
4354 .iter()
4355 .map(|c| {
4356 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4357 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4358 (p0 - p1).length()
4359 })
4360 .fold(0.0_f64, f64::max);
4361 assert!(
4362 max_gap > 1e-2,
4363 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4364 );
4365 }
4366
4367 #[test]
4372 fn plane_torus_wall_sections_close_into_their_loops() {
4373 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4374 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4375 for k in 1..200 {
4376 let (d, want) = match k.cmp(&100) {
4377 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4379 std::cmp::Ordering::Greater => (
4381 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4382 1,
4383 ),
4384 std::cmp::Ordering::Equal => continue,
4385 };
4386 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4387 let closed = loops
4388 .iter()
4389 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4390 .count();
4391 assert_eq!(
4392 (loops.len(), closed),
4393 (want, want),
4394 "R {major} r {minor}, wall at {d}"
4395 );
4396 }
4397 }
4398 }
4399
4400 #[test]
4404 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4405 let (major, minor) = (4.0, 1.5);
4406 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4407 for tilt in [0.03_f64, 0.08, 0.2] {
4408 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4409 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4410 assert_eq!(curves.len(), 2, "tilt {tilt}");
4411 for c in &curves {
4412 let (t0, t1) = c.curve.domain();
4413 let off = (0..=400)
4414 .map(|k| {
4415 let p = c
4416 .curve
4417 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4418 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4419 })
4420 .fold(0.0_f64, |m, e| m.max(e.abs()));
4421 assert!(
4422 off < 1e-6,
4423 "tilt {tilt}: fitted section {off} off the torus"
4424 );
4425 }
4426 }
4427 }
4428
4429 #[test]
4430 fn line_torus_box_edge_crossing_is_exact() {
4431 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4434 let ts = intersect_line_torus(
4435 &torus,
4436 Point3::new(6.0, -4.0, -5.0),
4437 Vec3::new(0.0, 0.0, 1.0),
4438 );
4439 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4441 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4442 let rho = 6.0_f64.hypot(4.0);
4443 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4444 assert!(
4445 (zs[0] - (-z_exp)).abs() < 1e-9,
4446 "z0={} exp={}",
4447 zs[0],
4448 -z_exp
4449 );
4450 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4451 for &t in &ts {
4453 let p = Point3::new(6.0, -4.0, -5.0 + t);
4454 let rho = p.x().hypot(p.y());
4455 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4456 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4457 }
4458 }
4459
4460 #[test]
4461 fn line_torus_miss_and_tangent() {
4462 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4463 let miss = intersect_line_torus(
4465 &torus,
4466 Point3::new(20.0, 0.0, 0.0),
4467 Vec3::new(0.0, 0.0, 1.0),
4468 );
4469 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4470 let axis =
4472 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4473 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4474 }
4475
4476 #[test]
4477 fn dispatch_via_analytic_surface() {
4478 let cyl =
4479 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4480 .unwrap();
4481 let curves = intersect_plane_analytic(
4482 AnalyticSurface::Cylinder(&cyl),
4483 Vec3::new(0.0, 0.0, 1.0),
4484 0.0,
4485 )
4486 .unwrap();
4487 assert!(!curves.is_empty());
4488 }
4489
4490 #[test]
4491 fn perpendicular_cylinders_intersect() {
4492 let cyl_z =
4493 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4494 .unwrap();
4495 let cyl_x =
4496 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4497 .unwrap();
4498
4499 let curves = intersect_analytic_analytic(
4500 AnalyticSurface::Cylinder(&cyl_z),
4501 AnalyticSurface::Cylinder(&cyl_x),
4502 16,
4503 )
4504 .unwrap();
4505
4506 assert!(
4507 !curves.is_empty(),
4508 "perpendicular cylinders should intersect"
4509 );
4510
4511 for c in &curves {
4512 assert!(
4513 c.points.len() >= 2,
4514 "intersection curve should have >= 2 points, got {}",
4515 c.points.len()
4516 );
4517 }
4518 }
4519
4520 #[test]
4523 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4524 let cyl_z =
4525 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4526 .unwrap();
4527 let cyl_x =
4528 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4529 .unwrap();
4530 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4531 .unwrap()
4532 .unwrap();
4533 assert_eq!(curves.len(), 1);
4534 let curve = &curves[0].curve;
4535 let (t0, t1) = curve.domain();
4536 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4537 let off = |p: Point3| {
4538 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4539 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4540 on_z.max(on_x)
4541 };
4542 let worst = (0..=400)
4543 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4544 .fold(0.0, f64::max);
4545 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4546 }
4547
4548 #[test]
4552 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4553 let cyl_z =
4554 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4555 .unwrap();
4556 let cyl_x =
4557 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4558 .unwrap();
4559 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4560 .unwrap()
4561 .expect("the thin cylinder's sweep finds the loop");
4562 assert_eq!(curves.len(), 1);
4563 }
4564
4565 #[test]
4566 fn sphere_cylinder_intersect() {
4567 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4568 let cyl =
4569 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4570 .unwrap();
4571
4572 let curves = intersect_analytic_analytic(
4573 AnalyticSurface::Sphere(&sphere),
4574 AnalyticSurface::Cylinder(&cyl),
4575 16,
4576 )
4577 .unwrap();
4578
4579 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4583 }
4584
4585 #[test]
4586 fn exact_sphere_cylinder_coaxial_two_circles() {
4587 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4590 let cyl =
4591 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4592 .unwrap();
4593 let circles = exact_sphere_cylinder(&sphere, &cyl)
4594 .unwrap()
4595 .expect("coaxial case returns Some");
4596 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4597 let mut zs: Vec<f64> = circles
4598 .iter()
4599 .filter_map(|c| match c {
4600 ExactIntersectionCurve::Circle(circle) => {
4601 assert!(
4602 (circle.radius() - 3.0).abs() < 1e-9,
4603 "rim radius == cyl radius"
4604 );
4605 Some(circle.center().z())
4606 }
4607 _ => None,
4608 })
4609 .collect();
4610 assert_eq!(zs.len(), 2, "both sections must be exact circles");
4611 zs.sort_by(f64::total_cmp);
4612 let z = 27.0_f64.sqrt();
4613 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4614 }
4615
4616 #[test]
4617 fn exact_sphere_cylinder_non_coaxial_defers() {
4618 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4620 let cyl =
4621 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4622 .unwrap();
4623 assert!(
4624 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4625 "non-coaxial sphere/cylinder defers to the marcher"
4626 );
4627 }
4628
4629 #[test]
4630 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4631 let cone = ConicalSurface::new(
4633 Point3::new(0.0, 0.0, 3.0),
4634 Vec3::new(0.0, 0.0, -1.0),
4635 2.0_f64.atan(),
4636 )
4637 .unwrap();
4638 for (height, radius, count) in [
4639 (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), ] {
4645 let centre = Point3::new(0.0, 0.0, height);
4646 let ball = SphericalSurface::new(centre, radius).unwrap();
4647 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4648 let circles = circles_of(&curves);
4649 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4650 for circle in circles {
4651 for k in 0..16 {
4652 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4653 let on_ball = (p - centre).length() - radius;
4654 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4655 assert!(
4656 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4657 "ball at {height}: off by {on_ball}, {on_cone}"
4658 );
4659 }
4660 }
4661 }
4662 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4663 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4664 let wide =
4667 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4668 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4669 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4670 let circles = circles_of(&curves);
4671 assert_eq!(circles.len(), 1, "the touch");
4672 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4673 assert!(
4674 (circles[0].radius() - touch).abs() < 1e-3,
4675 "{}",
4676 circles[0].radius()
4677 );
4678 }
4679
4680 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
4682 curves
4683 .iter()
4684 .filter_map(|c| match c {
4685 ExactIntersectionCurve::Circle(circle) => Some(circle),
4686 _ => None,
4687 })
4688 .collect()
4689 }
4690
4691 fn worst_off(
4694 circles: &[&Circle3D],
4695 torus: &ToroidalSurface,
4696 other: impl Fn(Point3) -> f64,
4697 ) -> f64 {
4698 let mut worst = 0.0_f64;
4699 for circle in circles {
4700 for k in 0..16 {
4701 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4702 let q = p - torus.center();
4703 let along = q.dot(torus.z_axis());
4704 let rho = (q - torus.z_axis() * along).length();
4705 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
4706 worst = worst.max(off).max(other(p).abs());
4707 }
4708 }
4709 worst
4710 }
4711
4712 #[test]
4713 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
4714 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4715 for height in [0.0, 1.0] {
4716 let centre = Point3::new(0.0, 0.0, height);
4717 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
4718 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
4719 let circles = circles_of(&curves);
4720 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
4721 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
4722 assert!(worst < 1e-9, "height {height}: {worst}");
4723 }
4724 }
4725
4726 #[test]
4727 fn exact_sphere_torus_misses_touches_and_defers() {
4728 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4729 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
4730 assert!(
4731 exact_sphere_torus(&ball(0.0, 1.0), &torus)
4732 .unwrap()
4733 .unwrap()
4734 .is_empty(),
4735 "a small ball in the hole misses"
4736 );
4737 assert!(
4738 exact_sphere_torus(&ball(0.0, 2.5), &torus)
4739 .unwrap()
4740 .is_none(),
4741 "a ball touching the inner equator defers"
4742 );
4743 assert!(
4744 exact_sphere_torus(&ball(1.0, 3.0), &torus)
4745 .unwrap()
4746 .is_none(),
4747 "a ball off the axis defers"
4748 );
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 Vec3::new(0.0, 0.0, 1.0),
4754 Vec3::new(1.0, 0.0, 0.0),
4755 )
4756 .unwrap();
4757 assert!(
4758 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
4759 .unwrap()
4760 .is_none()
4761 );
4762 }
4763
4764 #[test]
4765 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
4766 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4767 let z = Vec3::new(0.0, 0.0, 1.0);
4768 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
4769 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
4770 let circles = circles_of(&curves);
4771 assert_eq!((curves.len(), circles.len()), (2, 2));
4772 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
4773 assert!(worst < 1e-9, "{worst}");
4774 assert!(
4775 exact_cylinder_torus(&rod(2.0), &torus)
4776 .unwrap()
4777 .unwrap()
4778 .is_empty(),
4779 "a rod clear in the hole misses"
4780 );
4781 assert!(
4782 exact_cylinder_torus(&rod(5.5), &torus).unwrap().is_none(),
4783 "a wall touching the outer equator defers"
4784 );
4785 let tilted =
4786 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
4787 .unwrap();
4788 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
4789 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
4790 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
4791 let spindle = ToroidalSurface::with_axis_and_ref_dir(
4792 Point3::new(0.0, 0.0, 0.0),
4793 1.0,
4794 2.0,
4795 z,
4796 Vec3::new(1.0, 0.0, 0.0),
4797 )
4798 .unwrap();
4799 assert!(
4800 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
4801 "a spindle torus's inner lemon also meets the rod"
4802 );
4803 }
4804
4805 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
4808 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4809 let cyl =
4810 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
4811 .unwrap();
4812 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
4813 .unwrap()
4814 .unwrap();
4815 let mut worst: f64 = 0.0;
4816 for c in &curves {
4817 for ip in &c.points {
4818 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
4819 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
4820 worst = worst
4821 .max((on_sphere - ip.point).length())
4822 .max((on_cylinder - ip.point).length());
4823 }
4824 let (t0, t1) = c.curve.domain();
4825 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
4826 for k in 0..=400 {
4827 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
4828 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
4829 let on_cylinder = ((p.x() - cylinder_origin.x())
4830 .hypot(p.y() - cylinder_origin.y())
4831 - cylinder_radius)
4832 .abs();
4833 worst = worst.max(on_sphere).max(on_cylinder);
4834 }
4835 }
4836 (curves.len(), worst)
4837 }
4838
4839 #[test]
4842 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
4843 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
4844 assert_eq!(count, 2);
4845 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
4846 }
4847
4848 #[test]
4850 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
4851 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
4852 assert_eq!(count, 1);
4853 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
4854 }
4855
4856 #[test]
4857 fn disjoint_cylinders_no_intersection() {
4858 let cyl_a =
4859 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4860 .unwrap();
4861 let cyl_b =
4862 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4863 .unwrap();
4864
4865 let curves = intersect_analytic_analytic(
4866 AnalyticSurface::Cylinder(&cyl_a),
4867 AnalyticSurface::Cylinder(&cyl_b),
4868 16,
4869 )
4870 .unwrap();
4871
4872 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
4873 }
4874
4875 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
4879 use crate::traits::ParametricCurve;
4880 match curve {
4881 ExactIntersectionCurve::Circle(c) => (0..=64)
4882 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
4883 .collect(),
4884 ExactIntersectionCurve::Ellipse(e) => (0..=64)
4885 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
4886 .collect(),
4887 ExactIntersectionCurve::Points(pts) => pts.clone(),
4888 }
4889 }
4890
4891 fn assert_on_plane_and_cone(
4894 curves: &[ExactIntersectionCurve],
4895 cone: &ConicalSurface,
4896 n: Vec3,
4897 d: f64,
4898 z_bound: (f64, f64),
4899 ) {
4900 assert!(!curves.is_empty(), "expected at least one section curve");
4901 let mut total = 0;
4902 for curve in curves {
4903 for p in collect_points(curve) {
4904 total += 1;
4905 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
4906 assert!(
4907 plane_err < 1e-9,
4908 "point off plane by {plane_err:.2e}: {p:?}"
4909 );
4910 let (u, v) = cone.project_point(p);
4911 let q = cone.evaluate(u, v);
4912 let cone_err =
4913 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
4914 .sqrt();
4915 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
4916 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
4917 assert!(
4918 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
4919 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
4920 p.z()
4921 );
4922 }
4923 }
4924 assert!(total >= 8, "too few section points ({total})");
4925 }
4926
4927 #[test]
4928 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
4929 let cone = ConicalSurface::new(
4933 Point3::new(0.0, 0.0, 0.0),
4934 Vec3::new(0.0, 0.0, 1.0),
4935 std::f64::consts::FRAC_PI_4,
4936 )
4937 .unwrap();
4938 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
4939 let d = n.z() * 5.0;
4941 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4942 assert!(
4943 curves
4944 .iter()
4945 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
4946 "oblique steep plane × cone must yield an exact Ellipse"
4947 );
4948 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
4950 }
4951
4952 #[test]
4953 fn oblique_plane_cone_wrong_nappe_is_empty() {
4954 let cone = ConicalSurface::new(
4958 Point3::new(0.0, 0.0, 0.0),
4959 Vec3::new(0.0, 0.0, 1.0),
4960 std::f64::consts::FRAC_PI_4,
4961 )
4962 .unwrap();
4963 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
4964 let d = n.z() * -5.0;
4965 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4966 assert!(
4967 curves.is_empty(),
4968 "plane on the phantom-nappe side must yield no real curve, got {}",
4969 curves.len()
4970 );
4971 }
4972
4973 #[test]
4974 fn oblique_plane_cone_parabola_on_both_single_branch() {
4975 let cone = ConicalSurface::new(
4978 Point3::new(0.0, 0.0, 0.0),
4979 Vec3::new(0.0, 0.0, 1.0),
4980 std::f64::consts::FRAC_PI_4,
4981 )
4982 .unwrap();
4983 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
4984 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
4986 assert_eq!(
4987 curves.len(),
4988 1,
4989 "a parabola is a single branch, got {}",
4990 curves.len()
4991 );
4992 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
4994 }
4995
4996 #[test]
4997 fn oblique_plane_cone_hyperbola_real_nappe_only() {
4998 let cone = ConicalSurface::new(
5006 Point3::new(-59.0, -59.0, 15.85),
5007 Vec3::new(0.0, 0.0, -1.0),
5008 std::f64::consts::FRAC_PI_4,
5009 )
5010 .unwrap();
5011 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5012 let d = -58.360_56;
5013 let cos_theta = n.dot(cone.axis()).abs();
5014 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5015 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5016 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5019 for c in &curves {
5021 assert!(
5022 matches!(c, ExactIntersectionCurve::Points(_)),
5023 "hyperbola must be sampled Points, not a closed conic"
5024 );
5025 }
5026 }
5027}