1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::aabb::Aabb3;
11use crate::curves::{Circle3D, Ellipse3D};
12use crate::frame::Frame3;
13use crate::nurbs::curve::NurbsCurve;
14use crate::nurbs::fitting::interpolate;
15use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
16use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
17use crate::tolerance::Tolerance;
18use crate::vec::{Point3, Vec3};
19
20#[derive(Debug, Clone)]
22pub enum ExactIntersectionCurve {
23 Circle(Circle3D),
25 Ellipse(Ellipse3D),
27 Points(Vec<Point3>),
29}
30
31pub fn exact_plane_analytic(
42 surface: AnalyticSurface<'_>,
43 plane_normal: Vec3,
44 plane_d: f64,
45) -> Result<Vec<ExactIntersectionCurve>, MathError> {
46 exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
47}
48
49pub fn exact_plane_analytic_reaching(
57 surface: AnalyticSurface<'_>,
58 plane_normal: Vec3,
59 plane_d: f64,
60 reach: f64,
61) -> Result<Vec<ExactIntersectionCurve>, MathError> {
62 match surface {
63 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
64 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
65 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
66 AnalyticSurface::Torus(torus) => {
67 if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
68 return Ok(circles);
69 }
70 if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
71 return Ok(loops
72 .into_iter()
73 .map(ExactIntersectionCurve::Points)
74 .collect());
75 }
76 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
78 Ok(chains
79 .into_iter()
80 .map(ExactIntersectionCurve::Points)
81 .collect())
82 }
83 }
84}
85
86fn exact_plane_torus(
100 torus: &ToroidalSurface,
101 normal: Vec3,
102 d: f64,
103) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
104 let len = normal.length();
105 let n = normal.normalize()?;
106 let d = d / len;
107 let axis = torus.z_axis();
108 let center = torus.center();
109 let (big, small) = (torus.major_radius(), torus.minor_radius());
110 let height = d - dot_np(n, center);
111 let along = n.dot(axis);
112 if along.abs() > 1.0 - 1e-10 {
113 if height.abs() >= small - 1e-10 * small {
114 if height.abs() > small + 1e-10 * small {
115 return Ok(Some(Vec::new()));
116 }
117 if big <= 1e-10 * small
124 || axis.cross(n).length() > 1e-12
125 || (small - height.abs()).abs() > crate::tolerance::Tolerance::default().linear
126 {
127 return Ok(None);
128 }
129 let middle = center + n * height;
130 return Ok(Some(vec![ExactIntersectionCurve::Circle(Circle3D::new(
131 middle, n, big,
132 )?)]));
133 }
134 let reach = small.mul_add(small, -(height * height)).sqrt();
135 if big - reach <= 1e-10 * big {
136 return Ok(None);
137 }
138 let middle = center + n * height;
139 return Ok(Some(vec![
140 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
141 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
142 ]));
143 }
144 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
145 let out = axis.cross(n).normalize()?;
146 return Ok(Some(vec![
147 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
148 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
149 ]));
150 }
151 Ok(None)
152}
153
154fn exact_plane_cylinder(
160 cyl: &CylindricalSurface,
161 normal: Vec3,
162 d: f64,
163) -> Result<Vec<ExactIntersectionCurve>, MathError> {
164 let axis = cyl.axis();
165 let cos_theta = normal.dot(axis).abs();
166 let r = cyl.radius();
167
168 if cos_theta < 1e-10 {
169 let chains = sample_plane_cylinder(cyl, normal, d)?;
172 return Ok(chains
173 .into_iter()
174 .map(ExactIntersectionCurve::Points)
175 .collect());
176 }
177
178 let n_dot_axis = normal.dot(axis);
181 let n_dot_origin = dot_np(normal, cyl.origin());
182 let t = (d - n_dot_origin) / n_dot_axis;
183 let center_on_axis = Point3::new(
184 cyl.origin().x() + t * axis.x(),
185 cyl.origin().y() + t * axis.y(),
186 cyl.origin().z() + t * axis.z(),
187 );
188
189 if cos_theta > 1.0 - 1e-10 {
190 let circle = Circle3D::new(center_on_axis, normal, r)?;
192 Ok(vec![ExactIntersectionCurve::Circle(circle)])
193 } else {
194 let semi_minor = r;
198 let semi_major = r / cos_theta;
199
200 let axis_proj = Vec3::new(
204 axis.x() - n_dot_axis * normal.x(),
205 axis.y() - n_dot_axis * normal.y(),
206 axis.z() - n_dot_axis * normal.z(),
207 );
208 let u_axis = axis_proj.normalize()?;
209 let v_axis = normal.cross(u_axis);
210
211 let ellipse = Ellipse3D::with_axes(
212 center_on_axis,
213 normal,
214 semi_major,
215 semi_minor,
216 u_axis,
217 v_axis,
218 )?;
219 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
220 }
221}
222
223fn exact_plane_sphere(
227 sphere: &SphericalSurface,
228 normal: Vec3,
229 d: f64,
230) -> Result<Vec<ExactIntersectionCurve>, MathError> {
231 let h = dot_np(normal, sphere.center()) - d;
232 let r = sphere.radius();
233
234 if h.abs() > r - 1e-10 {
235 return Ok(vec![]);
236 }
237
238 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
239 let circle_center = Point3::new(
240 h.mul_add(-normal.x(), sphere.center().x()),
241 h.mul_add(-normal.y(), sphere.center().y()),
242 h.mul_add(-normal.z(), sphere.center().z()),
243 );
244
245 let circle = Circle3D::new(circle_center, normal, circle_r)?;
246 Ok(vec![ExactIntersectionCurve::Circle(circle)])
247}
248
249pub fn exact_sphere_sphere(
261 s1: &SphericalSurface,
262 s2: &SphericalSurface,
263) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
264 let delta = s2.center() - s1.center();
265 let d = delta.length();
266 let (r1, r2) = (s1.radius(), s2.radius());
267 let scale = r1.max(r2);
268 if d <= 1e-12 * scale {
269 return Ok(((r1 - r2).abs() > 1e-12 * scale).then(Vec::new));
271 }
272 let normal = delta * (1.0 / d);
273 let (centre, toward, r_small, r_big) = if r1 <= r2 {
276 (s1.center(), normal, r1, r2)
277 } else {
278 (s2.center(), -normal, r2, r1)
279 };
280 let along = d.mul_add(d, r_small.mul_add(r_small, -(r_big * r_big))) / (2.0 * d);
281 if along.abs() > r_small - 1e-10 * scale.max(1.0) {
282 return Ok(Some(Vec::new()));
283 }
284 let radius = r_small.mul_add(r_small, -(along * along)).sqrt();
285 let circle = Circle3D::new(centre + toward * along, normal, radius)?;
286 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
287}
288
289fn exact_plane_cone(
298 cone: &ConicalSurface,
299 normal: Vec3,
300 d: f64,
301 reach: f64,
302) -> Result<Vec<ExactIntersectionCurve>, MathError> {
303 let axis = cone.axis();
304 let cos_theta = normal.dot(axis).abs();
305 let half_angle = cone.half_angle();
306
307 if cos_theta > 1.0 - 1e-10 {
308 let n_dot_axis = normal.dot(axis);
311 let n_dot_apex = dot_np(normal, cone.apex());
312 let t = (d - n_dot_apex) / n_dot_axis;
313
314 if t.abs() < 1e-10 {
319 return Ok(vec![]);
320 }
321
322 let center = Point3::new(
323 cone.apex().x() + t * axis.x(),
324 cone.apex().y() + t * axis.y(),
325 cone.apex().z() + t * axis.z(),
326 );
327 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
331 if circle_r < 1e-15 {
332 return Ok(vec![]);
333 }
334
335 let circle = Circle3D::new(center, normal, circle_r)?;
336 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
337 }
338
339 let c = normal.dot(axis);
351 let p2 = (1.0 - c * c).max(0.0);
352 let p = p2.sqrt();
353 let k = half_angle.sin().powi(2);
354 let a_coeff = p2 - k;
355
356 let m = Vec3::new(
358 axis.x() - c * normal.x(),
359 axis.y() - c * normal.y(),
360 axis.z() - c * normal.z(),
361 );
362 let m_len = m.length();
363 if m_len < 1e-12 {
364 let chains = sample_plane_cone(cone, normal, d, reach)?;
367 return Ok(chains
368 .into_iter()
369 .map(ExactIntersectionCurve::Points)
370 .collect());
371 }
372 let e1 = m * (1.0 / m_len);
373 let e2 = normal.cross(e1);
374 let apex = cone.apex();
375 let e = d - dot_np(normal, apex);
376
377 if a_coeff < -1e-9 {
380 let abs_a = -a_coeff; if e * c < 0.0 {
387 return Ok(vec![]);
388 }
389 let s_c = e * c * p / abs_a;
392 let rhs = e * e * k * (1.0 - k) / abs_a;
393 if rhs <= 0.0 {
394 return Ok(vec![]);
395 }
396 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
399 return Ok(vec![]);
400 }
401 let center = apex + normal * e + e1 * s_c;
402 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
403 (semi_s, semi_t, e1, e2)
404 } else {
405 (semi_t, semi_s, e2, e1)
406 };
407 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
408 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
409 }
410
411 let chains = sample_plane_cone(cone, normal, d, reach)?;
414 Ok(chains
415 .into_iter()
416 .map(ExactIntersectionCurve::Points)
417 .collect())
418}
419
420#[allow(clippy::many_single_char_names)]
435pub fn plane_cone_conic_arc(
436 cone: &ConicalSurface,
437 normal: Vec3,
438 d: f64,
439 from: Point3,
440 to: Point3,
441) -> Result<Option<NurbsCurve>, MathError> {
442 let len = normal.length();
443 if len < 1e-15 {
444 return Err(MathError::ZeroVector);
445 }
446 let (normal, d) = (normal * (1.0 / len), d / len);
447 let axis = cone.axis();
448 let c = normal.dot(axis);
449 let p2 = (1.0 - c * c).max(0.0);
450 let p = p2.sqrt();
451 let k = cone.half_angle().sin().powi(2);
452 let a_coeff = p2 - k;
453 let m = Vec3::new(
454 axis.x() - c * normal.x(),
455 axis.y() - c * normal.y(),
456 axis.z() - c * normal.z(),
457 );
458 let m_len = m.length();
459 if m_len < 1e-12 || a_coeff < -1e-9 {
460 return Ok(None);
461 }
462 let e1 = m * (1.0 / m_len);
463 let e2 = normal.cross(e1);
464 let apex = cone.apex();
465 let e = d - dot_np(normal, apex);
466 let origin = apex + normal * e;
467 let plane_st = |q: Point3| {
468 let w = q - origin;
469 (w.dot(e1), w.dot(e2))
470 };
471 let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
472 let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
473 if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
474 return Ok(None);
475 }
476 let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
477 let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
478 let (control, weights) = if a_coeff.abs() <= 1e-9 {
479 let lin = 2.0 * e * c * p;
481 if lin.abs() < 1e-12 * scale {
482 return Ok(None);
483 }
484 let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
485 if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
486 || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
487 {
488 return Ok(None);
489 }
490 let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
491 (vec![from, mid, to], vec![1.0; 3])
492 } else {
493 let s_c = -e * c * p / a_coeff;
495 let r = e * e * k * (1.0 - k) / a_coeff;
496 if r <= 0.0 {
497 return Ok(None);
498 }
499 let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
500 let (x0, x1) = (s0 - s_c, s1 - s_c);
501 if x0 * x1 <= 0.0 {
502 return Ok(None);
503 }
504 let side = x0.signum();
505 let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
506 let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
507 if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
508 return Ok(None);
509 }
510 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
511 let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
512 let mut control = vec![from];
513 let mut weights = vec![1.0];
514 for i in 0..pieces {
515 #[allow(clippy::cast_precision_loss)]
516 let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
517 let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
518 let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
519 let w = half.cosh();
520 control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
521 weights.push(w);
522 control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
523 weights.push(1.0);
524 }
525 (control, weights)
526 };
527 let pieces = (control.len() - 1) / 2;
528 let mut knots = vec![0.0; 3];
529 for i in 1..pieces {
530 #[allow(clippy::cast_precision_loss)]
531 knots.extend([i as f64; 2]);
532 }
533 #[allow(clippy::cast_precision_loss)]
534 knots.extend([pieces as f64; 3]);
535 let curve = NurbsCurve::new(2, knots, control, weights)?;
536 let (sin_a, cos_a) = cone.half_angle().sin_cos();
541 let off_cone = |q: Point3| {
542 let w = q - apex;
543 let h = w.dot(axis);
544 (w - axis * h)
545 .length()
546 .mul_add(sin_a, -(h.abs() * cos_a))
547 .abs()
548 };
549 for i in 0..pieces {
550 for f in [0.25, 0.5, 0.75] {
551 #[allow(clippy::cast_precision_loss)]
552 if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
553 return Ok(None);
554 }
555 }
556 }
557 Ok(Some(curve))
558}
559
560#[derive(Clone, Copy)]
562pub enum AnalyticSurface<'a> {
563 Cylinder(&'a CylindricalSurface),
565 Cone(&'a ConicalSurface),
567 Sphere(&'a SphericalSurface),
569 Torus(&'a ToroidalSurface),
571}
572
573fn dot_np(n: Vec3, p: Point3) -> f64 {
575 n.dot(Vec3::new(p.x(), p.y(), p.z()))
576}
577
578pub fn intersect_plane_analytic(
586 surface: AnalyticSurface<'_>,
587 normal: Vec3,
588 d: f64,
589) -> Result<Vec<IntersectionCurve>, MathError> {
590 match surface {
591 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
592 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
593 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
594 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
595 }
596}
597
598pub fn sample_plane_analytic(
609 surface: AnalyticSurface<'_>,
610 normal: Vec3,
611 d: f64,
612) -> Result<Vec<Vec<Point3>>, MathError> {
613 match surface {
614 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
615 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
616 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
617 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
618 }
619}
620
621#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
623fn sample_plane_cylinder(
624 cyl: &CylindricalSurface,
625 normal: Vec3,
626 d: f64,
627) -> Result<Vec<Vec<Point3>>, MathError> {
628 let n_samples = 64_usize;
629 let mut points = Vec::with_capacity(n_samples + 1);
630
631 for i in 0..=n_samples {
632 let u = TAU * (i as f64) / (n_samples as f64);
633 let base = cyl.evaluate(u, 0.0);
634 let n_dot_axis = normal.dot(cyl.axis());
635 let n_dot_base = dot_np(normal, base);
636
637 if n_dot_axis.abs() < 1e-12 {
638 if (n_dot_base - d).abs() < 1e-6 {
639 points.push(base);
640 }
641 } else {
642 let v = (d - n_dot_base) / n_dot_axis;
643 if v.abs() <= 100.0 {
644 points.push(cyl.evaluate(u, v));
645 }
646 }
647 }
648
649 if points.len() < 2 {
650 Ok(vec![])
651 } else {
652 Ok(vec![points])
653 }
654}
655
656#[allow(clippy::cast_precision_loss)]
658fn sample_plane_sphere(
659 sphere: &SphericalSurface,
660 normal: Vec3,
661 d: f64,
662) -> Result<Vec<Vec<Point3>>, MathError> {
663 let h = dot_np(normal, sphere.center()) - d;
664 let r = sphere.radius();
665
666 if h.abs() > r - 1e-10 {
667 return Ok(vec![]);
668 }
669
670 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
671 let circle_center = Point3::new(
672 h.mul_add(-normal.x(), sphere.center().x()),
673 h.mul_add(-normal.y(), sphere.center().y()),
674 h.mul_add(-normal.z(), sphere.center().z()),
675 );
676
677 let basis = Frame3::from_normal(circle_center, normal)?;
678 let u_dir = basis.x;
679 let v_dir = basis.y;
680
681 let n_samples = 64_usize;
682 let mut points = Vec::with_capacity(n_samples + 1);
683
684 for i in 0..=n_samples {
685 let theta = TAU * (i as f64) / (n_samples as f64);
686 let (sin_t, cos_t) = theta.sin_cos();
687 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
688 }
689
690 Ok(vec![points])
691}
692
693#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
705fn sample_plane_cone(
706 cone: &ConicalSurface,
707 normal: Vec3,
708 d: f64,
709 reach: f64,
710) -> Result<Vec<Vec<Point3>>, MathError> {
711 let apex = cone.apex();
712 let n_dot_apex = dot_np(normal, apex);
713 let e = d - n_dot_apex;
714
715 let n_samples = 512_usize;
719 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
720 let mut v_min = f64::INFINITY;
721 for i in 0..n_samples {
722 let u = TAU * (i as f64) / (n_samples as f64);
723 let g = cone.evaluate(u, 1.0) - apex;
724 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
725 if n_dot_g.abs() < 1e-12 {
726 vs.push(None);
727 continue;
728 }
729 let v = e / n_dot_g;
730 if v >= -1e-12 {
731 let v = v.max(0.0);
732 v_min = v_min.min(v);
733 vs.push(Some(v));
734 } else {
735 vs.push(None);
736 }
737 }
738
739 if !v_min.is_finite() {
740 return Ok(Vec::new());
741 }
742
743 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
752
753 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
756
757 let point_at = |u: f64, v: f64| -> Point3 {
758 let g = cone.evaluate(u, 1.0) - apex;
759 apex + g * v
760 };
761 #[allow(clippy::cast_precision_loss)]
762 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
763 let n_dot_g_at = |u: f64| -> f64 {
764 let g = cone.evaluate(u, 1.0) - apex;
765 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
766 };
767
768 if kept.iter().all(Option::is_some) {
769 let mut pts: Vec<Point3> = kept
771 .iter()
772 .enumerate()
773 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
774 .collect();
775 if let Some(&first) = pts.first() {
776 pts.push(first);
777 }
778 return Ok(vec![pts]);
779 }
780
781 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
790 let Some(v_end) = kept[i_end] else {
791 return Vec::new();
792 };
793 let u_end = u_of(i_end);
794 #[allow(clippy::cast_precision_loss)]
795 let pitch = TAU / (n_samples as f64);
796 let u_next = if forward {
797 u_end + pitch
798 } else {
799 u_end - pitch
800 };
801 let target = e / v_max;
802 let h_end = n_dot_g_at(u_end) - target;
803 let h_next = n_dot_g_at(u_next) - target;
804 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
805 return Vec::new();
806 }
807 let (mut lo, mut hi) = (u_end, u_next);
808 for _ in 0..60 {
809 let mid = f64::midpoint(lo, hi);
810 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
811 lo = mid;
812 } else {
813 hi = mid;
814 }
815 }
816 let u_star = f64::midpoint(lo, hi);
817 let tail_n = 8_usize;
818 (1..=tail_n)
819 .filter_map(|k| {
820 #[allow(clippy::cast_precision_loss)]
821 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
822 let ng = n_dot_g_at(u);
823 if ng.abs() < 1e-12 {
824 return None;
825 }
826 let v = e / ng;
827 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
828 })
829 .collect()
830 };
831
832 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
835 let mut chains: Vec<Vec<Point3>> = Vec::new();
836 let mut run: Vec<usize> = Vec::new();
837 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
838 if run.len() >= 2 {
839 let first = run[0];
840 let last = run[run.len() - 1];
841 let mut pts: Vec<Point3> = tail(first, false, &kept);
842 pts.reverse();
843 pts.extend(
844 run.iter()
845 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
846 );
847 pts.extend(tail(last, true, &kept));
848 chains.push(pts);
849 }
850 run.clear();
851 };
852 for k in 0..n_samples {
853 let idx = (gap + k) % n_samples;
854 if kept[idx].is_some() {
855 run.push(idx);
856 } else {
857 flush(&mut run, &mut chains);
858 }
859 }
860 flush(&mut run, &mut chains);
861 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
862}
863
864#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
870 torus: &ToroidalSurface,
871 normal: Vec3,
872 d: f64,
873) -> Result<Vec<Vec<Point3>>, MathError> {
874 Ok(plane_torus_loops(torus, normal, d, 128)
875 .into_iter()
876 .map(|run| run.into_iter().map(|p| p.point).collect())
877 .collect())
878}
879
880#[allow(clippy::cast_precision_loss)]
890pub fn intersect_plane_cylinder(
891 cyl: &CylindricalSurface,
892 normal: Vec3,
893 d: f64,
894) -> Result<Vec<IntersectionCurve>, MathError> {
895 let n_samples = 64_usize;
896 let mut points_3d = Vec::new();
897 let mut ipoints = Vec::new();
898
899 for i in 0..=n_samples {
900 let u = TAU * (i as f64) / (n_samples as f64);
901 let base = cyl.evaluate(u, 0.0);
904 let n_dot_axis = normal.dot(cyl.axis());
905 let n_dot_base = dot_np(normal, base);
906
907 if n_dot_axis.abs() < 1e-12 {
908 if (n_dot_base - d).abs() < 1e-6 {
910 let pt = base;
911 points_3d.push(pt);
912 ipoints.push(IntersectionPoint {
913 point: pt,
914 param1: (u, 0.0),
915 param2: (0.0, 0.0),
916 });
917 }
918 } else {
919 let v = (d - n_dot_base) / n_dot_axis;
920 if v.abs() <= 100.0 {
922 let pt = cyl.evaluate(u, v);
923 points_3d.push(pt);
924 ipoints.push(IntersectionPoint {
925 point: pt,
926 param1: (u, v),
927 param2: (0.0, 0.0),
928 });
929 }
930 }
931 }
932
933 build_curves_from_points(&points_3d, ipoints)
934}
935
936#[allow(clippy::cast_precision_loss)]
945pub fn intersect_plane_sphere(
946 sphere: &SphericalSurface,
947 normal: Vec3,
948 d: f64,
949) -> Result<Vec<IntersectionCurve>, MathError> {
950 let h = dot_np(normal, sphere.center()) - d;
951 let r = sphere.radius();
952
953 if h.abs() > r - 1e-10 {
955 return Ok(vec![]);
956 }
957
958 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
959 let circle_center = Point3::new(
960 h.mul_add(-normal.x(), sphere.center().x()),
961 h.mul_add(-normal.y(), sphere.center().y()),
962 h.mul_add(-normal.z(), sphere.center().z()),
963 );
964
965 let basis = Frame3::from_normal(circle_center, normal)?;
967 let u_dir = basis.x;
968 let v_dir = basis.y;
969
970 let n_samples = 64_usize;
971 let mut points_3d = Vec::new();
972 let mut ipoints = Vec::new();
973
974 for i in 0..=n_samples {
975 let theta = TAU * (i as f64) / (n_samples as f64);
976 let (sin_t, cos_t) = theta.sin_cos();
977 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
978 points_3d.push(pt);
979 ipoints.push(IntersectionPoint {
980 point: pt,
981 param1: (theta, 0.0),
982 param2: (0.0, 0.0),
983 });
984 }
985
986 build_curves_from_points(&points_3d, ipoints)
987}
988
989#[allow(clippy::cast_precision_loss)]
998pub fn intersect_plane_cone(
999 cone: &ConicalSurface,
1000 normal: Vec3,
1001 d: f64,
1002) -> Result<Vec<IntersectionCurve>, MathError> {
1003 let n_samples = 64_usize;
1004 let mut points_3d = Vec::new();
1005 let mut ipoints = Vec::new();
1006
1007 for i in 0..n_samples {
1008 let u = TAU * (i as f64) / (n_samples as f64);
1009 let apex = cone.apex();
1012 let n_dot_apex = dot_np(normal, apex);
1013 let p1 = cone.evaluate(u, 1.0);
1015 let dir = p1 - apex;
1016 let n_dot_dir = normal.dot(dir);
1017
1018 if n_dot_dir.abs() < 1e-12 {
1019 continue;
1020 }
1021
1022 let v = (d - n_dot_apex) / n_dot_dir;
1023 if v.abs() > 1e-10 && v.abs() < 100.0 {
1025 let pt = cone.evaluate(u, v);
1026 points_3d.push(pt);
1027 ipoints.push(IntersectionPoint {
1028 point: pt,
1029 param1: (u, v),
1030 param2: (0.0, 0.0),
1031 });
1032 }
1033 }
1034
1035 build_curves_from_points(&points_3d, ipoints)
1036}
1037
1038#[allow(clippy::unnecessary_wraps)]
1050pub fn intersect_plane_torus(
1051 torus: &ToroidalSurface,
1052 normal: Vec3,
1053 d: f64,
1054) -> Result<Vec<IntersectionCurve>, MathError> {
1055 let mut curves = Vec::new();
1059 for ipts in plane_torus_loops(torus, normal, d, 128) {
1060 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1061 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1062 curves.push(IntersectionCurve {
1063 curve,
1064 points: ipts,
1065 });
1066 }
1067 }
1068
1069 Ok(curves)
1070}
1071
1072const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1075
1076#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1098fn plane_torus_loops(
1099 torus: &ToroidalSurface,
1100 normal: Vec3,
1101 d: f64,
1102 n_v: usize,
1103) -> Vec<Vec<IntersectionPoint>> {
1104 let big_r = torus.major_radius();
1105 let small_r = torus.minor_radius();
1106 let a = normal.dot(torus.x_axis());
1107 let b = normal.dot(torus.y_axis());
1108 let c = normal.dot(torus.z_axis());
1109 let s = a.hypot(b);
1110 let phi = b.atan2(a);
1111 let d_local = d - dot_np(normal, torus.center());
1112 let point = |u: f64, v: f64| IntersectionPoint {
1113 point: torus.evaluate(u, v),
1114 param1: (u, v.rem_euclid(TAU)),
1115 param2: (0.0, 0.0),
1116 };
1117 let closed = |mut run: Vec<IntersectionPoint>| {
1118 run.push(run[0]);
1119 run
1120 };
1121
1122 if s < 1e-12 {
1124 if c.abs() < 1e-12 {
1125 return Vec::new();
1126 }
1127 let sin_v = d_local / (small_r * c);
1128 if sin_v.abs() > 1.0 + 1e-9 {
1129 return Vec::new();
1130 }
1131 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1132 let v1 = std::f64::consts::PI - v0;
1133 let mut vs = vec![v0];
1134 let apart = (v1 - v0).rem_euclid(TAU);
1137 if apart.min(TAU - apart) > 1e-9 {
1138 vs.push(v1);
1139 }
1140 return vs
1141 .into_iter()
1142 .map(|v| {
1143 closed(
1144 (0..n_v)
1145 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1146 .collect(),
1147 )
1148 })
1149 .collect();
1150 }
1151
1152 let step = TAU / (n_v as f64);
1155 let v_off = step * 0.5;
1156 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1158 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1159 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1160 let scan: Vec<f64> = (0..n_v).map(|i| (i as f64).mul_add(step, v_off)).collect();
1161 let touches = |lo: f64, hi: f64| {
1164 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1165 let (mut lo, mut hi) = (lo, hi);
1166 for _ in 0..80 {
1167 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1168 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1169 hi = m2;
1170 } else {
1171 lo = m1;
1172 }
1173 }
1174 1.0 - rhs_at(f64::midpoint(lo, hi)).abs() < 1e-12
1175 };
1176 let turn = |v_in: f64, v_out: f64| {
1178 let (mut lo, mut hi) = (v_in, v_out);
1179 for _ in 0..60 {
1180 let mid = f64::midpoint(lo, hi);
1181 if inside(mid) {
1182 lo = mid;
1183 } else {
1184 hi = mid;
1185 }
1186 }
1187 lo
1188 };
1189 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1190 if in_scan.iter().all(|&x| x) {
1191 let touching = scan.iter().any(|&v| touches(v, v + step));
1192 return [1.0, -1.0]
1193 .into_iter()
1194 .map(|sign| {
1195 let run: Vec<IntersectionPoint> = scan.iter().map(|&v| branch(v, sign)).collect();
1196 if touching { run } else { closed(run) }
1197 })
1198 .collect();
1199 }
1200 let Some(first) = (0..n_v).find(|&i| in_scan[i] && !in_scan[(i + n_v - 1) % n_v]) else {
1201 return Vec::new();
1202 };
1203 let mut loops = Vec::new();
1204 let mut k = 0;
1205 while k < n_v {
1206 let i = (first + k) % n_v;
1207 if !in_scan[i] {
1208 k += 1;
1209 continue;
1210 }
1211 let len = (0..n_v - k).take_while(|&j| in_scan[(i + j) % n_v]).count();
1213 let v_a = scan[i];
1214 let v_b = ((len - 1) as f64).mul_add(step, v_a);
1215 let run_v = |j: usize| (j as f64).mul_add(step, v_a);
1216 let (t_lo, t_hi) = (turn(v_a, v_a - step), turn(v_b, v_b + step));
1217 let touching = (0..len - 1).any(|j| touches(run_v(j), run_v(j + 1)));
1218 let (u_lo, u_hi) = (0..len)
1226 .map(run_v)
1227 .chain([t_lo, t_hi])
1228 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1229 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1230 (lo.min(u), hi.max(u))
1231 });
1232 let m = (len as f64)
1233 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1234 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1235 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1236 .ceil();
1237 let at = |k: f64| {
1238 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1239 (t_hi - t_lo).mul_add(f, t_lo)
1240 };
1241 let steps = m as usize;
1242 let mut pts: Vec<IntersectionPoint> =
1243 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1244 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1245 loops.push(if touching { pts } else { closed(pts) });
1246 k += len;
1247 }
1248 loops
1249}
1250
1251#[allow(clippy::cast_precision_loss)]
1260fn plane_torus_winding_loops(
1261 torus: &ToroidalSurface,
1262 normal: Vec3,
1263 d: f64,
1264 n_v: usize,
1265) -> Option<Vec<Vec<Point3>>> {
1266 let big_r = torus.major_radius();
1267 let small_r = torus.minor_radius();
1268 let a = normal.dot(torus.x_axis());
1269 let b = normal.dot(torus.y_axis());
1270 let c = normal.dot(torus.z_axis());
1271 let s = a.hypot(b);
1272 if s < 1e-12 * normal.length() || small_r >= big_r {
1273 return None;
1274 }
1275 let phi = b.atan2(a);
1276 let d_local = d - dot_np(normal, torus.center());
1277 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1278 let dense = 8 * n_v;
1279 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1280 return None;
1281 }
1282 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1283 for i in 0..n_v {
1284 let v = TAU * i as f64 / n_v as f64;
1285 let delta = rhs(v).acos();
1286 loops[0].push(torus.evaluate(phi + delta, v));
1287 loops[1].push(torus.evaluate(phi - delta, v));
1288 }
1289 Some(
1290 loops
1291 .into_iter()
1292 .map(|mut run| {
1293 run.push(run[0]);
1294 run
1295 })
1296 .collect(),
1297 )
1298}
1299
1300#[must_use]
1314pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1315 let c = torus.center();
1316 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1317 let big_r = torus.major_radius();
1318 let small_r = torus.minor_radius();
1319
1320 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1322 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1323 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1324 let (c0, c1) = (za.dot(o), za.dot(dir));
1325
1326 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1328 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1329 let g0 = a0.mul_add(
1330 a0,
1331 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1332 );
1333
1334 let four_rr = 4.0 * big_r * big_r;
1336 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1337 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1338 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1339
1340 let e4 = g2 * g2;
1342 let e3 = 2.0 * g2 * g1;
1343 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1344 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1345 let e0 = g0.mul_add(g0, -h0);
1346
1347 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1348 let tube = |t: f64, across: bool| -> f64 {
1354 let p = origin + dir * t;
1355 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1356 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1357 let centre = if across { -big_r } else { big_r };
1358 (a.hypot(b) - centre).hypot(cc) - small_r
1359 };
1360 for t in &mut roots {
1361 let across = big_r < small_r && tube(*t, true).abs() < tube(*t, false).abs();
1362 for _ in 0..8 {
1363 let eps = 1e-7;
1364 let f = tube(*t, across);
1365 let df = (tube(*t + eps, across) - tube(*t - eps, across)) / (2.0 * eps);
1366 if df.abs() <= 1e-12 {
1367 break;
1368 }
1369 let step = f / df;
1370 *t -= step;
1371 if step.abs() < 1e-14 * (1.0 + t.abs()) {
1372 break;
1373 }
1374 }
1375 }
1376 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1377 roots.dedup_by(|a, b| (*a - *b).abs() < 1e-9 * (1.0 + b.abs()));
1378 roots
1379}
1380
1381fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1384 if c4.abs() < 1e-14 {
1386 return real_roots_cubic(c3, c2, c1, c0);
1387 }
1388 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1390 let eval = |z: Complex| -> Complex {
1391 let mut acc = Complex::new(1.0, 0.0);
1393 acc = acc * z + Complex::new(a, 0.0);
1394 acc = acc * z + Complex::new(b, 0.0);
1395 acc = acc * z + Complex::new(c, 0.0);
1396 acc * z + Complex::new(d, 0.0)
1397 };
1398 let seed = Complex::new(0.4, 0.9);
1400 let mut r = [
1401 Complex::new(1.0, 0.0),
1402 seed,
1403 seed * seed,
1404 seed * seed * seed,
1405 ];
1406 for _ in 0..100 {
1407 let mut max_step = 0.0_f64;
1408 for i in 0..4 {
1409 let mut denom = Complex::new(1.0, 0.0);
1410 for j in 0..4 {
1411 if i != j {
1412 denom = denom * (r[i] - r[j]);
1413 }
1414 }
1415 if denom.norm() < 1e-300 {
1416 continue;
1417 }
1418 let step = eval(r[i]) / denom;
1419 r[i] = r[i] - step;
1420 max_step = max_step.max(step.norm());
1421 }
1422 if max_step < 1e-14 {
1423 break;
1424 }
1425 }
1426 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1433 let mut out: Vec<f64> = Vec::new();
1434 for z in r {
1435 if z.im.abs() >= 1e-7 {
1436 continue;
1437 }
1438 let x = z.re;
1439 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1442 if p_real(x).abs() > 1e-6 * scale {
1443 continue;
1444 }
1445 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1446 continue;
1447 }
1448 out.push(x);
1449 }
1450 out
1451}
1452
1453fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1455 if a.abs() < 1e-14 {
1456 return real_roots_quadratic(b, c, d);
1457 }
1458 let (b, c, d) = (b / a, c / a, d / a);
1460 let p = c - b * b / 3.0;
1461 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1462 let shift = -b / 3.0;
1463 let disc = q * q / 4.0 + p * p * p / 27.0;
1464 if disc > 1e-14 {
1465 let sq = disc.sqrt();
1466 let u = (-q / 2.0 + sq).cbrt();
1467 let v = (-q / 2.0 - sq).cbrt();
1468 vec![u + v + shift]
1469 } else if disc < -1e-14 {
1470 let m = 2.0 * (-p / 3.0).sqrt();
1472 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1473 (0..3)
1474 .map(|k| {
1475 m.mul_add(
1476 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1477 shift,
1478 )
1479 })
1480 .collect()
1481 } else {
1482 let u = (-q / 2.0).cbrt();
1484 vec![2.0 * u + shift, -u + shift]
1485 }
1486}
1487
1488fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1490 if a.abs() < 1e-14 {
1491 if b.abs() < 1e-14 {
1492 return Vec::new();
1493 }
1494 return vec![-c / b];
1495 }
1496 let disc = b * b - 4.0 * a * c;
1497 if disc < 0.0 {
1498 Vec::new()
1499 } else {
1500 let sq = disc.sqrt();
1501 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1502 }
1503}
1504
1505#[derive(Clone, Copy)]
1507struct Complex {
1508 re: f64,
1509 im: f64,
1510}
1511
1512impl Complex {
1513 const fn new(re: f64, im: f64) -> Self {
1514 Self { re, im }
1515 }
1516 fn norm(self) -> f64 {
1517 self.re.hypot(self.im)
1518 }
1519}
1520
1521impl std::ops::Add for Complex {
1522 type Output = Self;
1523 fn add(self, o: Self) -> Self {
1524 Self::new(self.re + o.re, self.im + o.im)
1525 }
1526}
1527
1528impl std::ops::Sub for Complex {
1529 type Output = Self;
1530 fn sub(self, o: Self) -> Self {
1531 Self::new(self.re - o.re, self.im - o.im)
1532 }
1533}
1534
1535impl std::ops::Mul for Complex {
1536 type Output = Self;
1537 fn mul(self, o: Self) -> Self {
1538 Self::new(
1539 self.re.mul_add(o.re, -(self.im * o.im)),
1540 self.re.mul_add(o.im, self.im * o.re),
1541 )
1542 }
1543}
1544
1545impl std::ops::Div for Complex {
1546 type Output = Self;
1547 fn div(self, o: Self) -> Self {
1548 let den = o.re.mul_add(o.re, o.im * o.im);
1549 Self::new(
1550 self.re.mul_add(o.re, self.im * o.im) / den,
1551 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1552 )
1553 }
1554}
1555
1556fn build_curves_from_points(
1560 points_3d: &[Point3],
1561 ipoints: Vec<IntersectionPoint>,
1562) -> Result<Vec<IntersectionCurve>, MathError> {
1563 if points_3d.len() < 2 {
1564 return Ok(vec![]);
1565 }
1566
1567 let degree = 3.min(points_3d.len() - 1);
1568 let curve = interpolate(points_3d, degree)?;
1569 Ok(vec![IntersectionCurve {
1570 curve,
1571 points: ipoints,
1572 }])
1573}
1574
1575#[allow(
1587 clippy::cast_precision_loss,
1588 clippy::too_many_lines,
1589 clippy::similar_names,
1590 clippy::unnecessary_wraps,
1591 clippy::type_complexity
1592)]
1593pub fn intersect_analytic_analytic(
1594 a: AnalyticSurface<'_>,
1595 b: AnalyticSurface<'_>,
1596 grid_res: usize,
1597) -> Result<Vec<IntersectionCurve>, MathError> {
1598 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1599}
1600
1601pub fn intersect_analytic_analytic_bounded(
1612 a: AnalyticSurface<'_>,
1613 b: AnalyticSurface<'_>,
1614 grid_res: usize,
1615 v_range_hint_a: Option<(f64, f64)>,
1616 v_range_hint_b: Option<(f64, f64)>,
1617) -> Result<Vec<IntersectionCurve>, MathError> {
1618 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1619}
1620
1621pub fn intersect_analytic_analytic_in_region(
1634 a: AnalyticSurface<'_>,
1635 b: AnalyticSurface<'_>,
1636 grid_res: usize,
1637 v_range_hint_a: Option<(f64, f64)>,
1638 v_range_hint_b: Option<(f64, f64)>,
1639 region: Aabb3,
1640) -> Result<Vec<IntersectionCurve>, MathError> {
1641 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1642}
1643
1644fn intersect_analytic_analytic_impl(
1645 a: AnalyticSurface<'_>,
1646 b: AnalyticSurface<'_>,
1647 grid_res: usize,
1648 v_range_hint_a: Option<(f64, f64)>,
1649 v_range_hint_b: Option<(f64, f64)>,
1650 region: Option<Aabb3>,
1651) -> Result<Vec<IntersectionCurve>, MathError> {
1652 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1655 return Ok(result);
1656 }
1657
1658 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1659 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1660 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1661 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1662
1663 let diag_a = {
1665 let p00 = surf_a(u_range_a.0, v_range_a.0);
1666 let p11 = surf_a(u_range_a.1, v_range_a.1);
1667 (p00 - p11).length()
1668 };
1669 let diag_b = {
1670 let p00 = surf_b(u_range_b.0, v_range_b.0);
1671 let p11 = surf_b(u_range_b.1, v_range_b.1);
1672 (p00 - p11).length()
1673 };
1674 let char_size = diag_a.min(diag_b).max(0.1);
1675
1676 #[allow(clippy::type_complexity)]
1680 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1681 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1685 let mut min_dist = f64::INFINITY;
1686
1687 #[allow(clippy::cast_precision_loss)]
1688 for ia in 0..grid_res {
1689 for ja in 0..grid_res {
1690 let ua =
1691 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1692 let va =
1693 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1694
1695 let pa = surf_a(ua, va);
1696
1697 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1699 let pb = surf_b(ub, vb);
1700 let dist = (pa - pb).length();
1701 min_dist = min_dist.min(dist);
1702
1703 if dist < seed_threshold {
1704 let mid = Point3::new(
1709 (pa.x() + pb.x()) * 0.5,
1710 (pa.y() + pb.y()) * 0.5,
1711 (pa.z() + pb.z()) * 0.5,
1712 );
1713 seeds.push((mid, (ua, va), (ub, vb)));
1714 }
1715 }
1716 }
1717
1718 let reject_dist = (char_size / grid_res as f64) * 3.0;
1727 if min_dist > reject_dist {
1728 return Ok(vec![]);
1729 }
1730
1731 if seeds.is_empty() {
1732 return Ok(vec![]);
1733 }
1734
1735 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1739 let dedup_radius = march_step * 10.0;
1740 let mut unique_seeds = Vec::new();
1741 for seed in &seeds {
1742 let dominated = unique_seeds
1743 .iter()
1744 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1745 if !dominated {
1746 unique_seeds.push(*seed);
1747 }
1748 }
1749
1750 let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1755 if let Some(r) = region {
1756 for seed in &mut unique_seeds {
1757 let mut p = seed.0;
1758 for _ in 0..8 {
1759 let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1760 let pa = surf_a(ua, va);
1761 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1762 let pb = surf_b(ub, vb);
1763 p = Point3::new(
1764 (pa.x() + pb.x()) * 0.5,
1765 (pa.y() + pb.y()) * 0.5,
1766 (pa.z() + pb.z()) * 0.5,
1767 );
1768 if (pa - pb).length() < 1e-9 {
1769 break;
1770 }
1771 }
1772 seed.0 = p;
1773 }
1774 unique_seeds.retain(|seed| r.contains_point(seed.0));
1775 }
1776
1777 let mut curves = Vec::new();
1779 let mut used_seeds = vec![false; unique_seeds.len()];
1780
1781 for si in 0..unique_seeds.len() {
1782 if used_seeds[si] {
1783 continue;
1784 }
1785 used_seeds[si] = true;
1786
1787 let march_result = march_analytic_intersection(
1788 &a,
1789 &b,
1790 surf_a.as_ref(),
1791 norm_a.as_ref(),
1792 surf_b.as_ref(),
1793 norm_b.as_ref(),
1794 unique_seeds[si].0,
1795 u_range_a,
1796 v_range_a,
1797 u_range_b,
1798 v_range_b,
1799 march_step,
1800 is_u_periodic(&a),
1801 is_u_periodic(&b),
1802 region,
1803 );
1804
1805 if march_result.len() >= 2 {
1806 for (sj, other) in unique_seeds.iter().enumerate() {
1807 if !used_seeds[sj]
1808 && march_result
1809 .iter()
1810 .any(|p| (*p - other.0).length() < dedup_radius)
1811 {
1812 used_seeds[sj] = true;
1813 }
1814 }
1815
1816 let ipts: Vec<IntersectionPoint> = march_result
1817 .iter()
1818 .map(|&pt| IntersectionPoint {
1819 point: pt,
1820 param1: (0.0, 0.0),
1821 param2: (0.0, 0.0),
1822 })
1823 .collect();
1824
1825 let degree = 3.min(march_result.len() - 1);
1826 if let Ok(curve) = interpolate(&march_result, degree) {
1827 curves.push(IntersectionCurve {
1828 curve,
1829 points: ipts,
1830 });
1831 }
1832 }
1833 }
1834
1835 Ok(curves)
1836}
1837
1838#[allow(clippy::too_many_lines)]
1852fn try_algebraic_intersection(
1853 a: &AnalyticSurface<'_>,
1854 b: &AnalyticSurface<'_>,
1855 v_range_a: Option<(f64, f64)>,
1856 v_range_b: Option<(f64, f64)>,
1857) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1858 match (a, b) {
1859 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1860 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1861 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1862 ),
1863 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1864 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1865 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1866 ),
1867 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1868 algebraic_sphere_sphere(s1, s2).map(Some)
1869 }
1870 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1871 let axis_dot = c1.axis().dot(c2.axis()).abs();
1872 if axis_dot > 1.0 - 1e-10 {
1873 let delta = c2.origin() - c1.origin();
1875 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1876 let along = delta_vec.dot(c1.axis());
1877 let perp = (delta_vec - c1.axis() * along).length();
1878 if perp < 1e-8 {
1879 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1882 return Ok(None); }
1884 return Ok(Some(vec![])); }
1886 }
1887 algebraic_cylinder_cylinder(c1, c2)
1889 }
1890 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1892 algebraic_sphere_cylinder(s, c, true)
1893 }
1894 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1895 algebraic_sphere_cylinder(s, c, false)
1896 }
1897 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1898 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1899 Ok(ruling_cone_sphere(cone, sphere, true))
1900 }
1901 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1902 Ok(ruling_cone_sphere(cone, sphere, false))
1903 }
1904 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1905 Ok(parallel_axis_torus_cylinder(t, c, true)
1906 .or_else(|| ruling_torus_cylinder(t, c, true)))
1907 }
1908 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1909 Ok(parallel_axis_torus_cylinder(t, c, false)
1910 .or_else(|| ruling_torus_cylinder(t, c, false)))
1911 }
1912 _ => Ok(None),
1913 }
1914}
1915
1916fn parallel_axis_torus_cylinder(
1923 torus: &ToroidalSurface,
1924 cyl: &CylindricalSurface,
1925 torus_first: bool,
1926) -> Option<Vec<IntersectionCurve>> {
1927 let axis = torus.z_axis();
1928 let along = cyl.axis().dot(axis);
1929 if along.abs() < 1.0 - 1e-10 {
1930 return None;
1931 }
1932 let offset = cyl.origin() - torus.center();
1933 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1934 return None;
1935 }
1936 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1937 let roots = |u: f64| {
1938 let q = cyl.evaluate(u, 0.0) - torus.center();
1939 let height = q.dot(axis);
1940 let rho = (q - axis * height).length();
1941 let reach = minor * minor - (rho - major) * (rho - major);
1942 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1943 };
1944 let samples = ruling_samples(cyl, &roots);
1945 let loops = if samples.iter().all(Option::is_some) {
1946 closed_ruling_loops(cyl, &roots, &samples)
1947 } else {
1948 partial_ruling_loops(cyl, &roots, &samples)
1949 };
1950 if loops.is_empty() {
1951 return None;
1952 }
1953 Some(fit_ruling_loops(&loops, |p| {
1954 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1955 }))
1956}
1957
1958fn meridian_crossings(
1964 first: (f64, f64, f64),
1965 second: (f64, f64, f64),
1966 scale: f64,
1967) -> Option<Vec<(f64, f64)>> {
1968 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1969 let (dx, dz) = (x2 - x1, z2 - z1);
1970 let dist = dx.hypot(dz);
1971 let slack = 1e-9 * scale;
1972 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1973 return None;
1974 }
1975 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1976 return Some(Vec::new());
1977 }
1978 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1979 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1980 let (ux, uz) = (dx / dist, dz / dist);
1981 let mut crossings = Vec::with_capacity(2);
1982 for side in [1.0, -1.0] {
1983 let rho = x1 + along * ux - side * across * uz;
1984 if rho <= slack {
1985 return None;
1986 }
1987 crossings.push((rho, z1 + along * uz + side * across * ux));
1988 }
1989 Some(crossings)
1990}
1991
1992fn circles_about_axis(
1994 base: Point3,
1995 axis: Vec3,
1996 crossings: &[(f64, f64)],
1997) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1998 crossings
1999 .iter()
2000 .map(|&(rho, z)| {
2001 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
2002 })
2003 .collect()
2004}
2005
2006pub fn exact_torus_torus(
2017 first: &ToroidalSurface,
2018 second: &ToroidalSurface,
2019) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2020 let axis = first.z_axis();
2021 let scale = first.major_radius() + second.major_radius();
2022 let offset = second.center() - first.center();
2023 if first.minor_radius() >= first.major_radius()
2025 || second.minor_radius() >= second.major_radius()
2026 || axis.cross(second.z_axis()).length() > 1e-9
2027 || offset.cross(axis).length() > 1e-9 * scale
2028 {
2029 return Ok(None);
2030 }
2031 let Some(crossings) = meridian_crossings(
2032 (first.major_radius(), 0.0, first.minor_radius()),
2033 (
2034 second.major_radius(),
2035 offset.dot(axis),
2036 second.minor_radius(),
2037 ),
2038 scale,
2039 ) else {
2040 return Ok(None);
2041 };
2042 circles_about_axis(first.center(), axis, &crossings).map(Some)
2043}
2044
2045pub fn exact_cylinder_torus(
2059 cylinder: &CylindricalSurface,
2060 torus: &ToroidalSurface,
2061) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2062 let axis = torus.z_axis();
2063 let scale = torus.major_radius() + cylinder.radius();
2064 let offset = cylinder.origin() - torus.center();
2065 let linear = crate::tolerance::Tolerance::default().linear;
2066 if axis.cross(cylinder.axis()).length() > 1e-9
2067 || offset.cross(axis).length() > 1e-9 * scale
2068 || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2069 {
2070 return Ok(None);
2071 }
2072 let gap = cylinder.radius() - torus.major_radius();
2073 let small = torus.minor_radius();
2074 if (gap.abs() - small).abs() <= linear {
2078 return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2079 }
2080 if gap.abs() > small {
2081 return Ok(Some(Vec::new()));
2082 }
2083 let height = small.mul_add(small, -(gap * gap)).sqrt();
2084 circles_about_axis(
2085 torus.center(),
2086 axis,
2087 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2088 )
2089 .map(Some)
2090}
2091
2092pub fn exact_sphere_torus(
2105 sphere: &SphericalSurface,
2106 torus: &ToroidalSurface,
2107) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2108 let axis = torus.z_axis();
2109 let scale = torus.major_radius() + sphere.radius();
2110 let offset = sphere.center() - torus.center();
2111 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2113 return Ok(None);
2114 }
2115 let Some(crossings) = meridian_crossings(
2116 (0.0, offset.dot(axis), sphere.radius()),
2117 (torus.major_radius(), 0.0, torus.minor_radius()),
2118 scale,
2119 ) else {
2120 return Ok(None);
2121 };
2122 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2123}
2124
2125pub fn exact_cone_cone(
2150 c1: &ConicalSurface,
2151 c2: &ConicalSurface,
2152) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2153 let axis = c1.axis();
2154 let axis2 = c2.axis();
2155
2156 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2158 return Ok(None); }
2160 let apex1 = c1.apex();
2161 let apex2 = c2.apex();
2162 let delta = apex2 - apex1;
2163 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2164 let along = delta_v.dot(axis);
2165 if (delta_v - axis * along).length() > 1e-8 {
2166 return offset_parallel_cone_cone(c1, c2);
2167 }
2168
2169 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2170 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2171 return Ok(None); }
2173 let m1 = c1.half_angle().cos() / s1;
2174 let m2 = c2.half_angle().cos() / s2;
2175 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2176 let d2 = along; let denom = m1 - m2 * sigma;
2179 if denom.abs() < 1e-12 {
2180 if sigma > 0.0 && d2.abs() < 1e-9 {
2183 return Ok(None);
2184 }
2185 return Ok(Some(vec![]));
2186 }
2187
2188 let t_star = (-m2 * sigma * d2) / denom;
2189 let radius = m1 * t_star;
2190 if radius < 1e-12 {
2191 return Ok(Some(vec![])); }
2193
2194 let center = Point3::new(
2195 apex1.x() + axis.x() * t_star,
2196 apex1.y() + axis.y() * t_star,
2197 apex1.z() + axis.z() * t_star,
2198 );
2199 let circle = Circle3D::new(center, axis, radius)?;
2200 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2201}
2202
2203fn offset_parallel_cone_cone(
2214 c1: &ConicalSurface,
2215 c2: &ConicalSurface,
2216) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2217 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2218 return Ok(None); }
2220 let t1 = c1.half_angle().tan();
2221 let t2 = c2.half_angle().tan();
2222 if !t1.is_finite() || !t2.is_finite() {
2223 return Ok(None);
2224 }
2225 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2226 return Ok(None);
2227 }
2228
2229 let w = c1.axis();
2230 let apex1 = c1.apex();
2231 let apex2 = c2.apex();
2232 let delta = apex2 - apex1;
2233 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2234 let s = delta_v.dot(w);
2235 let tm = 0.5 * (t1 + t2);
2236 let k = 1.0 + tm * tm;
2237
2238 let n = (delta_v - w * (k * s)) * 2.0;
2242 let n_len = n.length();
2243 if n_len < 1e-12 {
2244 return Ok(None);
2245 }
2246 let n_hat = n * (1.0 / n_len);
2247 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2248
2249 let axis2 = c2.axis();
2255 let scale = 1.0 + delta_v.length();
2256 let mut out = Vec::new();
2257 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2258 let samples: Vec<Point3> = match &curve {
2259 ExactIntersectionCurve::Circle(c) => (0..4)
2260 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2261 .collect(),
2262 ExactIntersectionCurve::Ellipse(e) => (0..4)
2263 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2264 .collect(),
2265 ExactIntersectionCurve::Points(_) => return Ok(None),
2266 };
2267 let on_real_nappe = |p: &Point3| {
2268 let rel = *p - apex2;
2269 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2270 };
2271 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2272 match hits {
2273 0 => {}
2274 4 => out.push(curve),
2275 _ => return Ok(None),
2276 }
2277 }
2278 Ok(Some(out))
2279}
2280
2281pub fn exact_cone_cylinder(
2301 cone: &ConicalSurface,
2302 cyl: &CylindricalSurface,
2303) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2304 let axis = cone.axis();
2305 let cyl_axis = cyl.axis();
2306
2307 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2309 return Ok(None);
2310 }
2311 let apex = cone.apex();
2312 let delta = apex - cyl.origin();
2313 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2314 let along = delta_v.dot(cyl_axis);
2315 if (delta_v - cyl_axis * along).length() > 1e-8 {
2316 return Ok(None);
2317 }
2318
2319 let s = cone.half_angle().sin();
2320 if s.abs() < 1e-12 {
2321 return Ok(None); }
2323 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2325 return Ok(None); }
2327
2328 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2330 return Ok(Some(vec![])); }
2332 let center = Point3::new(
2333 apex.x() + axis.x() * t_star,
2334 apex.y() + axis.y() * t_star,
2335 apex.z() + axis.z() * t_star,
2336 );
2337 let circle = Circle3D::new(center, axis, cyl.radius())?;
2338 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2339}
2340
2341fn algebraic_cone_cone(
2350 c1: &ConicalSurface,
2351 c2: &ConicalSurface,
2352) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2353 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2354 return Ok(None);
2355 };
2356 let mut curves = Vec::new();
2357 for exact in exacts {
2358 let n_samples = 33;
2359 let mut positions = Vec::with_capacity(n_samples);
2360 let mut points = Vec::with_capacity(n_samples);
2361 #[allow(clippy::cast_precision_loss)]
2362 for i in 0..n_samples {
2363 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2364 let pt = match &exact {
2365 ExactIntersectionCurve::Circle(circle) => {
2366 crate::traits::ParametricCurve::evaluate(circle, theta)
2367 }
2368 ExactIntersectionCurve::Ellipse(ellipse) => {
2369 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2370 }
2371 ExactIntersectionCurve::Points(_) => break,
2372 };
2373 positions.push(pt);
2374 points.push(IntersectionPoint {
2375 point: pt,
2376 param1: (0.0, 0.0),
2377 param2: (0.0, 0.0),
2378 });
2379 }
2380 if positions.is_empty() {
2381 continue;
2382 }
2383 let degree = 3.min(positions.len() - 1);
2384 let curve = interpolate(&positions, degree)?;
2385 curves.push(IntersectionCurve { curve, points });
2386 }
2387 Ok(Some(curves))
2388}
2389
2390pub fn exact_sphere_cylinder(
2410 sphere: &SphericalSurface,
2411 cyl: &CylindricalSurface,
2412) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2413 let sc = sphere.center();
2414 let r_sphere = sphere.radius();
2415 let co = cyl.origin();
2416 let axis = cyl.axis();
2417 let r_cyl = cyl.radius();
2418
2419 let delta = sc - co;
2421 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2422 let along = delta_vec.dot(axis);
2423 let perp_vec = delta_vec - axis * along;
2424 let d_perp = perp_vec.length();
2425
2426 if d_perp > 1e-7 {
2429 return Ok(None);
2430 }
2431
2432 if r_cyl > r_sphere + 1e-10 {
2435 return Ok(Some(vec![]));
2436 }
2437 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2438 if z_sq < 0.0 {
2439 return Ok(Some(vec![]));
2440 }
2441 let z = z_sq.sqrt();
2442
2443 let center_axis_pt = Point3::new(
2446 co.x() + axis.x() * along,
2447 co.y() + axis.y() * along,
2448 co.z() + axis.z() * along,
2449 );
2450
2451 let mut circles = Vec::new();
2452 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2453 for &z_offset in offsets {
2454 let center = Point3::new(
2455 center_axis_pt.x() + axis.x() * z_offset,
2456 center_axis_pt.y() + axis.y() * z_offset,
2457 center_axis_pt.z() + axis.z() * z_offset,
2458 );
2459 let circle = Circle3D::new(center, axis, r_cyl)?;
2460 circles.push(ExactIntersectionCurve::Circle(circle));
2461 }
2462 Ok(Some(circles))
2463}
2464
2465pub fn exact_cone_sphere(
2483 cone: &ConicalSurface,
2484 sphere: &SphericalSurface,
2485) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2486 let offset = cone.apex() - sphere.center();
2487 let along = offset.dot(cone.axis());
2488 if (offset - cone.axis() * along).length() > 1e-7 {
2489 return Ok(None);
2490 }
2491 let lin_tol = Tolerance::new().linear;
2492 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2493 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2494 let b = 2.0 * sin_a * along;
2495 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2496 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2499 if disc < -noise {
2500 return Ok(Some(vec![]));
2501 }
2502 let roots: &[f64] = if far - near < lin_tol {
2503 &[far]
2504 } else {
2505 &[near, far]
2506 };
2507 let mut circles = Vec::new();
2508 for &v in roots {
2509 if v * cos_a > lin_tol {
2510 let centre = cone.apex() + cone.axis() * (v * sin_a);
2511 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2512 circles.push(ExactIntersectionCurve::Circle(circle));
2513 }
2514 }
2515 Ok(Some(circles))
2516}
2517
2518fn algebraic_sphere_cylinder(
2527 sphere: &SphericalSurface,
2528 cyl: &CylindricalSurface,
2529 sphere_first: bool,
2530) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2531 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2532 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2533 };
2534
2535 let mut curves = Vec::new();
2536 for exact in exacts {
2537 let ExactIntersectionCurve::Circle(circle) = exact else {
2538 continue;
2539 };
2540 let n_samples = 33;
2541 let mut points = Vec::with_capacity(n_samples);
2542 let mut positions = Vec::with_capacity(n_samples);
2543 #[allow(clippy::cast_precision_loss)]
2544 for i in 0..n_samples {
2545 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2546 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2547 positions.push(pt);
2548 let (param1, param2) = in_order(
2549 sphere.project_point(pt),
2550 cyl.project_point(pt),
2551 sphere_first,
2552 );
2553 points.push(IntersectionPoint {
2554 point: pt,
2555 param1,
2556 param2,
2557 });
2558 }
2559 let degree = 3.min(positions.len() - 1);
2560 let curve = interpolate(&positions, degree)?;
2561 curves.push(IntersectionCurve { curve, points });
2562 }
2563
2564 Ok(Some(curves))
2565}
2566
2567fn off_axis_sphere_cylinder(
2576 sphere: &SphericalSurface,
2577 cyl: &CylindricalSurface,
2578 sphere_first: bool,
2579) -> Option<Vec<IntersectionCurve>> {
2580 let (centre, radius) = (sphere.center(), sphere.radius());
2581 let axis = cyl.axis();
2582 let offset = centre - cyl.origin();
2583 let axis_distance = (offset - axis * offset.dot(axis)).length();
2584 let lin_tol = Tolerance::new().linear;
2585 if axis_distance > radius + cyl.radius() + lin_tol
2586 || axis_distance + radius < cyl.radius() - lin_tol
2587 {
2588 return Some(Vec::new());
2589 }
2590 let roots = |u: f64| {
2591 let q = cyl.evaluate(u, 0.0) - centre;
2592 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2593 };
2594 let samples = ruling_samples(cyl, &roots);
2595 let loops = if samples.iter().all(Option::is_some) {
2596 closed_ruling_loops(cyl, &roots, &samples)
2597 } else {
2598 partial_ruling_loops(cyl, &roots, &samples)
2599 };
2600 if loops.is_empty() {
2601 return None;
2602 }
2603 Some(fit_ruling_loops(&loops, |p| {
2604 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2605 }))
2606}
2607
2608const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2611 if a_first { (a, b) } else { (b, a) }
2612}
2613
2614#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2628fn algebraic_cylinder_cylinder(
2629 c1: &CylindricalSurface,
2630 c2: &CylindricalSurface,
2631) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2632 let alpha = c1.axis().dot(c2.axis());
2633 let a_coeff = 1.0 - alpha * alpha;
2634
2635 if a_coeff.abs() < 1e-12 {
2637 return Ok(None);
2638 }
2639
2640 let r1 = c1.radius();
2641 let r2 = c2.radius();
2642 let o1 = c1.origin();
2643 let o2 = c2.origin();
2644 let a1 = c1.axis();
2645 let a2 = c2.axis();
2646
2647 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2650 let cross = a1.cross(a2);
2651 let cross_len = cross.length();
2652 if cross_len > 1e-12 {
2653 let axis_dist = delta.dot(cross).abs() / cross_len;
2654 if axis_dist > r1 + r2 + Tolerance::new().linear {
2655 return Ok(Some(vec![])); }
2657 }
2658
2659 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2665 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2666 let alpha = sweep.axis().dot(a);
2667 let quad = 1.0 - alpha * alpha;
2668 let (axis, sweep) = (sweep.axis(), sweep.clone());
2669 move |u: f64| {
2670 let q = sweep.evaluate(u, 0.0) - o;
2671 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2672 let b = 2.0 * (q_a1 - alpha * q_a2);
2673 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2674 ruling_quadratic(quad, b, c)
2675 }
2676 };
2677 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2678 let samples1 = ruling_samples(c1, &roots1);
2679 let loops = if samples1.iter().all(Option::is_some) {
2680 closed_ruling_loops(c1, &roots1, &samples1)
2681 } else {
2682 let samples2 = ruling_samples(c2, &roots2);
2683 if samples2.iter().all(Option::is_some) {
2684 closed_ruling_loops(c2, &roots2, &samples2)
2685 } else if samples1.iter().any(Option::is_some) {
2686 partial_ruling_loops(c1, &roots1, &samples1)
2687 } else {
2688 partial_ruling_loops(c2, &roots2, &samples2)
2689 }
2690 };
2691 if loops.is_empty() {
2692 return Ok(None);
2693 }
2694 Ok(Some(fit_ruling_loops(&loops, |p| {
2695 (c1.project_point(p), c2.project_point(p))
2696 })))
2697}
2698
2699fn ruling_cone_cylinder(
2706 cone: &ConicalSurface,
2707 cyl: &CylindricalSurface,
2708 cone_first: bool,
2709) -> Option<Vec<IntersectionCurve>> {
2710 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2711 if sin_t < 1e-12 || cos_t < 1e-12 {
2712 return None;
2713 }
2714 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2715 let s = 1.0 / (sin_t * sin_t);
2716 let alpha = w.dot(d);
2717 let quad = 1.0 - s * alpha * alpha;
2718 if quad.abs() < 1e-9 {
2719 return None;
2720 }
2721 let roots = |u: f64| {
2722 let delta = cyl.evaluate(u, 0.0) - apex;
2723 let (dd, dw) = (delta.dot(d), delta.dot(w));
2724 let b = 2.0 * (dw - s * dd * alpha);
2725 let c = delta.dot(delta) - s * dd * dd;
2726 ruling_quadratic(quad, b, c)
2727 };
2728 let lin_tol = Tolerance::new().linear;
2729 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2730 #[allow(clippy::cast_precision_loss)]
2731 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2732 let (disc, vp, vm) = roots(u);
2733 disc >= -lin_tol
2734 && [vp, vm]
2735 .iter()
2736 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2737 });
2738 if far_nappe {
2739 return None;
2740 }
2741 let samples = ruling_samples(cyl, &roots);
2742 let scan = WINDOW_SCAN * RULING_SAMPLES;
2747 #[allow(clippy::cast_precision_loss)]
2750 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2751 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2752 let mut k = start;
2753 while k < start + scan {
2754 if !meets(k) {
2755 k += 1;
2756 continue;
2757 }
2758 let first = k;
2759 while k < start + scan && meets(k) {
2760 k += 1;
2761 }
2762 let covered = (first..k)
2763 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2764 .count();
2765 if covered < WINDOW_MIN_SAMPLES {
2766 return None;
2767 }
2768 }
2769 }
2770 let loops = if samples.iter().all(Option::is_some) {
2771 closed_ruling_loops(cyl, &roots, &samples)
2772 } else {
2773 partial_ruling_loops(cyl, &roots, &samples)
2774 };
2775 if loops.is_empty() {
2776 return None;
2777 }
2778 Some(fit_ruling_loops(&loops, |p| {
2779 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2780 }))
2781}
2782
2783fn ruling_torus_cylinder(
2793 torus: &ToroidalSurface,
2794 cyl: &CylindricalSurface,
2795 torus_first: bool,
2796) -> Option<Vec<IntersectionCurve>> {
2797 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2798 || torus.minor_radius() >= torus.major_radius()
2799 {
2800 return None;
2801 }
2802 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2803 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2804 let count = rows[0].len();
2805 let scan = WINDOW_SCAN * RULING_SAMPLES;
2806 #[allow(clippy::cast_precision_loss)]
2807 if count == 0
2808 || count % 2 == 1
2809 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2810 {
2811 return None;
2812 }
2813 let loops: Vec<Vec<Point3>> = (0..count)
2814 .map(|j| {
2815 let mut pts: Vec<Point3> = rows
2816 .iter()
2817 .enumerate()
2818 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2819 .collect();
2820 pts.push(pts[0]);
2821 pts
2822 })
2823 .collect();
2824 Some(fit_ruling_loops(&loops, |p| {
2825 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2826 }))
2827}
2828
2829fn ruling_cone_sphere(
2838 cone: &ConicalSurface,
2839 sphere: &SphericalSurface,
2840 cone_first: bool,
2841) -> Option<Vec<IntersectionCurve>> {
2842 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2843 let offset = apex - centre;
2844 let lin_tol = Tolerance::new().linear;
2845 let along = offset.dot(cone.axis());
2846 let across = (offset - cone.axis() * along).length();
2847 if across < lin_tol {
2848 return None;
2849 }
2850 let k = offset.dot(offset) - radius * radius;
2856 if radius - offset.length() > lin_tol {
2857 let exit = |u: f64| {
2858 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2859 let root = h.mul_add(h, -k).sqrt();
2860 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2861 };
2862 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2867 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2868 .collect();
2869 for _ in 0..10 {
2870 let mut refined = Vec::with_capacity(2 * samples.len());
2871 for pair in samples.windows(2) {
2872 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2873 refined.push(pair[0]);
2874 let um = 0.5 * (u0 + u1);
2875 let pm = exit(um);
2876 let chord = (p1 - p0).length();
2877 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2878 refined.push((um, pm));
2879 }
2880 }
2881 refined.extend(samples.last().copied());
2882 if refined.len() == samples.len() {
2883 break;
2884 }
2885 samples = refined;
2886 }
2887 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2888 if let Some(last) = pts.last_mut() {
2889 *last = samples[0].1;
2890 }
2891 return Some(fit_ruling_loops(&[pts], |p| {
2892 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2893 }));
2894 }
2895 let crossing = |h: f64| {
2899 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2900 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2901 };
2902 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2907 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2908 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2909 {
2910 return window_cone_sphere(cone, sphere, cone_first);
2911 }
2912 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2913 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2914 .collect::<Option<_>>()?;
2915 let loops: Vec<Vec<Point3>> = [0, 1]
2916 .iter()
2917 .map(|&j| {
2918 let mut pts: Vec<Point3> = rows
2919 .iter()
2920 .enumerate()
2921 .map(|(i, &(near, far))| {
2922 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2923 })
2924 .collect();
2925 pts.push(pts[0]);
2926 pts
2927 })
2928 .collect();
2929 Some(fit_ruling_loops(&loops, |p| {
2930 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2931 }))
2932}
2933
2934fn window_cone_sphere(
2949 cone: &ConicalSurface,
2950 sphere: &SphericalSurface,
2951 cone_first: bool,
2952) -> Option<Vec<IntersectionCurve>> {
2953 let offset = cone.apex() - sphere.center();
2954 let lin_tol = Tolerance::new().linear;
2955 if offset.length() - sphere.radius() <= lin_tol {
2956 return None;
2957 }
2958 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2959 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2960 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2961 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2962 if a < lin_tol {
2963 return None;
2964 }
2965 let reach = (-k.sqrt() - c) / a;
2966 if reach <= -1.0 {
2967 return Some(Vec::new());
2968 }
2969 if reach >= 1.0 {
2970 return None;
2971 }
2972 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2973 let half = std::f64::consts::PI - half;
2974 let n = RULING_SAMPLES;
2975 let mut pts: Vec<Point3> = (0..n)
2976 .map(|i| {
2977 #[allow(clippy::cast_precision_loss)]
2978 let theta = TAU * i as f64 / n as f64;
2979 let u = half.mul_add(-theta.cos(), mid);
2980 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2981 let split = h.mul_add(h, -k).max(0.0).sqrt();
2982 cone.evaluate(u, -h - split.copysign(theta.sin()))
2983 })
2984 .collect();
2985 pts.push(pts[0]);
2986 Some(fit_ruling_loops(&[pts], |p| {
2987 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2988 }))
2989}
2990
2991const WINDOW_SCAN: usize = 16;
2994const WINDOW_MIN_SAMPLES: usize = 8;
2995
2996const RULING_SAMPLES: usize = 128;
3000
3001#[allow(clippy::cast_precision_loss)]
3002fn ruling_u(i: usize) -> f64 {
3003 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
3004}
3005
3006fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
3008 let disc = b * b - 4.0 * quad * c;
3009 let root = disc.max(0.0).sqrt();
3010 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
3011}
3012
3013fn ruling_samples(
3017 sweep: &CylindricalSurface,
3018 roots: &impl Fn(f64) -> (f64, f64, f64),
3019) -> Vec<Option<(Point3, Point3)>> {
3020 let lin_tol = Tolerance::new().linear;
3021 (0..RULING_SAMPLES)
3022 .map(|i| {
3023 let u = ruling_u(i);
3024 let (disc, vp, vm) = roots(u);
3025 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
3026 })
3027 .collect()
3028}
3029
3030fn closed_ruling_loops(
3037 sweep: &CylindricalSurface,
3038 roots: &impl Fn(f64) -> (f64, f64, f64),
3039 samples: &[Option<(Point3, Point3)>],
3040) -> Vec<Vec<Point3>> {
3041 let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3042 if let Some((neck, touching)) = narrowest_ruling(roots) {
3043 let count = 2 * RULING_SAMPLES;
3044 #[allow(clippy::cast_precision_loss)]
3045 (0..count)
3046 .map(|i| {
3047 let t = i as f64 / count as f64;
3048 let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3049 let (_, vp, vm) = roots(u);
3050 if i == 0 && touching {
3051 let at = sweep.evaluate(u, 0.5 * (vp + vm));
3052 (at, at)
3053 } else {
3054 (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3055 }
3056 })
3057 .unzip()
3058 } else {
3059 samples.iter().flatten().copied().unzip()
3060 };
3061 plus.push(plus[0]);
3062 minus.push(minus[0]);
3063 vec![plus, minus]
3064}
3065
3066fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3074 let scan = WINDOW_SCAN * RULING_SAMPLES;
3075 #[allow(clippy::cast_precision_loss)]
3076 let step = TAU / scan as f64;
3077 let gap = |u: f64| {
3078 let (_, vp, vm) = roots(u);
3079 (vp - vm).abs()
3080 };
3081 #[allow(clippy::cast_precision_loss)]
3082 let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3083 let widest = gaps.iter().copied().fold(0.0, f64::max);
3084 let tol = Tolerance::new().linear * (1.0 + widest);
3085 let mut necks = Vec::new();
3086 for k in 0..scan {
3087 let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3088 if here > before || here >= after || here >= 0.25 * widest {
3089 continue;
3090 }
3091 #[allow(clippy::cast_precision_loss)]
3092 let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3093 for _ in 0..100 {
3094 let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3095 if gap(a) < gap(b) {
3096 hi = b;
3097 } else {
3098 lo = a;
3099 }
3100 }
3101 let u = 0.5 * (lo + hi);
3102 necks.push((u, gap(u) <= tol));
3103 }
3104 match necks[..] {
3105 [neck] => Some(neck),
3106 _ => None,
3107 }
3108}
3109
3110fn partial_ruling_loops(
3115 sweep: &CylindricalSurface,
3116 roots: &impl Fn(f64) -> (f64, f64, f64),
3117 samples: &[Option<(Point3, Point3)>],
3118) -> Vec<Vec<Point3>> {
3119 let branch_point = |inside: usize, outside: usize| -> Point3 {
3120 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3121 if (hi - lo).abs() > std::f64::consts::PI {
3122 hi += if hi < lo { TAU } else { -TAU };
3123 }
3124 for _ in 0..60 {
3125 let mid = 0.5 * (lo + hi);
3126 if roots(mid).0 >= 0.0 {
3127 lo = mid;
3128 } else {
3129 hi = mid;
3130 }
3131 }
3132 let (_, vp, vm) = roots(lo);
3133 sweep.evaluate(lo, 0.5 * (vp + vm))
3134 };
3135 let Some(first_gap) = samples.iter().position(Option::is_none) else {
3136 return Vec::new();
3137 };
3138 let mut loops = Vec::new();
3139 let mut k = 0;
3140 while k < RULING_SAMPLES {
3141 let i = (first_gap + k) % RULING_SAMPLES;
3142 if samples[i].is_none() {
3143 k += 1;
3144 continue;
3145 }
3146 let start = i;
3147 let mut run = Vec::new();
3148 while k < RULING_SAMPLES {
3149 let j = (first_gap + k) % RULING_SAMPLES;
3150 let Some(pair) = samples[j] else { break };
3151 run.push(pair);
3152 k += 1;
3153 }
3154 let end = (start + run.len() - 1) % RULING_SAMPLES;
3155 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3156 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3157 let mut pts = vec![head];
3158 pts.extend(run.iter().map(|p| p.0));
3159 pts.push(tail);
3160 pts.extend(run.iter().rev().map(|p| p.1));
3161 pts.push(head);
3162 loops.push(pts);
3163 }
3164 loops
3165}
3166
3167fn fit_ruling_loops(
3170 loops: &[Vec<Point3>],
3171 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3172) -> Vec<IntersectionCurve> {
3173 let mut curves = Vec::new();
3174 for pts in loops {
3175 if pts.len() < 4 {
3176 continue;
3177 }
3178 let ipts: Vec<IntersectionPoint> = pts
3179 .iter()
3180 .map(|&p| {
3181 let (param1, param2) = params(p);
3182 IntersectionPoint {
3183 point: p,
3184 param1,
3185 param2,
3186 }
3187 })
3188 .collect();
3189 let degree = 3.min(pts.len() - 1);
3190 if let Ok(curve) = interpolate(pts, degree) {
3191 curves.push(IntersectionCurve {
3192 curve,
3193 points: ipts,
3194 });
3195 }
3196 }
3197 curves
3198}
3199
3200#[allow(clippy::unnecessary_wraps)]
3226fn algebraic_parallel_cone_cylinder(
3227 cone: &ConicalSurface,
3228 cyl: &CylindricalSurface,
3229 v_range_cone: Option<(f64, f64)>,
3230 v_range_cyl: Option<(f64, f64)>,
3231) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3232 let axis = cone.axis();
3233 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3234 return Ok(None); }
3236
3237 let apex = cone.apex();
3238 let delta = cyl.origin() - apex;
3239 let along = delta.dot(axis);
3240 let perp = delta - axis * along;
3241 let d = perp.length();
3242 if d < 1e-9 {
3243 return Ok(None); }
3245
3246 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3247 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3248
3249 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3250 if cos_t < 1e-12 || sin_t < 1e-12 {
3251 return Ok(None);
3252 }
3253 let r = cyl.radius();
3254
3255 let mut v_min = (d - r).abs() / cos_t;
3257 let mut v_max = (d + r) / cos_t;
3258 if v_max <= v_min {
3259 return Ok(Some(vec![]));
3260 }
3261
3262 let mut lo = v_min;
3268 let mut hi = v_max;
3269 if let Some((a, b)) = v_range_cone {
3274 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3275 lo = lo.max(a);
3276 hi = hi.min(b);
3277 }
3278 if let Some((a, b)) = v_range_cyl {
3279 let flip = cyl.axis().dot(axis);
3282 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3283 let (a, b) = (to_cone_v(a), to_cone_v(b));
3284 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3285 lo = lo.max(a);
3286 hi = hi.min(b);
3287 }
3288 let (turn_lo, turn_hi) = (v_min, v_max);
3289 v_min = lo.max(v_min);
3290 v_max = hi.min(v_max);
3291 if v_max - v_min <= 1e-12 {
3292 return Ok(Some(vec![]));
3293 }
3294 let slack = Tolerance::new().linear;
3302 #[allow(clippy::cast_precision_loss)]
3303 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3304 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3305 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3306 .map(|i| {
3307 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3308 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3309 let off = foot - apex;
3310 let across = off - axis * off.dot(axis);
3311 apex + across + axis * (across.length() * sin_t / cos_t)
3312 })
3313 .collect();
3314 pts.push(pts[0]);
3315 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3316 (cone.project_point(p), cyl.project_point(p))
3317 })));
3318 }
3319
3320 let n_samples = 128;
3321 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3322 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3323 #[allow(clippy::cast_precision_loss)]
3324 for i in 0..=n_samples {
3325 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3326 let rho = v * cos_t;
3327 if rho < 1e-12 {
3328 if (d - r).abs() < 1e-12 {
3336 let apex = cone.evaluate(phi0, v);
3337 plus.push(apex);
3338 minus.push(apex);
3339 }
3340 continue;
3341 }
3342 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3343 let alpha = cos_alpha.acos();
3344 plus.push(cone.evaluate(phi0 + alpha, v));
3345 minus.push(cone.evaluate(phi0 - alpha, v));
3346 }
3347
3348 let mut curves = Vec::new();
3349 for pts in [&plus, &minus] {
3350 if pts.len() < 4 {
3353 continue;
3354 }
3355 let ipts: Vec<IntersectionPoint> = pts
3356 .iter()
3357 .map(|&p| IntersectionPoint {
3358 point: p,
3359 param1: cone.project_point(p),
3360 param2: cyl.project_point(p),
3361 })
3362 .collect();
3363 let degree = 3.min(pts.len() - 1);
3364 match interpolate(pts, degree) {
3365 Ok(curve) => curves.push(IntersectionCurve {
3366 curve,
3367 points: ipts,
3368 }),
3369 Err(_) => return Ok(None),
3374 }
3375 }
3376
3377 Ok(Some(curves))
3378}
3379
3380fn algebraic_sphere_sphere(
3388 s1: &SphericalSurface,
3389 s2: &SphericalSurface,
3390) -> Result<Vec<IntersectionCurve>, MathError> {
3391 let c1 = s1.center();
3392 let c2 = s2.center();
3393 let r1 = s1.radius();
3394 let r2 = s2.radius();
3395
3396 let delta = c2 - c1;
3397 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3398 let d = d_sq.sqrt();
3399
3400 if d < 1e-12 {
3401 return Ok(vec![]);
3403 }
3404
3405 if d > r1 + r2 + 1e-10 {
3407 return Ok(vec![]); }
3409 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3410 return Ok(vec![]); }
3412
3413 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3415
3416 let r_circle_sq = r1 * r1 - d1 * d1;
3418 if r_circle_sq < 0.0 {
3419 if r_circle_sq > -1e-10 {
3421 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3423 let tangent_pt = Point3::new(
3424 c1.x() + axis.x() * d1,
3425 c1.y() + axis.y() * d1,
3426 c1.z() + axis.z() * d1,
3427 );
3428 let ipt = IntersectionPoint {
3429 point: tangent_pt,
3430 param1: (0.0, 0.0),
3431 param2: (0.0, 0.0),
3432 };
3433 return Ok(vec![IntersectionCurve {
3435 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3436 points: vec![ipt],
3437 }]);
3438 }
3439 return Ok(vec![]);
3440 }
3441
3442 let r_circle = r_circle_sq.sqrt();
3443 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3444 let center = Point3::new(
3445 c1.x() + axis.x() * d1,
3446 c1.y() + axis.y() * d1,
3447 c1.z() + axis.z() * d1,
3448 );
3449
3450 let basis = Frame3::from_normal(center, axis)?;
3452 let u_dir = basis.x;
3453 let v_dir = basis.y;
3454
3455 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3458 let mut positions = Vec::with_capacity(n_samples);
3459 #[allow(clippy::cast_precision_loss)]
3460 for i in 0..n_samples {
3461 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3462 let (sin_t, cos_t) = theta.sin_cos();
3463 let pt = Point3::new(
3464 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3465 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3466 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3467 );
3468 positions.push(pt);
3469 points.push(IntersectionPoint {
3470 point: pt,
3471 param1: (0.0, 0.0),
3472 param2: (0.0, 0.0),
3473 });
3474 }
3475
3476 let degree = 3.min(positions.len() - 1);
3477 let curve = interpolate(&positions, degree)?;
3478
3479 Ok(vec![IntersectionCurve { curve, points }])
3480}
3481
3482#[allow(clippy::too_many_arguments)]
3488fn correct_to_intersection(
3489 a: &AnalyticSurface<'_>,
3490 b: &AnalyticSurface<'_>,
3491 surf_a: &dyn Fn(f64, f64) -> Point3,
3492 norm_a: &dyn Fn(f64, f64) -> Vec3,
3493 surf_b: &dyn Fn(f64, f64) -> Point3,
3494 norm_b: &dyn Fn(f64, f64) -> Vec3,
3495 point: Point3,
3496 u_range_a: (f64, f64),
3497 v_range_a: (f64, f64),
3498 u_range_b: (f64, f64),
3499 v_range_b: (f64, f64),
3500 max_iters: usize,
3501) -> Point3 {
3502 let mut p = point;
3503 for _ in 0..max_iters {
3504 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3505 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3506 let pa = surf_a(ua, va);
3507 let pb = surf_b(ub, vb);
3508 let na = norm_a(ua, va);
3509 let nb = norm_b(ub, vb);
3510 let pv = Vec3::new(p.x(), p.y(), p.z());
3511
3512 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3513 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3514
3515 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3516 break;
3517 }
3518
3519 let t = na.cross(nb);
3520 let t_len = t.length();
3521 if t_len < 1e-10 {
3522 return Point3::new(
3524 (pa.x() + pb.x()) * 0.5,
3525 (pa.y() + pb.y()) * 0.5,
3526 (pa.z() + pb.z()) * 0.5,
3527 );
3528 }
3529 let t_hat = t * (1.0 / t_len);
3530
3531 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3533 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3534 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3535 if det.abs() < 1e-15 {
3536 return Point3::new(
3537 (pa.x() + pb.x()) * 0.5,
3538 (pa.y() + pb.y()) * 0.5,
3539 (pa.z() + pb.z()) * 0.5,
3540 );
3541 }
3542 let inv = 1.0 / det;
3543 let dx = inv
3545 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3546 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3547 let dy = inv
3548 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3549 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3550 let dz = inv
3551 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3552 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3553 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3554
3555 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3558 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3559 let pc_a = surf_a(uc, vc);
3560 let pc_b = surf_b(ud, vd);
3561 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3562 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3563 .dot(norm_a(uc, vc))
3564 .abs();
3565 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3566 .dot(norm_b(ud, vd))
3567 .abs();
3568 if da_new > da.abs() && db_new > db.abs() {
3569 return p;
3570 }
3571
3572 p = candidate;
3573 }
3574 p
3575}
3576
3577#[allow(clippy::too_many_arguments)]
3583fn march_analytic_intersection(
3584 a: &AnalyticSurface<'_>,
3585 b: &AnalyticSurface<'_>,
3586 surf_a: &dyn Fn(f64, f64) -> Point3,
3587 norm_a: &dyn Fn(f64, f64) -> Vec3,
3588 surf_b: &dyn Fn(f64, f64) -> Point3,
3589 norm_b: &dyn Fn(f64, f64) -> Vec3,
3590 seed: Point3,
3591 u_range_a: (f64, f64),
3592 v_range_a: (f64, f64),
3593 u_range_b: (f64, f64),
3594 v_range_b: (f64, f64),
3595 initial_step: f64,
3596 u_periodic_a: bool,
3597 u_periodic_b: bool,
3598 region: Option<Aabb3>,
3599) -> Vec<Point3> {
3600 let max_steps = 500;
3601 let h_min = 1e-6;
3602 let h_max = initial_step * 4.0;
3603 let closure_dist = initial_step * 5.0;
3607 let max_angle = 10.0_f64.to_radians();
3609 let min_angle = 2.0_f64.to_radians();
3610
3611 let mut forward = Vec::new();
3613 let mut backward = Vec::new();
3615
3616 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3617 let mut current = seed;
3618 let mut h = initial_step;
3619 let mut prev_tangent: Option<Vec3> = None;
3620
3621 for _ in 0..max_steps {
3622 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3623 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3624
3625 let na = norm_a(ua, va);
3626 let nb = norm_b(ub, vb);
3627
3628 let tangent = na.cross(nb);
3629 let t_len = tangent.length();
3630 if t_len < 1e-10 {
3631 break;
3632 }
3633 let t_dir = tangent * (direction / t_len);
3634
3635 if let Some(prev_t) = prev_tangent {
3637 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3638 let angle = cos_angle.acos();
3639 if angle > max_angle && h > h_min {
3640 h = (h * 0.5).max(h_min);
3641 } else if angle < min_angle {
3642 h = (h * 2.0).min(h_max);
3643 }
3644 }
3645 prev_tangent = Some(t_dir);
3646
3647 let next = Point3::new(
3648 h.mul_add(t_dir.x(), current.x()),
3649 h.mul_add(t_dir.y(), current.y()),
3650 h.mul_add(t_dir.z(), current.z()),
3651 );
3652
3653 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3654 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3655
3656 let pa = surf_a(ua2, va2);
3657 let pb = surf_b(ub2, vb2);
3658 let mid = Point3::new(
3659 (pa.x() + pb.x()) * 0.5,
3660 (pa.y() + pb.y()) * 0.5,
3661 (pa.z() + pb.z()) * 0.5,
3662 );
3663 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3664 || va2 <= v_range_a.0
3665 || va2 >= v_range_a.1;
3666 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3667 || vb2 <= v_range_b.0
3668 || vb2 >= v_range_b.1;
3669
3670 if out_a || out_b {
3671 break;
3672 }
3673 if region.is_some_and(|r| !r.contains_point(mid)) {
3674 points.push(mid);
3675 break;
3676 }
3677
3678 let dist_to_seed = (mid - seed).length();
3682 if points.len() > 10 && dist_to_seed < closure_dist {
3683 points.push(seed);
3684 break;
3685 }
3686
3687 points.push(mid);
3688 current = mid;
3689 }
3690 }
3691
3692 backward.reverse();
3694 let mut result = backward;
3695 result.push(seed);
3696 result.append(&mut forward);
3697
3698 for pt in &mut result {
3700 *pt = correct_to_intersection(
3701 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3702 5,
3703 );
3704 }
3705
3706 result
3707}
3708
3709fn project_analytic(
3713 surface: &AnalyticSurface<'_>,
3714 point: Point3,
3715 u_range: (f64, f64),
3716 v_range: (f64, f64),
3717) -> (f64, f64) {
3718 match surface {
3719 AnalyticSurface::Cylinder(cyl) => {
3720 let (u, v) = cyl.project_point(point);
3721 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3722 }
3723 AnalyticSurface::Sphere(sphere) => {
3724 let (u, v) = sphere.project_point(point);
3725 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3726 }
3727 AnalyticSurface::Cone(cone) => {
3728 let (u, v) = cone.project_point(point);
3729 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3730 }
3731 AnalyticSurface::Torus(torus) => {
3732 let (u, v) = torus.project_point(point);
3733 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3734 }
3735 }
3736}
3737
3738fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3742 matches!(
3743 surface,
3744 AnalyticSurface::Cylinder(_)
3745 | AnalyticSurface::Cone(_)
3746 | AnalyticSurface::Sphere(_)
3747 | AnalyticSurface::Torus(_)
3748 )
3749}
3750
3751#[allow(clippy::type_complexity)]
3753fn surface_closures<'a>(
3754 surface: &'a AnalyticSurface<'a>,
3755) -> (
3756 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3757 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3758 (f64, f64),
3759 (f64, f64),
3760) {
3761 match surface {
3762 AnalyticSurface::Cylinder(cyl) => (
3763 Box::new(|u, v| cyl.evaluate(u, v)),
3764 Box::new(|u, v| cyl.normal(u, v)),
3765 (0.0, TAU),
3766 (-1.0, 1.0),
3767 ),
3768 AnalyticSurface::Cone(cone) => (
3769 Box::new(|u, v| cone.evaluate(u, v)),
3770 Box::new(|u, v| cone.normal(u, v)),
3771 (0.0, TAU),
3772 (0.01, 2.0),
3773 ),
3774 AnalyticSurface::Sphere(sphere) => (
3775 Box::new(|u, v| sphere.evaluate(u, v)),
3776 Box::new(|u, v| sphere.normal(u, v)),
3777 (0.0, TAU),
3778 (-FRAC_PI_2, FRAC_PI_2),
3779 ),
3780 AnalyticSurface::Torus(torus) => (
3781 Box::new(|u, v| torus.evaluate(u, v)),
3782 Box::new(|u, v| torus.normal(u, v)),
3783 (0.0, TAU),
3784 (0.0, TAU),
3785 ),
3786 }
3787}
3788
3789#[cfg(test)]
3790#[allow(clippy::unwrap_used, clippy::expect_used)]
3791mod tests {
3792 use super::*;
3793 use crate::tolerance::Tolerance;
3794
3795 #[test]
3799 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3800 let half_angle = 1.1_f64;
3801 let cone = ConicalSurface::new(
3802 Point3::new(0.0, 0.0, 0.0),
3803 Vec3::new(0.0, 0.0, 1.0),
3804 half_angle,
3805 )
3806 .unwrap();
3807 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3808 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3809 let chains =
3810 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3811 .unwrap();
3812 let chain = chains
3813 .iter()
3814 .find_map(|c| match c {
3815 ExactIntersectionCurve::Points(chain) => Some(chain),
3816 _ => None,
3817 })
3818 .expect("a parabola or hyperbola section is sampled");
3819 let (from, to) = (chain[2], chain[chain.len() - 3]);
3820 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3821 .unwrap()
3822 .expect("an exact arc");
3823 let (t0, t1) = arc.domain();
3824 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3825 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3826 for i in 0..=200 {
3827 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3828 let w = q - Point3::new(0.0, 0.0, 0.0);
3829 let off_plane = (normal.dot(w) - d).abs();
3830 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3831 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3832 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3833 }
3834 }
3835 }
3836
3837 #[test]
3841 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3842 let half_angle = 1.1_f64;
3843 let cone = ConicalSurface::new(
3844 Point3::new(0.0, 0.0, 0.0),
3845 Vec3::new(0.0, 0.0, 1.0),
3846 half_angle,
3847 )
3848 .unwrap();
3849 for shortfall in [1e-10, 3e-10, 8e-10] {
3850 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3851 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3852 let chains =
3853 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3854 .unwrap();
3855 let Some(chain) = chains.iter().find_map(|c| match c {
3856 ExactIntersectionCurve::Points(chain) => Some(chain),
3857 _ => None,
3858 }) else {
3859 continue;
3860 };
3861 let (from, to) = (chain[2], chain[chain.len() - 3]);
3862 assert!(
3863 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3864 .unwrap()
3865 .is_none(),
3866 "coincident ends"
3867 );
3868 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3869 continue;
3870 };
3871 let (t0, t1) = arc.domain();
3872 for i in 0..=200 {
3873 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3874 - Point3::new(0.0, 0.0, 0.0);
3875 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3876 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3877 }
3878 }
3879 }
3880
3881 #[test]
3882 fn plane_cylinder_perpendicular() {
3883 let cyl =
3884 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3885 .unwrap();
3886
3887 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3889 assert!(!curves.is_empty(), "should find intersection curve");
3890 assert!(
3891 curves[0].points.len() > 10,
3892 "should have many sample points"
3893 );
3894
3895 let tol = Tolerance::loose();
3896 for pt in &curves[0].points {
3897 assert!(
3898 tol.approx_eq(pt.point.z(), 3.0),
3899 "z should be ~3.0, got {}",
3900 pt.point.z()
3901 );
3902 let r = pt.point.x().hypot(pt.point.y());
3903 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3904 }
3905 }
3906
3907 #[test]
3908 fn plane_sphere_equator() {
3909 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3910
3911 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3912 assert!(!curves.is_empty());
3913
3914 let tol = Tolerance::loose();
3915 for pt in &curves[0].points {
3916 assert!(
3917 tol.approx_eq(pt.point.z(), 0.0),
3918 "z should be ~0, got {}",
3919 pt.point.z()
3920 );
3921 let r = pt.point.x().hypot(pt.point.y());
3922 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3923 }
3924 }
3925
3926 #[test]
3927 fn plane_sphere_no_intersection() {
3928 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3929
3930 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3931 assert!(curves.is_empty());
3932 }
3933
3934 #[test]
3935 fn plane_cone_cross_section() {
3936 let cone = ConicalSurface::new(
3937 Point3::new(0.0, 0.0, 0.0),
3938 Vec3::new(0.0, 0.0, 1.0),
3939 std::f64::consts::FRAC_PI_4,
3940 )
3941 .unwrap();
3942
3943 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3944 assert!(!curves.is_empty(), "should find intersection with cone");
3945 }
3946
3947 #[test]
3954 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3955 let c1 = ConicalSurface::new(
3956 Point3::new(
3957 -16.999_999_999_999_975,
3958 -16.999_999_999_999_975,
3959 5.849_999_999_999_951,
3960 ),
3961 Vec3::new(0.0, 0.0, -1.0),
3962 0.785_398_163_397_433_5,
3963 )
3964 .unwrap();
3965 let c2 = ConicalSurface::new(
3966 Point3::new(
3967 -16.750_000_000_000_036,
3968 -16.750_000_000_000_018,
3969 0.749_999_999_999_881,
3970 ),
3971 Vec3::new(0.0, 0.0, 1.0),
3972 0.785_398_163_397_467_6,
3973 )
3974 .unwrap();
3975
3976 let curves = exact_cone_cone(&c1, &c2)
3977 .unwrap()
3978 .expect("offset parallel equal-angle cones must take the radical-plane path");
3979 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3980 assert!(
3981 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3982 "expected an ellipse section, got {:?}",
3983 curves[0]
3984 );
3985 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3986 return;
3987 };
3988
3989 for i in 0..16 {
3993 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3994 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3995 let rel = p - cone.apex();
3996 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3997 let axial = rel_v.dot(cone.axis());
3998 let radial = (rel_v - cone.axis() * axial).length();
3999 assert!(
4000 axial > 0.0,
4001 "{label}: sample on phantom nappe (axial {axial})"
4002 );
4003 let expect = cone.half_angle().tan() * axial;
4004 assert!(
4005 (radial - expect).abs() < 1e-9,
4006 "{label}: sample off surface by {}",
4007 (radial - expect).abs()
4008 );
4009 }
4010 }
4011 }
4012
4013 #[test]
4017 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
4018 let c1 = ConicalSurface::new(
4019 Point3::new(0.0, 0.0, 5.0),
4020 Vec3::new(0.0, 0.0, -1.0),
4021 std::f64::consts::FRAC_PI_4,
4022 )
4023 .unwrap();
4024 let c2 = ConicalSurface::new(
4025 Point3::new(0.25, 0.25, 20.0),
4026 Vec3::new(0.0, 0.0, 1.0),
4027 std::f64::consts::FRAC_PI_4,
4028 )
4029 .unwrap();
4030 let curves = exact_cone_cone(&c1, &c2)
4031 .unwrap()
4032 .expect("radical-plane path");
4033 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
4034 }
4035
4036 #[test]
4039 fn offset_parallel_cones_with_unequal_angles_defer() {
4040 let c1 = ConicalSurface::new(
4041 Point3::new(0.0, 0.0, 5.0),
4042 Vec3::new(0.0, 0.0, -1.0),
4043 std::f64::consts::FRAC_PI_4,
4044 )
4045 .unwrap();
4046 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4047 .unwrap();
4048 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4049 }
4050
4051 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4055 let cone = ConicalSurface::new(
4056 Point3::new(0.0, 0.0, 0.0),
4057 Vec3::new(0.0, 0.0, 1.0),
4058 std::f64::consts::FRAC_PI_4,
4059 )
4060 .unwrap();
4061 let (s, c) = 40.0_f64.to_radians().sin_cos();
4062 let tube =
4063 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4064 (cone, tube)
4065 }
4066
4067 #[test]
4068 fn marcher_keeps_to_its_region() {
4069 let (cone, tube) = cone_and_tilted_tube();
4070 let run = |region: Option<Aabb3>| {
4071 let (a, b) = (
4072 AnalyticSurface::Cone(&cone),
4073 AnalyticSurface::Cylinder(&tube),
4074 );
4075 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4076 match region {
4077 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4078 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4079 }
4080 .unwrap()
4081 };
4082 assert!(!run(None).is_empty());
4083 let near = Aabb3 {
4084 min: Point3::new(-0.5, -2.0, 0.8),
4085 max: Point3::new(0.7, -0.7, 2.0),
4086 };
4087 let curves = run(Some(near));
4088 assert!(!curves.is_empty(), "the loop through the region is kept");
4089 let reach = near.expanded(0.6);
4091 for curve in &curves {
4092 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4093 }
4094 let away = Aabb3 {
4095 min: Point3::new(5.0, 5.0, 5.0),
4096 max: Point3::new(6.0, 6.0, 6.0),
4097 };
4098 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4099 }
4100
4101 #[test]
4102 fn coaxial_cones_cross_at_single_circle() {
4103 let outer = ConicalSurface::new(
4108 Point3::new(0.0, 0.0, 50.0),
4109 Vec3::new(0.0, 0.0, -1.0),
4110 5.0_f64.atan(),
4111 )
4112 .unwrap();
4113 let inner = ConicalSurface::new(
4114 Point3::new(0.0, 0.0, 90.0),
4115 Vec3::new(0.0, 0.0, -1.0),
4116 10.0_f64.atan(),
4117 )
4118 .unwrap();
4119
4120 let curves = intersect_analytic_analytic_bounded(
4121 AnalyticSurface::Cone(&outer),
4122 AnalyticSurface::Cone(&inner),
4123 32,
4124 None,
4125 None,
4126 )
4127 .unwrap();
4128
4129 assert_eq!(
4130 curves.len(),
4131 1,
4132 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4133 curves.len()
4134 );
4135 for p in &curves[0].points {
4136 let r = p.point.x().hypot(p.point.y());
4137 assert!(
4138 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4139 "intersection point off the expected z=10,r=8 circle: {:?}",
4140 p.point
4141 );
4142 }
4143 }
4144
4145 #[test]
4146 fn plane_torus_cross_section() {
4147 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4148
4149 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4150 assert!(
4151 !curves.is_empty(),
4152 "should find intersection curves with torus"
4153 );
4154 }
4155
4156 #[test]
4159 fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4160 for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4161 let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4162 for z in [3.0 - minor, 3.0 + minor] {
4163 let exact = exact_plane_analytic(
4164 AnalyticSurface::Torus(&torus),
4165 Vec3::new(0.0, 0.0, 1.0),
4166 z,
4167 )
4168 .unwrap();
4169 assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4170 let circle = match &exact[0] {
4171 ExactIntersectionCurve::Circle(c) => Some(c),
4172 _ => None,
4173 };
4174 let c = circle.expect("a circle");
4175 assert!((c.radius() - major).abs() < 1e-12);
4176 assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4177
4178 let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4179 assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4180 }
4181 let single = |normal: Vec3, d: f64| {
4183 matches!(
4184 exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4185 .unwrap()
4186 .as_slice(),
4187 [ExactIntersectionCurve::Circle(_)]
4188 )
4189 };
4190 assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4191 }
4192 }
4193
4194 #[test]
4198 fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4199 let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4200 let level = Vec3::new(0.0, 0.0, 1.0);
4201 let curves =
4202 |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4203 assert!(matches!(
4204 curves(3.0 + 100.1).as_slice(),
4205 [ExactIntersectionCurve::Circle(_)]
4206 ));
4207 let inside = curves(3.0 + 100.1 - 4e-7);
4208 assert!(!matches!(
4209 inside.as_slice(),
4210 [ExactIntersectionCurve::Circle(_)]
4211 ));
4212 }
4213
4214 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4217 let rho = p.x().hypot(p.y());
4218 ((rho - major).hypot(p.z())) - minor
4219 }
4220
4221 #[test]
4227 fn oblique_cone_cylinder_traces_curves_on_both() {
4228 use crate::traits::ParametricCurve;
4229 let cone = ConicalSurface::new(
4233 Point3::new(0.0, 0.0, 3.0),
4234 Vec3::new(0.0, 0.0, -1.0),
4235 2.0_f64.atan(),
4236 )
4237 .unwrap();
4238 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4239 let cyl =
4240 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4241 .unwrap();
4242 for cone_first in [true, false] {
4243 let (a, b) = if cone_first {
4244 (
4245 AnalyticSurface::Cone(&cone),
4246 AnalyticSurface::Cylinder(&cyl),
4247 )
4248 } else {
4249 (
4250 AnalyticSurface::Cylinder(&cyl),
4251 AnalyticSurface::Cone(&cone),
4252 )
4253 };
4254 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4255 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4256 for c in &curves {
4257 let (t0, t1) = c.curve.domain();
4258 for k in 0..=64 {
4259 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4260 let p = ParametricCurve::evaluate(&c.curve, t);
4261 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4264 assert!(
4265 (rod - 0.6).abs() < 1e-4,
4266 "x0 {x0}: off the rod by {}",
4267 rod - 0.6
4268 );
4269 let cone_r = p.x().hypot(p.y());
4270 assert!(
4271 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4272 "x0 {x0}: off the cone at {p:?}"
4273 );
4274 }
4275 }
4276 }
4277 }
4278 }
4279
4280 #[test]
4281 fn a_rod_through_a_rings_tube_traces_four_loops() {
4282 use crate::traits::ParametricCurve;
4283 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4284 let rod =
4287 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4288 .unwrap();
4289 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4290 assert_eq!(curves.len(), 4);
4291 for c in &curves {
4292 let (t0, t1) = c.curve.domain();
4293 for k in 0..=64 {
4294 let p =
4295 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4296 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4297 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4298 assert!(
4299 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4300 "off by {on_rod}, {on_ring}"
4301 );
4302 }
4303 }
4304 let high =
4306 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4307 .unwrap();
4308 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4309 let grazing =
4311 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4312 .unwrap();
4313 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4314 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4316 let thin =
4317 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4318 .unwrap();
4319 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4320 }
4321
4322 #[test]
4323 fn a_pin_through_a_ball_traces_two_loops() {
4324 use crate::traits::ParametricCurve;
4325 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4326 let half = 0.08_f64.atan();
4329 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4330 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4331 for cone_first in [true, false] {
4332 let (a, b) = if cone_first {
4333 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4334 } else {
4335 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4336 };
4337 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4338 assert_eq!(curves.len(), 2, "entry and exit loops");
4339 for c in &curves {
4340 let (t0, t1) = c.curve.domain();
4341 for k in 0..=64 {
4342 let p = ParametricCurve::evaluate(
4343 &c.curve,
4344 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4345 );
4346 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4347 let axial = apex.z() - p.z();
4348 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4349 assert!(
4350 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4351 "off by {on_ball}, {on_pin}"
4352 );
4353 }
4354 }
4355 }
4356 let coaxial =
4361 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4362 .unwrap();
4363 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4364 let aside = ConicalSurface::new(
4365 Point3::new(2.8, 0.0, 10.0),
4366 Vec3::new(0.0, 0.0, -1.0),
4367 FRAC_PI_2 - half,
4368 )
4369 .unwrap();
4370 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4371 let holding = ConicalSurface::new(
4372 Point3::new(1.0, 0.5, 1.0),
4373 Vec3::new(0.0, 0.0, -1.0),
4374 FRAC_PI_2 - half,
4375 )
4376 .unwrap();
4377 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4378 let away = ConicalSurface::new(
4379 Point3::new(1.0, 0.5, 10.0),
4380 Vec3::new(0.0, 0.0, 1.0),
4381 FRAC_PI_2 - half,
4382 )
4383 .unwrap();
4384 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4385 let step = TAU / 2048.0;
4389 let grazed =
4390 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4391 .unwrap();
4392 let wide =
4393 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4394 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4395 }
4396
4397 #[test]
4398 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4399 use crate::traits::ParametricCurve;
4400 let cone = ConicalSurface::new(
4402 Point3::new(0.0, 0.0, 3.0),
4403 Vec3::new(0.0, 0.0, -1.0),
4404 2.0_f64.atan(),
4405 )
4406 .unwrap();
4407 for (centre, radius) in [
4408 (Point3::new(1.0, 0.8, 1.2), 1.1),
4409 (Point3::new(1.5, 0.0, 0.0), 0.8),
4410 ] {
4411 let ball = SphericalSurface::new(centre, radius).unwrap();
4412 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4413 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4414 let (t0, t1) = curves[0].curve.domain();
4415 for k in 0..=64 {
4416 let p = ParametricCurve::evaluate(
4417 &curves[0].curve,
4418 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4419 );
4420 let on_ball = (p - centre).length() - radius;
4421 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4422 assert!(
4423 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4424 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4425 );
4426 }
4427 }
4428 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4430 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4431 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4432 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4433 }
4434
4435 #[test]
4436 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4437 use crate::traits::ParametricCurve;
4438 let cone = ConicalSurface::new(
4439 Point3::new(0.0, 0.0, 3.0),
4440 Vec3::new(0.0, 0.0, -1.0),
4441 2.0_f64.atan(),
4442 )
4443 .unwrap();
4444 for (centre, radius) in [
4447 (Point3::new(0.5, 0.0, 2.5), 2.0),
4448 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4449 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4450 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4451 ] {
4452 let ball = SphericalSurface::new(centre, radius).unwrap();
4453 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4454 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4455 let (t0, t1) = curves[0].curve.domain();
4456 for k in 0..=4096 {
4457 let p = ParametricCurve::evaluate(
4458 &curves[0].curve,
4459 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4460 );
4461 let on_ball = (p - centre).length() - radius;
4462 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4463 assert!(
4464 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4465 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4466 );
4467 }
4468 }
4469 }
4470
4471 #[test]
4472 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4473 let t = 2.0_f64.atan();
4474 let cone =
4475 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4476 let through_apex =
4478 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4479 .unwrap();
4480 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4481 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4483 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4484 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4485 let pin =
4488 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4489 let tube =
4490 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4491 .unwrap();
4492 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4493 }
4494
4495 #[test]
4496 fn parallel_cone_cylinder_gives_two_exact_branches() {
4497 use crate::traits::ParametricCurve;
4498 let cone = ConicalSurface::new(
4499 Point3::new(-5.45, -36.55, -4.85),
4500 Vec3::new(0.0, 0.0, 1.0),
4501 std::f64::consts::FRAC_PI_4,
4502 )
4503 .unwrap();
4504 let cyl = CylindricalSurface::new(
4505 Point3::new(-8.0, -34.0, -5.0),
4506 Vec3::new(0.0, 0.0, 1.0),
4507 4.45,
4508 )
4509 .unwrap();
4510 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4512 let curves = intersect_analytic_analytic_bounded(
4513 AnalyticSurface::Cone(&cone),
4514 AnalyticSurface::Cylinder(&cyl),
4515 32,
4516 Some(v_hint),
4517 Some((0.0, 2.5)),
4518 )
4519 .unwrap();
4520
4521 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4522 for c in &curves {
4523 let (t0, t1) = c.curve.domain();
4524 for k in 0..=32 {
4525 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4526 let p = ParametricCurve::evaluate(&c.curve, t);
4527 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4529 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4530 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4532 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4533 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4535 }
4536 }
4537 }
4538
4539 #[test]
4540 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4541 use crate::traits::ParametricCurve;
4542 let cone = ConicalSurface::new(
4544 Point3::new(0.0, 0.0, 3.0),
4545 Vec3::new(0.0, 0.0, -1.0),
4546 2.0_f64.atan(),
4547 )
4548 .unwrap();
4549 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4551 let rod =
4552 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4553 .unwrap();
4554 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4555 .unwrap()
4556 .unwrap();
4557 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4558 let (t0, t1) = curves[0].curve.domain();
4559 let (first, last) = (
4560 ParametricCurve::evaluate(&curves[0].curve, t0),
4561 ParametricCurve::evaluate(&curves[0].curve, t1),
4562 );
4563 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4564 for k in 0..=64 {
4565 let p = ParametricCurve::evaluate(
4566 &curves[0].curve,
4567 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4568 );
4569 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4570 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4571 assert!(
4572 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4573 "({x}, {y}): off by {on_rod}, {on_cone}"
4574 );
4575 }
4576 }
4577 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4580 let rod =
4581 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4582 .unwrap();
4583 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4584 .unwrap()
4585 .unwrap();
4586 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4587 }
4588 }
4589
4590 #[test]
4593 fn coaxial_cone_cylinder_defers_to_other_paths() {
4594 let cone = ConicalSurface::new(
4595 Point3::new(0.0, 0.0, 0.0),
4596 Vec3::new(0.0, 0.0, 1.0),
4597 std::f64::consts::FRAC_PI_4,
4598 )
4599 .unwrap();
4600 let cyl =
4601 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4602 .unwrap();
4603 assert!(
4604 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4605 .unwrap()
4606 .is_none()
4607 );
4608 }
4609
4610 #[test]
4611 fn oblique_cone_cylinder_defers_to_other_paths() {
4612 let cone = ConicalSurface::new(
4613 Point3::new(0.0, 0.0, 0.0),
4614 Vec3::new(0.0, 0.0, 1.0),
4615 std::f64::consts::FRAC_PI_4,
4616 )
4617 .unwrap();
4618 let cyl =
4619 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4620 .unwrap();
4621 assert!(
4622 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4623 .unwrap()
4624 .is_none()
4625 );
4626 }
4627
4628 #[test]
4629 fn plane_torus_lobe_closes_and_stays_on_surface() {
4630 use crate::traits::ParametricCurve;
4631 let (major, minor) = (10.0, 3.0);
4632 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4633
4634 for (n, d) in [
4638 (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), ] {
4642 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4643 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4644 for c in &curves {
4645 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4646 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4647 assert!(
4648 (p0 - p1).length() < 1e-7,
4649 "lobe not closed: gap={} (n={n:?} d={d})",
4650 (p0 - p1).length()
4651 );
4652 for k in 0..=64 {
4654 let t = f64::from(k) / 64.0;
4655 let p = ParametricCurve::evaluate(&c.curve, t);
4656 assert!(
4657 torus_implicit(p, major, minor).abs() < 1e-2,
4658 "off-surface point {p:?} implicit={}",
4659 torus_implicit(p, major, minor)
4660 );
4661 }
4662 }
4663 }
4664 }
4665
4666 #[test]
4667 fn plane_torus_inner_tangent_figure_eight_stays_open() {
4668 use crate::traits::ParametricCurve;
4669 let (major, minor) = (10.0, 3.0);
4670 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4671
4672 let curves =
4677 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4678 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4679 let max_gap = curves
4680 .iter()
4681 .map(|c| {
4682 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4683 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4684 (p0 - p1).length()
4685 })
4686 .fold(0.0_f64, f64::max);
4687 assert!(
4688 max_gap > 1e-2,
4689 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4690 );
4691 }
4692
4693 #[test]
4698 fn plane_torus_wall_sections_close_into_their_loops() {
4699 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4700 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4701 for k in 1..200 {
4702 let (d, want) = match k.cmp(&100) {
4703 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4705 std::cmp::Ordering::Greater => (
4707 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4708 1,
4709 ),
4710 std::cmp::Ordering::Equal => continue,
4711 };
4712 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4713 let closed = loops
4714 .iter()
4715 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4716 .count();
4717 assert_eq!(
4718 (loops.len(), closed),
4719 (want, want),
4720 "R {major} r {minor}, wall at {d}"
4721 );
4722 }
4723 }
4724 }
4725
4726 #[test]
4730 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4731 let (major, minor) = (4.0, 1.5);
4732 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4733 for tilt in [0.03_f64, 0.08, 0.2] {
4734 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4735 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4736 assert_eq!(curves.len(), 2, "tilt {tilt}");
4737 for c in &curves {
4738 let (t0, t1) = c.curve.domain();
4739 let off = (0..=400)
4740 .map(|k| {
4741 let p = c
4742 .curve
4743 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4744 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4745 })
4746 .fold(0.0_f64, |m, e| m.max(e.abs()));
4747 assert!(
4748 off < 1e-6,
4749 "tilt {tilt}: fitted section {off} off the torus"
4750 );
4751 }
4752 }
4753 }
4754
4755 #[test]
4756 fn line_torus_box_edge_crossing_is_exact() {
4757 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4760 let ts = intersect_line_torus(
4761 &torus,
4762 Point3::new(6.0, -4.0, -5.0),
4763 Vec3::new(0.0, 0.0, 1.0),
4764 );
4765 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4767 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4768 let rho = 6.0_f64.hypot(4.0);
4769 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4770 assert!(
4771 (zs[0] - (-z_exp)).abs() < 1e-9,
4772 "z0={} exp={}",
4773 zs[0],
4774 -z_exp
4775 );
4776 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4777 for &t in &ts {
4779 let p = Point3::new(6.0, -4.0, -5.0 + t);
4780 let rho = p.x().hypot(p.y());
4781 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4782 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4783 }
4784 }
4785
4786 #[test]
4791 fn line_spindle_torus_roots_lie_on_their_tubes() {
4792 let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
4793 let origin = Point3::new(42.36, 18.31, 3.4);
4794 let dir = Vec3::new(
4795 0.573_576_436_351_046,
4796 0.740_535_693_464_567_5,
4797 0.350_889_803_483_932_2,
4798 );
4799 let ts = intersect_line_torus(&torus, origin, dir);
4800 let tube = |t: f64, across: f64| {
4801 let q = origin + dir * t;
4802 ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
4803 };
4804 for &t in &ts {
4805 assert!(
4806 tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
4807 "root {t} off both tubes in {ts:?}"
4808 );
4809 }
4810 for want in [0.124, 0.398] {
4811 assert!(
4812 ts.iter().any(|&t| (t - want).abs() < 1e-3),
4813 "no root near {want} in {ts:?}"
4814 );
4815 }
4816 }
4817
4818 #[test]
4819 fn line_torus_miss_and_tangent() {
4820 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4821 let miss = intersect_line_torus(
4823 &torus,
4824 Point3::new(20.0, 0.0, 0.0),
4825 Vec3::new(0.0, 0.0, 1.0),
4826 );
4827 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4828 let axis =
4830 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4831 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4832 }
4833
4834 #[test]
4835 fn dispatch_via_analytic_surface() {
4836 let cyl =
4837 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4838 .unwrap();
4839 let curves = intersect_plane_analytic(
4840 AnalyticSurface::Cylinder(&cyl),
4841 Vec3::new(0.0, 0.0, 1.0),
4842 0.0,
4843 )
4844 .unwrap();
4845 assert!(!curves.is_empty());
4846 }
4847
4848 #[test]
4849 fn perpendicular_cylinders_intersect() {
4850 let cyl_z =
4851 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4852 .unwrap();
4853 let cyl_x =
4854 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4855 .unwrap();
4856
4857 let curves = intersect_analytic_analytic(
4858 AnalyticSurface::Cylinder(&cyl_z),
4859 AnalyticSurface::Cylinder(&cyl_x),
4860 16,
4861 )
4862 .unwrap();
4863
4864 assert!(
4865 !curves.is_empty(),
4866 "perpendicular cylinders should intersect"
4867 );
4868
4869 for c in &curves {
4870 assert!(
4871 c.points.len() >= 2,
4872 "intersection curve should have >= 2 points, got {}",
4873 c.points.len()
4874 );
4875 }
4876 }
4877
4878 #[test]
4881 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4882 let cyl_z =
4883 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4884 .unwrap();
4885 let cyl_x =
4886 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4887 .unwrap();
4888 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4889 .unwrap()
4890 .unwrap();
4891 assert_eq!(curves.len(), 1);
4892 let curve = &curves[0].curve;
4893 let (t0, t1) = curve.domain();
4894 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4895 let off = |p: Point3| {
4896 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4897 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4898 on_z.max(on_x)
4899 };
4900 let worst = (0..=400)
4901 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4902 .fold(0.0, f64::max);
4903 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4904 }
4905
4906 #[test]
4910 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4911 let cyl_z =
4912 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4913 .unwrap();
4914 let cyl_x =
4915 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4916 .unwrap();
4917 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4918 .unwrap()
4919 .expect("the thin cylinder's sweep finds the loop");
4920 assert_eq!(curves.len(), 1);
4921 }
4922
4923 #[test]
4924 fn sphere_cylinder_intersect() {
4925 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4926 let cyl =
4927 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4928 .unwrap();
4929
4930 let curves = intersect_analytic_analytic(
4931 AnalyticSurface::Sphere(&sphere),
4932 AnalyticSurface::Cylinder(&cyl),
4933 16,
4934 )
4935 .unwrap();
4936
4937 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4941 }
4942
4943 #[test]
4944 fn exact_sphere_cylinder_coaxial_two_circles() {
4945 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4948 let cyl =
4949 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4950 .unwrap();
4951 let circles = exact_sphere_cylinder(&sphere, &cyl)
4952 .unwrap()
4953 .expect("coaxial case returns Some");
4954 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4955 let mut zs: Vec<f64> = circles
4956 .iter()
4957 .filter_map(|c| match c {
4958 ExactIntersectionCurve::Circle(circle) => {
4959 assert!(
4960 (circle.radius() - 3.0).abs() < 1e-9,
4961 "rim radius == cyl radius"
4962 );
4963 Some(circle.center().z())
4964 }
4965 _ => None,
4966 })
4967 .collect();
4968 assert_eq!(zs.len(), 2, "both sections must be exact circles");
4969 zs.sort_by(f64::total_cmp);
4970 let z = 27.0_f64.sqrt();
4971 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4972 }
4973
4974 #[test]
4975 fn exact_sphere_cylinder_non_coaxial_defers() {
4976 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4978 let cyl =
4979 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4980 .unwrap();
4981 assert!(
4982 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4983 "non-coaxial sphere/cylinder defers to the marcher"
4984 );
4985 }
4986
4987 #[test]
4988 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4989 let cone = ConicalSurface::new(
4991 Point3::new(0.0, 0.0, 3.0),
4992 Vec3::new(0.0, 0.0, -1.0),
4993 2.0_f64.atan(),
4994 )
4995 .unwrap();
4996 for (height, radius, count) in [
4997 (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), ] {
5003 let centre = Point3::new(0.0, 0.0, height);
5004 let ball = SphericalSurface::new(centre, radius).unwrap();
5005 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
5006 let circles = circles_of(&curves);
5007 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
5008 for circle in circles {
5009 for k in 0..16 {
5010 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5011 let on_ball = (p - centre).length() - radius;
5012 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
5013 assert!(
5014 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
5015 "ball at {height}: off by {on_ball}, {on_cone}"
5016 );
5017 }
5018 }
5019 }
5020 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
5021 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
5022 let wide =
5025 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
5026 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
5027 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
5028 let circles = circles_of(&curves);
5029 assert_eq!(circles.len(), 1, "the touch");
5030 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
5031 assert!(
5032 (circles[0].radius() - touch).abs() < 1e-3,
5033 "{}",
5034 circles[0].radius()
5035 );
5036 }
5037
5038 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5040 curves
5041 .iter()
5042 .filter_map(|c| match c {
5043 ExactIntersectionCurve::Circle(circle) => Some(circle),
5044 _ => None,
5045 })
5046 .collect()
5047 }
5048
5049 fn worst_off(
5052 circles: &[&Circle3D],
5053 torus: &ToroidalSurface,
5054 other: impl Fn(Point3) -> f64,
5055 ) -> f64 {
5056 let mut worst = 0.0_f64;
5057 for circle in circles {
5058 for k in 0..16 {
5059 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5060 let q = p - torus.center();
5061 let along = q.dot(torus.z_axis());
5062 let rho = (q - torus.z_axis() * along).length();
5063 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5064 worst = worst.max(off).max(other(p).abs());
5065 }
5066 }
5067 worst
5068 }
5069
5070 #[test]
5071 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5072 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5073 for height in [0.0, 1.0] {
5074 let centre = Point3::new(0.0, 0.0, height);
5075 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5076 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5077 let circles = circles_of(&curves);
5078 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5079 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5080 assert!(worst < 1e-9, "height {height}: {worst}");
5081 }
5082 }
5083
5084 #[test]
5085 fn exact_sphere_torus_misses_touches_and_defers() {
5086 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5087 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5088 assert!(
5089 exact_sphere_torus(&ball(0.0, 1.0), &torus)
5090 .unwrap()
5091 .unwrap()
5092 .is_empty(),
5093 "a small ball in the hole misses"
5094 );
5095 assert!(
5096 exact_sphere_torus(&ball(0.0, 2.5), &torus)
5097 .unwrap()
5098 .is_none(),
5099 "a ball touching the inner equator defers"
5100 );
5101 assert!(
5102 exact_sphere_torus(&ball(1.0, 3.0), &torus)
5103 .unwrap()
5104 .is_none(),
5105 "a ball off the axis defers"
5106 );
5107 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5108 Point3::new(0.0, 0.0, 0.0),
5109 1.0,
5110 2.0,
5111 Vec3::new(0.0, 0.0, 1.0),
5112 Vec3::new(1.0, 0.0, 0.0),
5113 )
5114 .unwrap();
5115 assert!(
5116 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5117 .unwrap()
5118 .is_none()
5119 );
5120 }
5121
5122 #[test]
5123 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5124 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5125 let z = Vec3::new(0.0, 0.0, 1.0);
5126 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5127 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5128 let circles = circles_of(&curves);
5129 assert_eq!((curves.len(), circles.len()), (2, 2));
5130 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5131 assert!(worst < 1e-9, "{worst}");
5132 assert!(
5133 exact_cylinder_torus(&rod(2.0), &torus)
5134 .unwrap()
5135 .unwrap()
5136 .is_empty(),
5137 "a rod clear in the hole misses"
5138 );
5139 for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5140 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5141 let circles = circles_of(&curves);
5142 assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5143 assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5144 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5145 assert!(worst < 1e-9, "{label}: {worst}");
5146 }
5147 assert!(
5148 exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5149 .unwrap()
5150 .unwrap()
5151 .is_empty(),
5152 "a wall clear of the tube by more than the tolerance misses it"
5153 );
5154 assert_eq!(
5155 exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5156 .unwrap()
5157 .unwrap()
5158 .len(),
5159 2,
5160 "a wall into the tube by more than the tolerance crosses it twice"
5161 );
5162 for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5166 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5167 assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5168 }
5169 let tilted =
5170 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5171 .unwrap();
5172 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5173 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5174 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5175 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5176 Point3::new(0.0, 0.0, 0.0),
5177 1.0,
5178 2.0,
5179 z,
5180 Vec3::new(1.0, 0.0, 0.0),
5181 )
5182 .unwrap();
5183 assert!(
5184 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5185 "a spindle torus's inner lemon also meets the rod"
5186 );
5187 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5190 Point3::new(0.0, 0.0, 4.7),
5191 0.1,
5192 2.45,
5193 z,
5194 Vec3::new(1.0, 0.0, 0.0),
5195 )
5196 .unwrap();
5197 let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5198 let circles = circles_of(&curves);
5199 assert_eq!(circles.len(), 1);
5200 assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5201 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5202 assert!(worst < 1e-9, "{worst}");
5203 let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5204 let circles = circles_of(&curves);
5205 assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5206 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5207 assert!(worst < 1e-9, "{worst}");
5208 }
5209
5210 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5213 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5214 let cyl =
5215 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5216 .unwrap();
5217 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5218 .unwrap()
5219 .unwrap();
5220 let mut worst: f64 = 0.0;
5221 for c in &curves {
5222 for ip in &c.points {
5223 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5224 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5225 worst = worst
5226 .max((on_sphere - ip.point).length())
5227 .max((on_cylinder - ip.point).length());
5228 }
5229 let (t0, t1) = c.curve.domain();
5230 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5231 for k in 0..=400 {
5232 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5233 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5234 let on_cylinder = ((p.x() - cylinder_origin.x())
5235 .hypot(p.y() - cylinder_origin.y())
5236 - cylinder_radius)
5237 .abs();
5238 worst = worst.max(on_sphere).max(on_cylinder);
5239 }
5240 }
5241 (curves.len(), worst)
5242 }
5243
5244 #[test]
5247 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5248 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5249 assert_eq!(count, 2);
5250 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5251 }
5252
5253 #[test]
5255 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5256 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5257 assert_eq!(count, 1);
5258 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5259 }
5260
5261 #[test]
5262 fn disjoint_cylinders_no_intersection() {
5263 let cyl_a =
5264 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5265 .unwrap();
5266 let cyl_b =
5267 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5268 .unwrap();
5269
5270 let curves = intersect_analytic_analytic(
5271 AnalyticSurface::Cylinder(&cyl_a),
5272 AnalyticSurface::Cylinder(&cyl_b),
5273 16,
5274 )
5275 .unwrap();
5276
5277 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5278 }
5279
5280 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5284 use crate::traits::ParametricCurve;
5285 match curve {
5286 ExactIntersectionCurve::Circle(c) => (0..=64)
5287 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5288 .collect(),
5289 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5290 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5291 .collect(),
5292 ExactIntersectionCurve::Points(pts) => pts.clone(),
5293 }
5294 }
5295
5296 fn assert_on_plane_and_cone(
5299 curves: &[ExactIntersectionCurve],
5300 cone: &ConicalSurface,
5301 n: Vec3,
5302 d: f64,
5303 z_bound: (f64, f64),
5304 ) {
5305 assert!(!curves.is_empty(), "expected at least one section curve");
5306 let mut total = 0;
5307 for curve in curves {
5308 for p in collect_points(curve) {
5309 total += 1;
5310 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5311 assert!(
5312 plane_err < 1e-9,
5313 "point off plane by {plane_err:.2e}: {p:?}"
5314 );
5315 let (u, v) = cone.project_point(p);
5316 let q = cone.evaluate(u, v);
5317 let cone_err =
5318 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5319 .sqrt();
5320 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5321 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5322 assert!(
5323 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5324 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5325 p.z()
5326 );
5327 }
5328 }
5329 assert!(total >= 8, "too few section points ({total})");
5330 }
5331
5332 #[test]
5333 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5334 let cone = ConicalSurface::new(
5338 Point3::new(0.0, 0.0, 0.0),
5339 Vec3::new(0.0, 0.0, 1.0),
5340 std::f64::consts::FRAC_PI_4,
5341 )
5342 .unwrap();
5343 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5344 let d = n.z() * 5.0;
5346 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5347 assert!(
5348 curves
5349 .iter()
5350 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5351 "oblique steep plane × cone must yield an exact Ellipse"
5352 );
5353 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5355 }
5356
5357 #[test]
5358 fn oblique_plane_cone_wrong_nappe_is_empty() {
5359 let cone = ConicalSurface::new(
5363 Point3::new(0.0, 0.0, 0.0),
5364 Vec3::new(0.0, 0.0, 1.0),
5365 std::f64::consts::FRAC_PI_4,
5366 )
5367 .unwrap();
5368 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5369 let d = n.z() * -5.0;
5370 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5371 assert!(
5372 curves.is_empty(),
5373 "plane on the phantom-nappe side must yield no real curve, got {}",
5374 curves.len()
5375 );
5376 }
5377
5378 #[test]
5379 fn oblique_plane_cone_parabola_on_both_single_branch() {
5380 let cone = ConicalSurface::new(
5383 Point3::new(0.0, 0.0, 0.0),
5384 Vec3::new(0.0, 0.0, 1.0),
5385 std::f64::consts::FRAC_PI_4,
5386 )
5387 .unwrap();
5388 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5389 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5391 assert_eq!(
5392 curves.len(),
5393 1,
5394 "a parabola is a single branch, got {}",
5395 curves.len()
5396 );
5397 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5399 }
5400
5401 #[test]
5402 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5403 let cone = ConicalSurface::new(
5411 Point3::new(-59.0, -59.0, 15.85),
5412 Vec3::new(0.0, 0.0, -1.0),
5413 std::f64::consts::FRAC_PI_4,
5414 )
5415 .unwrap();
5416 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5417 let d = -58.360_56;
5418 let cos_theta = n.dot(cone.axis()).abs();
5419 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5420 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5421 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5424 for c in &curves {
5426 assert!(
5427 matches!(c, ExactIntersectionCurve::Points(_)),
5428 "hyperbola must be sampled Points, not a closed conic"
5429 );
5430 }
5431 }
5432
5433 #[test]
5437 fn sphere_sphere_meets_in_a_circle_on_both() {
5438 let ball = |x: f64, y: f64, z: f64, r: f64| {
5439 SphericalSurface::new(Point3::new(x, y, z), r).unwrap()
5440 };
5441 let (a, b) = (ball(0.0, 0.0, 0.0, 2.0), ball(1.6, 0.3, 0.2, 1.5));
5442 let curves = exact_sphere_sphere(&a, &b).unwrap().unwrap();
5443 assert_eq!(curves.len(), 1);
5444 let circle = match &curves[0] {
5445 ExactIntersectionCurve::Circle(circle) => circle,
5446 other => unreachable!("expected a circle, got {other:?}"),
5447 };
5448 for k in 0..16 {
5449 let p = circle.evaluate(std::f64::consts::TAU * f64::from(k) / 16.0);
5450 assert!(((p - a.center()).length() - 2.0).abs() < 1e-12);
5451 assert!(((p - b.center()).length() - 1.5).abs() < 1e-12);
5452 }
5453 assert!(
5454 exact_sphere_sphere(&a, &ball(4.0, 0.0, 0.0, 1.5))
5455 .unwrap()
5456 .unwrap()
5457 .is_empty()
5458 );
5459 assert!(
5460 exact_sphere_sphere(&a, &ball(0.2, 0.0, 0.0, 1.0))
5461 .unwrap()
5462 .unwrap()
5463 .is_empty()
5464 );
5465 assert!(
5466 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 1.0))
5467 .unwrap()
5468 .unwrap()
5469 .is_empty()
5470 );
5471 assert!(
5472 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 2.0))
5473 .unwrap()
5474 .is_none()
5475 );
5476
5477 let radius = |p: &SphericalSurface, q: &SphericalSurface| -> f64 {
5480 match exact_sphere_sphere(p, q).unwrap().unwrap().as_slice() {
5481 [ExactIntersectionCurve::Circle(c)] => c.radius(),
5482 other => unreachable!("expected one circle, got {other:?}"),
5483 }
5484 };
5485 let tiny = ball(2.0, 0.0, 0.0, 1e-5);
5486 assert!((radius(&a, &tiny) - 1e-5).abs() < 1e-9);
5487 assert!((radius(&tiny, &a) - 1e-5).abs() < 1e-9);
5488 let near = radius(&ball(0.0, 0.0, 0.0, 2.0), &ball(3.5, 0.0, 0.0, 1.500_001));
5489 let far = radius(
5490 &ball(1e10, 0.0, 0.0, 2.0),
5491 &ball(1e10 + 3.5, 0.0, 0.0, 1.500_001),
5492 );
5493 assert!(near > 1e-3);
5494 assert!((far - near).abs() < 1e-9 * near.max(1.0));
5495 }
5496}