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)]
1099fn plane_torus_loops(
1100 torus: &ToroidalSurface,
1101 normal: Vec3,
1102 d: f64,
1103 n_v: usize,
1104) -> Vec<Vec<IntersectionPoint>> {
1105 let big_r = torus.major_radius();
1106 let small_r = torus.minor_radius();
1107 let a = normal.dot(torus.x_axis());
1108 let b = normal.dot(torus.y_axis());
1109 let c = normal.dot(torus.z_axis());
1110 let s = a.hypot(b);
1111 let phi = b.atan2(a);
1112 let d_local = d - dot_np(normal, torus.center());
1113 let point = |u: f64, v: f64| IntersectionPoint {
1114 point: torus.evaluate(u, v),
1115 param1: (u, v.rem_euclid(TAU)),
1116 param2: (0.0, 0.0),
1117 };
1118 let closed = |mut run: Vec<IntersectionPoint>| {
1119 run.push(run[0]);
1120 run
1121 };
1122
1123 if s < 1e-12 {
1125 if c.abs() < 1e-12 {
1126 return Vec::new();
1127 }
1128 let sin_v = d_local / (small_r * c);
1129 if sin_v.abs() > 1.0 + 1e-9 {
1130 return Vec::new();
1131 }
1132 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1133 let v1 = std::f64::consts::PI - v0;
1134 let mut vs = vec![v0];
1135 let apart = (v1 - v0).rem_euclid(TAU);
1138 if apart.min(TAU - apart) > 1e-9 {
1139 vs.push(v1);
1140 }
1141 return vs
1142 .into_iter()
1143 .map(|v| {
1144 closed(
1145 (0..n_v)
1146 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1147 .collect(),
1148 )
1149 })
1150 .collect();
1151 }
1152
1153 let step = TAU / (n_v as f64);
1156 let v_off = step * 0.5;
1157 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1159 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1160 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1161 let peak = |lo: f64, hi: f64| {
1163 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1164 let (mut lo, mut hi) = (lo, hi);
1165 for _ in 0..80 {
1166 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1167 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1168 hi = m2;
1169 } else {
1170 lo = m1;
1171 }
1172 }
1173 let v = f64::midpoint(lo, hi);
1174 (v, rhs_at(v).abs())
1175 };
1176 let mut scan: Vec<f64> = Vec::with_capacity(n_v + 2);
1180 for i in 0..n_v {
1181 let v = (i as f64).mul_add(step, v_off);
1182 scan.push(v);
1183 if inside(v) && inside(v + step) {
1184 let (at, top) = peak(v, v + step);
1185 if top > 1.0 + 1e-12 {
1186 scan.push(at);
1187 }
1188 }
1189 }
1190 let n = scan.len();
1191 #[allow(clippy::cast_possible_wrap)]
1193 let scan_v = |i: isize| {
1194 let len = n as isize;
1195 scan[i.rem_euclid(len) as usize] + TAU * (i.div_euclid(len) as f64)
1196 };
1197 let touch_at = |lo: f64, hi: f64| {
1200 let (v, top) = peak(lo, hi);
1201 ((top - 1.0).abs() < 1e-12).then_some(v)
1202 };
1203 let node = |v: f64| {
1205 let u = if rhs_at(v) > 0.0 {
1206 phi
1207 } else {
1208 phi + std::f64::consts::PI
1209 };
1210 point(u, v)
1211 };
1212 let turn = |v_in: f64, v_out: f64| {
1214 let (mut lo, mut hi) = (v_in, v_out);
1215 for _ in 0..60 {
1216 let mid = f64::midpoint(lo, hi);
1217 if inside(mid) {
1218 lo = mid;
1219 } else {
1220 hi = mid;
1221 }
1222 }
1223 lo
1224 };
1225 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1226 #[allow(clippy::cast_possible_wrap)]
1227 if in_scan.iter().all(|&x| x) {
1228 let at_node = (0..n as isize).find_map(|i| touch_at(scan_v(i), scan_v(i + 1)));
1229 return [1.0, -1.0]
1230 .into_iter()
1231 .map(|sign| match at_node {
1232 Some(v_node) => {
1233 let mut run = vec![node(v_node)];
1234 run.extend((1..n_v).map(|i| branch((i as f64).mul_add(step, v_node), sign)));
1235 closed(run)
1236 }
1237 None => closed(scan.iter().map(|&v| branch(v, sign)).collect()),
1238 })
1239 .collect();
1240 }
1241 let Some(first) = (0..n).find(|&i| in_scan[i] && !in_scan[(i + n - 1) % n]) else {
1242 return Vec::new();
1243 };
1244 let mut loops = Vec::new();
1245 let mut k = 0;
1246 while k < n {
1247 let i = (first + k) % n;
1248 if !in_scan[i] {
1249 k += 1;
1250 continue;
1251 }
1252 let len = (0..n - k).take_while(|&j| in_scan[(i + j) % n]).count();
1254 #[allow(clippy::cast_possible_wrap)]
1255 let run_v = |j: isize| scan_v(i as isize + j);
1256 #[allow(clippy::cast_possible_wrap)]
1257 let last = len as isize - 1;
1258 let (t_lo, t_hi) = (
1259 turn(run_v(0), run_v(-1)),
1260 turn(run_v(last), run_v(last + 1)),
1261 );
1262 let at_node = (0..last).find_map(|j| touch_at(run_v(j), run_v(j + 1)));
1263 let (u_lo, u_hi) = (0..=last)
1271 .map(run_v)
1272 .chain([t_lo, t_hi])
1273 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1274 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1275 (lo.min(u), hi.max(u))
1276 });
1277 let m = (len as f64)
1278 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1279 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1280 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1281 .ceil();
1282 let at = |k: f64| {
1283 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1284 (t_hi - t_lo).mul_add(f, t_lo)
1285 };
1286 let steps = m as usize;
1287 if let Some(v_node) = at_node {
1288 for (from, to) in [(v_node, t_lo), (v_node, t_hi)] {
1291 let along = |k: usize| {
1292 let f = 0.5 * (1.0 - (std::f64::consts::PI * k as f64 / m).cos());
1293 (to - from).mul_add(f, from)
1294 };
1295 let mut pts = vec![node(v_node)];
1296 pts.extend((1..=steps).map(|k| branch(along(k), 1.0)));
1297 pts.extend((1..steps).rev().map(|k| branch(along(k), -1.0)));
1298 loops.push(closed(pts));
1299 }
1300 } else {
1301 let mut pts: Vec<IntersectionPoint> =
1302 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1303 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1304 loops.push(closed(pts));
1305 }
1306 k += len;
1307 }
1308 loops
1309}
1310
1311#[allow(clippy::cast_precision_loss)]
1320fn plane_torus_winding_loops(
1321 torus: &ToroidalSurface,
1322 normal: Vec3,
1323 d: f64,
1324 n_v: usize,
1325) -> Option<Vec<Vec<Point3>>> {
1326 let big_r = torus.major_radius();
1327 let small_r = torus.minor_radius();
1328 let a = normal.dot(torus.x_axis());
1329 let b = normal.dot(torus.y_axis());
1330 let c = normal.dot(torus.z_axis());
1331 let s = a.hypot(b);
1332 if s < 1e-12 * normal.length() || small_r >= big_r {
1333 return None;
1334 }
1335 let phi = b.atan2(a);
1336 let d_local = d - dot_np(normal, torus.center());
1337 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1338 let dense = 8 * n_v;
1339 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1340 return None;
1341 }
1342 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1343 for i in 0..n_v {
1344 let v = TAU * i as f64 / n_v as f64;
1345 let delta = rhs(v).acos();
1346 loops[0].push(torus.evaluate(phi + delta, v));
1347 loops[1].push(torus.evaluate(phi - delta, v));
1348 }
1349 Some(
1350 loops
1351 .into_iter()
1352 .map(|mut run| {
1353 run.push(run[0]);
1354 run
1355 })
1356 .collect(),
1357 )
1358}
1359
1360#[must_use]
1374pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1375 let c = torus.center();
1376 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1377 let big_r = torus.major_radius();
1378 let small_r = torus.minor_radius();
1379
1380 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1382 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1383 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1384 let (c0, c1) = (za.dot(o), za.dot(dir));
1385
1386 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1388 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1389 let g0 = a0.mul_add(
1390 a0,
1391 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1392 );
1393
1394 let four_rr = 4.0 * big_r * big_r;
1396 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1397 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1398 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1399
1400 let e4 = g2 * g2;
1402 let e3 = 2.0 * g2 * g1;
1403 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1404 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1405 let e0 = g0.mul_add(g0, -h0);
1406
1407 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1408 let tube = |t: f64, across: bool| -> f64 {
1414 let p = origin + dir * t;
1415 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1416 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1417 let centre = if across { -big_r } else { big_r };
1418 (a.hypot(b) - centre).hypot(cc) - small_r
1419 };
1420 for t in &mut roots {
1421 let across = big_r < small_r && tube(*t, true).abs() < tube(*t, false).abs();
1422 for _ in 0..8 {
1423 let eps = 1e-7;
1424 let f = tube(*t, across);
1425 let df = (tube(*t + eps, across) - tube(*t - eps, across)) / (2.0 * eps);
1426 if df.abs() <= 1e-12 {
1427 break;
1428 }
1429 let step = f / df;
1430 *t -= step;
1431 if step.abs() < 1e-14 * (1.0 + t.abs()) {
1432 break;
1433 }
1434 }
1435 }
1436 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1437 roots.dedup_by(|a, b| (*a - *b).abs() < 1e-9 * (1.0 + b.abs()));
1438 roots
1439}
1440
1441fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1444 if c4.abs() < 1e-14 {
1446 return real_roots_cubic(c3, c2, c1, c0);
1447 }
1448 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1450 let eval = |z: Complex| -> Complex {
1451 let mut acc = Complex::new(1.0, 0.0);
1453 acc = acc * z + Complex::new(a, 0.0);
1454 acc = acc * z + Complex::new(b, 0.0);
1455 acc = acc * z + Complex::new(c, 0.0);
1456 acc * z + Complex::new(d, 0.0)
1457 };
1458 let seed = Complex::new(0.4, 0.9);
1460 let mut r = [
1461 Complex::new(1.0, 0.0),
1462 seed,
1463 seed * seed,
1464 seed * seed * seed,
1465 ];
1466 for _ in 0..100 {
1467 let mut max_step = 0.0_f64;
1468 for i in 0..4 {
1469 let mut denom = Complex::new(1.0, 0.0);
1470 for j in 0..4 {
1471 if i != j {
1472 denom = denom * (r[i] - r[j]);
1473 }
1474 }
1475 if denom.norm() < 1e-300 {
1476 continue;
1477 }
1478 let step = eval(r[i]) / denom;
1479 r[i] = r[i] - step;
1480 max_step = max_step.max(step.norm());
1481 }
1482 if max_step < 1e-14 {
1483 break;
1484 }
1485 }
1486 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1493 let mut out: Vec<f64> = Vec::new();
1494 for z in r {
1495 if z.im.abs() >= 1e-7 {
1496 continue;
1497 }
1498 let x = z.re;
1499 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1502 if p_real(x).abs() > 1e-6 * scale {
1503 continue;
1504 }
1505 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1506 continue;
1507 }
1508 out.push(x);
1509 }
1510 out
1511}
1512
1513fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1515 if a.abs() < 1e-14 {
1516 return real_roots_quadratic(b, c, d);
1517 }
1518 let (b, c, d) = (b / a, c / a, d / a);
1520 let p = c - b * b / 3.0;
1521 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1522 let shift = -b / 3.0;
1523 let disc = q * q / 4.0 + p * p * p / 27.0;
1524 if disc > 1e-14 {
1525 let sq = disc.sqrt();
1526 let u = (-q / 2.0 + sq).cbrt();
1527 let v = (-q / 2.0 - sq).cbrt();
1528 vec![u + v + shift]
1529 } else if disc < -1e-14 {
1530 let m = 2.0 * (-p / 3.0).sqrt();
1532 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1533 (0..3)
1534 .map(|k| {
1535 m.mul_add(
1536 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1537 shift,
1538 )
1539 })
1540 .collect()
1541 } else {
1542 let u = (-q / 2.0).cbrt();
1544 vec![2.0 * u + shift, -u + shift]
1545 }
1546}
1547
1548fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1550 if a.abs() < 1e-14 {
1551 if b.abs() < 1e-14 {
1552 return Vec::new();
1553 }
1554 return vec![-c / b];
1555 }
1556 let disc = b * b - 4.0 * a * c;
1557 if disc < 0.0 {
1558 Vec::new()
1559 } else {
1560 let sq = disc.sqrt();
1561 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1562 }
1563}
1564
1565#[derive(Clone, Copy)]
1567struct Complex {
1568 re: f64,
1569 im: f64,
1570}
1571
1572impl Complex {
1573 const fn new(re: f64, im: f64) -> Self {
1574 Self { re, im }
1575 }
1576 fn norm(self) -> f64 {
1577 self.re.hypot(self.im)
1578 }
1579}
1580
1581impl std::ops::Add for Complex {
1582 type Output = Self;
1583 fn add(self, o: Self) -> Self {
1584 Self::new(self.re + o.re, self.im + o.im)
1585 }
1586}
1587
1588impl std::ops::Sub for Complex {
1589 type Output = Self;
1590 fn sub(self, o: Self) -> Self {
1591 Self::new(self.re - o.re, self.im - o.im)
1592 }
1593}
1594
1595impl std::ops::Mul for Complex {
1596 type Output = Self;
1597 fn mul(self, o: Self) -> Self {
1598 Self::new(
1599 self.re.mul_add(o.re, -(self.im * o.im)),
1600 self.re.mul_add(o.im, self.im * o.re),
1601 )
1602 }
1603}
1604
1605impl std::ops::Div for Complex {
1606 type Output = Self;
1607 fn div(self, o: Self) -> Self {
1608 let den = o.re.mul_add(o.re, o.im * o.im);
1609 Self::new(
1610 self.re.mul_add(o.re, self.im * o.im) / den,
1611 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1612 )
1613 }
1614}
1615
1616fn build_curves_from_points(
1620 points_3d: &[Point3],
1621 ipoints: Vec<IntersectionPoint>,
1622) -> Result<Vec<IntersectionCurve>, MathError> {
1623 if points_3d.len() < 2 {
1624 return Ok(vec![]);
1625 }
1626
1627 let degree = 3.min(points_3d.len() - 1);
1628 let curve = interpolate(points_3d, degree)?;
1629 Ok(vec![IntersectionCurve {
1630 curve,
1631 points: ipoints,
1632 }])
1633}
1634
1635#[allow(
1647 clippy::cast_precision_loss,
1648 clippy::too_many_lines,
1649 clippy::similar_names,
1650 clippy::unnecessary_wraps,
1651 clippy::type_complexity
1652)]
1653pub fn intersect_analytic_analytic(
1654 a: AnalyticSurface<'_>,
1655 b: AnalyticSurface<'_>,
1656 grid_res: usize,
1657) -> Result<Vec<IntersectionCurve>, MathError> {
1658 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1659}
1660
1661pub fn intersect_analytic_analytic_bounded(
1672 a: AnalyticSurface<'_>,
1673 b: AnalyticSurface<'_>,
1674 grid_res: usize,
1675 v_range_hint_a: Option<(f64, f64)>,
1676 v_range_hint_b: Option<(f64, f64)>,
1677) -> Result<Vec<IntersectionCurve>, MathError> {
1678 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1679}
1680
1681pub fn intersect_analytic_analytic_in_region(
1694 a: AnalyticSurface<'_>,
1695 b: AnalyticSurface<'_>,
1696 grid_res: usize,
1697 v_range_hint_a: Option<(f64, f64)>,
1698 v_range_hint_b: Option<(f64, f64)>,
1699 region: Aabb3,
1700) -> Result<Vec<IntersectionCurve>, MathError> {
1701 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1702}
1703
1704fn intersect_analytic_analytic_impl(
1705 a: AnalyticSurface<'_>,
1706 b: AnalyticSurface<'_>,
1707 grid_res: usize,
1708 v_range_hint_a: Option<(f64, f64)>,
1709 v_range_hint_b: Option<(f64, f64)>,
1710 region: Option<Aabb3>,
1711) -> Result<Vec<IntersectionCurve>, MathError> {
1712 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1715 return Ok(result);
1716 }
1717
1718 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1719 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1720 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1721 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1722
1723 let diag_a = {
1725 let p00 = surf_a(u_range_a.0, v_range_a.0);
1726 let p11 = surf_a(u_range_a.1, v_range_a.1);
1727 (p00 - p11).length()
1728 };
1729 let diag_b = {
1730 let p00 = surf_b(u_range_b.0, v_range_b.0);
1731 let p11 = surf_b(u_range_b.1, v_range_b.1);
1732 (p00 - p11).length()
1733 };
1734 let char_size = diag_a.min(diag_b).max(0.1);
1735
1736 #[allow(clippy::type_complexity)]
1740 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1741 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1745 let mut min_dist = f64::INFINITY;
1746
1747 #[allow(clippy::cast_precision_loss)]
1748 for ia in 0..grid_res {
1749 for ja in 0..grid_res {
1750 let ua =
1751 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1752 let va =
1753 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1754
1755 let pa = surf_a(ua, va);
1756
1757 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1759 let pb = surf_b(ub, vb);
1760 let dist = (pa - pb).length();
1761 min_dist = min_dist.min(dist);
1762
1763 if dist < seed_threshold {
1764 let mid = Point3::new(
1769 (pa.x() + pb.x()) * 0.5,
1770 (pa.y() + pb.y()) * 0.5,
1771 (pa.z() + pb.z()) * 0.5,
1772 );
1773 seeds.push((mid, (ua, va), (ub, vb)));
1774 }
1775 }
1776 }
1777
1778 let reject_dist = (char_size / grid_res as f64) * 3.0;
1787 if min_dist > reject_dist {
1788 return Ok(vec![]);
1789 }
1790
1791 if seeds.is_empty() {
1792 return Ok(vec![]);
1793 }
1794
1795 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1799 let dedup_radius = march_step * 10.0;
1800 let mut unique_seeds = Vec::new();
1801 for seed in &seeds {
1802 let dominated = unique_seeds
1803 .iter()
1804 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1805 if !dominated {
1806 unique_seeds.push(*seed);
1807 }
1808 }
1809
1810 let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1815 if let Some(r) = region {
1816 for seed in &mut unique_seeds {
1817 let mut p = seed.0;
1818 for _ in 0..8 {
1819 let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1820 let pa = surf_a(ua, va);
1821 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1822 let pb = surf_b(ub, vb);
1823 p = Point3::new(
1824 (pa.x() + pb.x()) * 0.5,
1825 (pa.y() + pb.y()) * 0.5,
1826 (pa.z() + pb.z()) * 0.5,
1827 );
1828 if (pa - pb).length() < 1e-9 {
1829 break;
1830 }
1831 }
1832 seed.0 = p;
1833 }
1834 unique_seeds.retain(|seed| r.contains_point(seed.0));
1835 }
1836
1837 let mut curves = Vec::new();
1839 let mut used_seeds = vec![false; unique_seeds.len()];
1840
1841 for si in 0..unique_seeds.len() {
1842 if used_seeds[si] {
1843 continue;
1844 }
1845 used_seeds[si] = true;
1846
1847 let march_result = march_analytic_intersection(
1848 &a,
1849 &b,
1850 surf_a.as_ref(),
1851 norm_a.as_ref(),
1852 surf_b.as_ref(),
1853 norm_b.as_ref(),
1854 unique_seeds[si].0,
1855 u_range_a,
1856 v_range_a,
1857 u_range_b,
1858 v_range_b,
1859 march_step,
1860 is_u_periodic(&a),
1861 is_u_periodic(&b),
1862 region,
1863 );
1864
1865 if march_result.len() >= 2 {
1866 for (sj, other) in unique_seeds.iter().enumerate() {
1867 if !used_seeds[sj]
1868 && march_result
1869 .iter()
1870 .any(|p| (*p - other.0).length() < dedup_radius)
1871 {
1872 used_seeds[sj] = true;
1873 }
1874 }
1875
1876 let ipts: Vec<IntersectionPoint> = march_result
1877 .iter()
1878 .map(|&pt| IntersectionPoint {
1879 point: pt,
1880 param1: (0.0, 0.0),
1881 param2: (0.0, 0.0),
1882 })
1883 .collect();
1884
1885 let degree = 3.min(march_result.len() - 1);
1886 if let Ok(curve) = interpolate(&march_result, degree) {
1887 curves.push(IntersectionCurve {
1888 curve,
1889 points: ipts,
1890 });
1891 }
1892 }
1893 }
1894
1895 Ok(curves)
1896}
1897
1898#[allow(clippy::too_many_lines)]
1912fn try_algebraic_intersection(
1913 a: &AnalyticSurface<'_>,
1914 b: &AnalyticSurface<'_>,
1915 v_range_a: Option<(f64, f64)>,
1916 v_range_b: Option<(f64, f64)>,
1917) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1918 match (a, b) {
1919 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1920 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1921 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1922 ),
1923 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1924 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1925 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1926 ),
1927 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1928 algebraic_sphere_sphere(s1, s2).map(Some)
1929 }
1930 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1931 let axis_dot = c1.axis().dot(c2.axis()).abs();
1932 if axis_dot > 1.0 - 1e-10 {
1933 let delta = c2.origin() - c1.origin();
1935 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1936 let along = delta_vec.dot(c1.axis());
1937 let perp = (delta_vec - c1.axis() * along).length();
1938 if perp < 1e-8 {
1939 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1942 return Ok(None); }
1944 return Ok(Some(vec![])); }
1946 }
1947 algebraic_cylinder_cylinder(c1, c2)
1949 }
1950 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1952 algebraic_sphere_cylinder(s, c, true)
1953 }
1954 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1955 algebraic_sphere_cylinder(s, c, false)
1956 }
1957 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1958 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1959 Ok(ruling_cone_sphere(cone, sphere, true))
1960 }
1961 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1962 Ok(ruling_cone_sphere(cone, sphere, false))
1963 }
1964 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1965 Ok(parallel_axis_torus_cylinder(t, c, true)
1966 .or_else(|| ruling_torus_cylinder(t, c, true)))
1967 }
1968 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1969 Ok(parallel_axis_torus_cylinder(t, c, false)
1970 .or_else(|| ruling_torus_cylinder(t, c, false)))
1971 }
1972 _ => Ok(None),
1973 }
1974}
1975
1976fn parallel_axis_torus_cylinder(
1983 torus: &ToroidalSurface,
1984 cyl: &CylindricalSurface,
1985 torus_first: bool,
1986) -> Option<Vec<IntersectionCurve>> {
1987 let axis = torus.z_axis();
1988 let along = cyl.axis().dot(axis);
1989 if along.abs() < 1.0 - 1e-10 {
1990 return None;
1991 }
1992 let offset = cyl.origin() - torus.center();
1993 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1994 return None;
1995 }
1996 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1997 let roots = |u: f64| {
1998 let q = cyl.evaluate(u, 0.0) - torus.center();
1999 let height = q.dot(axis);
2000 let rho = (q - axis * height).length();
2001 let reach = minor * minor - (rho - major) * (rho - major);
2002 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
2003 };
2004 let samples = ruling_samples(cyl, &roots);
2005 let loops = if samples.iter().all(Option::is_some) {
2006 closed_ruling_loops(cyl, &roots, &samples)
2007 } else {
2008 partial_ruling_loops(cyl, &roots, &samples)
2009 };
2010 if loops.is_empty() {
2011 return None;
2012 }
2013 Some(fit_ruling_loops(&loops, |p| {
2014 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2015 }))
2016}
2017
2018fn meridian_crossings(
2024 first: (f64, f64, f64),
2025 second: (f64, f64, f64),
2026 scale: f64,
2027) -> Option<Vec<(f64, f64)>> {
2028 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
2029 let (dx, dz) = (x2 - x1, z2 - z1);
2030 let dist = dx.hypot(dz);
2031 let slack = 1e-9 * scale;
2032 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
2033 return None;
2034 }
2035 if dist > r1 + r2 || dist < (r1 - r2).abs() {
2036 return Some(Vec::new());
2037 }
2038 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
2039 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
2040 let (ux, uz) = (dx / dist, dz / dist);
2041 let mut crossings = Vec::with_capacity(2);
2042 for side in [1.0, -1.0] {
2043 let rho = x1 + along * ux - side * across * uz;
2044 if rho <= slack {
2045 return None;
2046 }
2047 crossings.push((rho, z1 + along * uz + side * across * ux));
2048 }
2049 Some(crossings)
2050}
2051
2052fn circles_about_axis(
2054 base: Point3,
2055 axis: Vec3,
2056 crossings: &[(f64, f64)],
2057) -> Result<Vec<ExactIntersectionCurve>, MathError> {
2058 crossings
2059 .iter()
2060 .map(|&(rho, z)| {
2061 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
2062 })
2063 .collect()
2064}
2065
2066pub fn exact_torus_torus(
2077 first: &ToroidalSurface,
2078 second: &ToroidalSurface,
2079) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2080 let axis = first.z_axis();
2081 let scale = first.major_radius() + second.major_radius();
2082 let offset = second.center() - first.center();
2083 if first.minor_radius() >= first.major_radius()
2085 || second.minor_radius() >= second.major_radius()
2086 || axis.cross(second.z_axis()).length() > 1e-9
2087 || offset.cross(axis).length() > 1e-9 * scale
2088 {
2089 return Ok(None);
2090 }
2091 let Some(crossings) = meridian_crossings(
2092 (first.major_radius(), 0.0, first.minor_radius()),
2093 (
2094 second.major_radius(),
2095 offset.dot(axis),
2096 second.minor_radius(),
2097 ),
2098 scale,
2099 ) else {
2100 return Ok(None);
2101 };
2102 circles_about_axis(first.center(), axis, &crossings).map(Some)
2103}
2104
2105pub fn exact_cylinder_torus(
2119 cylinder: &CylindricalSurface,
2120 torus: &ToroidalSurface,
2121) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2122 let axis = torus.z_axis();
2123 let scale = torus.major_radius() + cylinder.radius();
2124 let offset = cylinder.origin() - torus.center();
2125 let linear = crate::tolerance::Tolerance::default().linear;
2126 if axis.cross(cylinder.axis()).length() > 1e-9
2127 || offset.cross(axis).length() > 1e-9 * scale
2128 || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2129 {
2130 return Ok(None);
2131 }
2132 let gap = cylinder.radius() - torus.major_radius();
2133 let small = torus.minor_radius();
2134 if (gap.abs() - small).abs() <= linear {
2138 return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2139 }
2140 if gap.abs() > small {
2141 return Ok(Some(Vec::new()));
2142 }
2143 let height = small.mul_add(small, -(gap * gap)).sqrt();
2144 circles_about_axis(
2145 torus.center(),
2146 axis,
2147 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2148 )
2149 .map(Some)
2150}
2151
2152pub fn exact_sphere_torus(
2165 sphere: &SphericalSurface,
2166 torus: &ToroidalSurface,
2167) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2168 let axis = torus.z_axis();
2169 let scale = torus.major_radius() + sphere.radius();
2170 let offset = sphere.center() - torus.center();
2171 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2173 return Ok(None);
2174 }
2175 let Some(crossings) = meridian_crossings(
2176 (0.0, offset.dot(axis), sphere.radius()),
2177 (torus.major_radius(), 0.0, torus.minor_radius()),
2178 scale,
2179 ) else {
2180 return Ok(None);
2181 };
2182 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2183}
2184
2185pub fn exact_cone_cone(
2210 c1: &ConicalSurface,
2211 c2: &ConicalSurface,
2212) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2213 let axis = c1.axis();
2214 let axis2 = c2.axis();
2215
2216 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2218 return Ok(None); }
2220 let apex1 = c1.apex();
2221 let apex2 = c2.apex();
2222 let delta = apex2 - apex1;
2223 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2224 let along = delta_v.dot(axis);
2225 if (delta_v - axis * along).length() > 1e-8 {
2226 return offset_parallel_cone_cone(c1, c2);
2227 }
2228
2229 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2230 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2231 return Ok(None); }
2233 let m1 = c1.half_angle().cos() / s1;
2234 let m2 = c2.half_angle().cos() / s2;
2235 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2236 let d2 = along; let denom = m1 - m2 * sigma;
2239 if denom.abs() < 1e-12 {
2240 if sigma > 0.0 && d2.abs() < 1e-9 {
2243 return Ok(None);
2244 }
2245 return Ok(Some(vec![]));
2246 }
2247
2248 let t_star = (-m2 * sigma * d2) / denom;
2249 let radius = m1 * t_star;
2250 if radius < 1e-12 {
2251 return Ok(Some(vec![])); }
2253
2254 let center = Point3::new(
2255 apex1.x() + axis.x() * t_star,
2256 apex1.y() + axis.y() * t_star,
2257 apex1.z() + axis.z() * t_star,
2258 );
2259 let circle = Circle3D::new(center, axis, radius)?;
2260 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2261}
2262
2263fn offset_parallel_cone_cone(
2274 c1: &ConicalSurface,
2275 c2: &ConicalSurface,
2276) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2277 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2278 return Ok(None); }
2280 let t1 = c1.half_angle().tan();
2281 let t2 = c2.half_angle().tan();
2282 if !t1.is_finite() || !t2.is_finite() {
2283 return Ok(None);
2284 }
2285 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2286 return Ok(None);
2287 }
2288
2289 let w = c1.axis();
2290 let apex1 = c1.apex();
2291 let apex2 = c2.apex();
2292 let delta = apex2 - apex1;
2293 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2294 let s = delta_v.dot(w);
2295 let tm = 0.5 * (t1 + t2);
2296 let k = 1.0 + tm * tm;
2297
2298 let n = (delta_v - w * (k * s)) * 2.0;
2302 let n_len = n.length();
2303 if n_len < 1e-12 {
2304 return Ok(None);
2305 }
2306 let n_hat = n * (1.0 / n_len);
2307 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2308
2309 let axis2 = c2.axis();
2315 let scale = 1.0 + delta_v.length();
2316 let mut out = Vec::new();
2317 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2318 let samples: Vec<Point3> = match &curve {
2319 ExactIntersectionCurve::Circle(c) => (0..4)
2320 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2321 .collect(),
2322 ExactIntersectionCurve::Ellipse(e) => (0..4)
2323 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2324 .collect(),
2325 ExactIntersectionCurve::Points(_) => return Ok(None),
2326 };
2327 let on_real_nappe = |p: &Point3| {
2328 let rel = *p - apex2;
2329 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2330 };
2331 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2332 match hits {
2333 0 => {}
2334 4 => out.push(curve),
2335 _ => return Ok(None),
2336 }
2337 }
2338 Ok(Some(out))
2339}
2340
2341pub fn exact_cone_cylinder(
2361 cone: &ConicalSurface,
2362 cyl: &CylindricalSurface,
2363) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2364 let axis = cone.axis();
2365 let cyl_axis = cyl.axis();
2366
2367 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2369 return Ok(None);
2370 }
2371 let apex = cone.apex();
2372 let delta = apex - cyl.origin();
2373 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2374 let along = delta_v.dot(cyl_axis);
2375 if (delta_v - cyl_axis * along).length() > 1e-8 {
2376 return Ok(None);
2377 }
2378
2379 let s = cone.half_angle().sin();
2380 if s.abs() < 1e-12 {
2381 return Ok(None); }
2383 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2385 return Ok(None); }
2387
2388 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2390 return Ok(Some(vec![])); }
2392 let center = Point3::new(
2393 apex.x() + axis.x() * t_star,
2394 apex.y() + axis.y() * t_star,
2395 apex.z() + axis.z() * t_star,
2396 );
2397 let circle = Circle3D::new(center, axis, cyl.radius())?;
2398 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2399}
2400
2401fn algebraic_cone_cone(
2410 c1: &ConicalSurface,
2411 c2: &ConicalSurface,
2412) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2413 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2414 return Ok(None);
2415 };
2416 let mut curves = Vec::new();
2417 for exact in exacts {
2418 let n_samples = 33;
2419 let mut positions = Vec::with_capacity(n_samples);
2420 let mut points = Vec::with_capacity(n_samples);
2421 #[allow(clippy::cast_precision_loss)]
2422 for i in 0..n_samples {
2423 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2424 let pt = match &exact {
2425 ExactIntersectionCurve::Circle(circle) => {
2426 crate::traits::ParametricCurve::evaluate(circle, theta)
2427 }
2428 ExactIntersectionCurve::Ellipse(ellipse) => {
2429 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2430 }
2431 ExactIntersectionCurve::Points(_) => break,
2432 };
2433 positions.push(pt);
2434 points.push(IntersectionPoint {
2435 point: pt,
2436 param1: (0.0, 0.0),
2437 param2: (0.0, 0.0),
2438 });
2439 }
2440 if positions.is_empty() {
2441 continue;
2442 }
2443 let degree = 3.min(positions.len() - 1);
2444 let curve = interpolate(&positions, degree)?;
2445 curves.push(IntersectionCurve { curve, points });
2446 }
2447 Ok(Some(curves))
2448}
2449
2450pub fn exact_sphere_cylinder(
2470 sphere: &SphericalSurface,
2471 cyl: &CylindricalSurface,
2472) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2473 let sc = sphere.center();
2474 let r_sphere = sphere.radius();
2475 let co = cyl.origin();
2476 let axis = cyl.axis();
2477 let r_cyl = cyl.radius();
2478
2479 let delta = sc - co;
2481 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2482 let along = delta_vec.dot(axis);
2483 let perp_vec = delta_vec - axis * along;
2484 let d_perp = perp_vec.length();
2485
2486 if d_perp > 1e-7 {
2489 return Ok(None);
2490 }
2491
2492 if r_cyl > r_sphere + 1e-10 {
2495 return Ok(Some(vec![]));
2496 }
2497 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2498 if z_sq < 0.0 {
2499 return Ok(Some(vec![]));
2500 }
2501 let z = z_sq.sqrt();
2502
2503 let center_axis_pt = Point3::new(
2506 co.x() + axis.x() * along,
2507 co.y() + axis.y() * along,
2508 co.z() + axis.z() * along,
2509 );
2510
2511 let mut circles = Vec::new();
2512 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2513 for &z_offset in offsets {
2514 let center = Point3::new(
2515 center_axis_pt.x() + axis.x() * z_offset,
2516 center_axis_pt.y() + axis.y() * z_offset,
2517 center_axis_pt.z() + axis.z() * z_offset,
2518 );
2519 let circle = Circle3D::new(center, axis, r_cyl)?;
2520 circles.push(ExactIntersectionCurve::Circle(circle));
2521 }
2522 Ok(Some(circles))
2523}
2524
2525pub fn exact_cone_sphere(
2543 cone: &ConicalSurface,
2544 sphere: &SphericalSurface,
2545) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2546 let offset = cone.apex() - sphere.center();
2547 let along = offset.dot(cone.axis());
2548 if (offset - cone.axis() * along).length() > 1e-7 {
2549 return Ok(None);
2550 }
2551 let lin_tol = Tolerance::new().linear;
2552 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2553 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2554 let b = 2.0 * sin_a * along;
2555 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2556 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2559 if disc < -noise {
2560 return Ok(Some(vec![]));
2561 }
2562 let roots: &[f64] = if far - near < lin_tol {
2563 &[far]
2564 } else {
2565 &[near, far]
2566 };
2567 let mut circles = Vec::new();
2568 for &v in roots {
2569 if v * cos_a > lin_tol {
2570 let centre = cone.apex() + cone.axis() * (v * sin_a);
2571 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2572 circles.push(ExactIntersectionCurve::Circle(circle));
2573 }
2574 }
2575 Ok(Some(circles))
2576}
2577
2578fn algebraic_sphere_cylinder(
2587 sphere: &SphericalSurface,
2588 cyl: &CylindricalSurface,
2589 sphere_first: bool,
2590) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2591 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2592 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2593 };
2594
2595 let mut curves = Vec::new();
2596 for exact in exacts {
2597 let ExactIntersectionCurve::Circle(circle) = exact else {
2598 continue;
2599 };
2600 let n_samples = 33;
2601 let mut points = Vec::with_capacity(n_samples);
2602 let mut positions = Vec::with_capacity(n_samples);
2603 #[allow(clippy::cast_precision_loss)]
2604 for i in 0..n_samples {
2605 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2606 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2607 positions.push(pt);
2608 let (param1, param2) = in_order(
2609 sphere.project_point(pt),
2610 cyl.project_point(pt),
2611 sphere_first,
2612 );
2613 points.push(IntersectionPoint {
2614 point: pt,
2615 param1,
2616 param2,
2617 });
2618 }
2619 let degree = 3.min(positions.len() - 1);
2620 let curve = interpolate(&positions, degree)?;
2621 curves.push(IntersectionCurve { curve, points });
2622 }
2623
2624 Ok(Some(curves))
2625}
2626
2627fn off_axis_sphere_cylinder(
2636 sphere: &SphericalSurface,
2637 cyl: &CylindricalSurface,
2638 sphere_first: bool,
2639) -> Option<Vec<IntersectionCurve>> {
2640 let (centre, radius) = (sphere.center(), sphere.radius());
2641 let axis = cyl.axis();
2642 let offset = centre - cyl.origin();
2643 let axis_distance = (offset - axis * offset.dot(axis)).length();
2644 let lin_tol = Tolerance::new().linear;
2645 if axis_distance > radius + cyl.radius() + lin_tol
2646 || axis_distance + radius < cyl.radius() - lin_tol
2647 {
2648 return Some(Vec::new());
2649 }
2650 let roots = |u: f64| {
2651 let q = cyl.evaluate(u, 0.0) - centre;
2652 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2653 };
2654 let samples = ruling_samples(cyl, &roots);
2655 let loops = if samples.iter().all(Option::is_some) {
2656 closed_ruling_loops(cyl, &roots, &samples)
2657 } else {
2658 partial_ruling_loops(cyl, &roots, &samples)
2659 };
2660 if loops.is_empty() {
2661 return None;
2662 }
2663 Some(fit_ruling_loops(&loops, |p| {
2664 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2665 }))
2666}
2667
2668const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2671 if a_first { (a, b) } else { (b, a) }
2672}
2673
2674#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2688fn algebraic_cylinder_cylinder(
2689 c1: &CylindricalSurface,
2690 c2: &CylindricalSurface,
2691) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2692 let alpha = c1.axis().dot(c2.axis());
2693 let a_coeff = 1.0 - alpha * alpha;
2694
2695 if a_coeff.abs() < 1e-12 {
2697 return Ok(None);
2698 }
2699
2700 let r1 = c1.radius();
2701 let r2 = c2.radius();
2702 let o1 = c1.origin();
2703 let o2 = c2.origin();
2704 let a1 = c1.axis();
2705 let a2 = c2.axis();
2706
2707 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2710 let cross = a1.cross(a2);
2711 let cross_len = cross.length();
2712 if cross_len > 1e-12 {
2713 let axis_dist = delta.dot(cross).abs() / cross_len;
2714 if axis_dist > r1 + r2 + Tolerance::new().linear {
2715 return Ok(Some(vec![])); }
2717 }
2718
2719 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2725 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2726 let alpha = sweep.axis().dot(a);
2727 let quad = 1.0 - alpha * alpha;
2728 let (axis, sweep) = (sweep.axis(), sweep.clone());
2729 move |u: f64| {
2730 let q = sweep.evaluate(u, 0.0) - o;
2731 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2732 let b = 2.0 * (q_a1 - alpha * q_a2);
2733 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2734 ruling_quadratic(quad, b, c)
2735 }
2736 };
2737 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2738 let samples1 = ruling_samples(c1, &roots1);
2739 let loops = if samples1.iter().all(Option::is_some) {
2740 closed_ruling_loops(c1, &roots1, &samples1)
2741 } else {
2742 let samples2 = ruling_samples(c2, &roots2);
2743 if samples2.iter().all(Option::is_some) {
2744 closed_ruling_loops(c2, &roots2, &samples2)
2745 } else if samples1.iter().any(Option::is_some) {
2746 partial_ruling_loops(c1, &roots1, &samples1)
2747 } else {
2748 partial_ruling_loops(c2, &roots2, &samples2)
2749 }
2750 };
2751 if loops.is_empty() {
2752 return Ok(None);
2753 }
2754 Ok(Some(fit_ruling_loops(&loops, |p| {
2755 (c1.project_point(p), c2.project_point(p))
2756 })))
2757}
2758
2759fn ruling_cone_cylinder(
2766 cone: &ConicalSurface,
2767 cyl: &CylindricalSurface,
2768 cone_first: bool,
2769) -> Option<Vec<IntersectionCurve>> {
2770 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2771 if sin_t < 1e-12 || cos_t < 1e-12 {
2772 return None;
2773 }
2774 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2775 let s = 1.0 / (sin_t * sin_t);
2776 let alpha = w.dot(d);
2777 let quad = 1.0 - s * alpha * alpha;
2778 if quad.abs() < 1e-9 {
2779 return None;
2780 }
2781 let roots = |u: f64| {
2782 let delta = cyl.evaluate(u, 0.0) - apex;
2783 let (dd, dw) = (delta.dot(d), delta.dot(w));
2784 let b = 2.0 * (dw - s * dd * alpha);
2785 let c = delta.dot(delta) - s * dd * dd;
2786 ruling_quadratic(quad, b, c)
2787 };
2788 let lin_tol = Tolerance::new().linear;
2789 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2790 #[allow(clippy::cast_precision_loss)]
2791 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2792 let (disc, vp, vm) = roots(u);
2793 disc >= -lin_tol
2794 && [vp, vm]
2795 .iter()
2796 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2797 });
2798 if far_nappe {
2799 return None;
2800 }
2801 let samples = ruling_samples(cyl, &roots);
2802 let scan = WINDOW_SCAN * RULING_SAMPLES;
2807 #[allow(clippy::cast_precision_loss)]
2810 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2811 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2812 let mut k = start;
2813 while k < start + scan {
2814 if !meets(k) {
2815 k += 1;
2816 continue;
2817 }
2818 let first = k;
2819 while k < start + scan && meets(k) {
2820 k += 1;
2821 }
2822 let covered = (first..k)
2823 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2824 .count();
2825 if covered < WINDOW_MIN_SAMPLES {
2826 return None;
2827 }
2828 }
2829 }
2830 let loops = if samples.iter().all(Option::is_some) {
2831 closed_ruling_loops(cyl, &roots, &samples)
2832 } else {
2833 partial_ruling_loops(cyl, &roots, &samples)
2834 };
2835 if loops.is_empty() {
2836 return None;
2837 }
2838 Some(fit_ruling_loops(&loops, |p| {
2839 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2840 }))
2841}
2842
2843fn ruling_torus_cylinder(
2853 torus: &ToroidalSurface,
2854 cyl: &CylindricalSurface,
2855 torus_first: bool,
2856) -> Option<Vec<IntersectionCurve>> {
2857 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2858 || torus.minor_radius() >= torus.major_radius()
2859 {
2860 return None;
2861 }
2862 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2863 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2864 let count = rows[0].len();
2865 let scan = WINDOW_SCAN * RULING_SAMPLES;
2866 #[allow(clippy::cast_precision_loss)]
2867 if count == 0
2868 || count % 2 == 1
2869 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2870 {
2871 return None;
2872 }
2873 let loops: Vec<Vec<Point3>> = (0..count)
2874 .map(|j| {
2875 let mut pts: Vec<Point3> = rows
2876 .iter()
2877 .enumerate()
2878 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2879 .collect();
2880 pts.push(pts[0]);
2881 pts
2882 })
2883 .collect();
2884 Some(fit_ruling_loops(&loops, |p| {
2885 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2886 }))
2887}
2888
2889fn ruling_cone_sphere(
2898 cone: &ConicalSurface,
2899 sphere: &SphericalSurface,
2900 cone_first: bool,
2901) -> Option<Vec<IntersectionCurve>> {
2902 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2903 let offset = apex - centre;
2904 let lin_tol = Tolerance::new().linear;
2905 let along = offset.dot(cone.axis());
2906 let across = (offset - cone.axis() * along).length();
2907 if across < lin_tol {
2908 return None;
2909 }
2910 let k = offset.dot(offset) - radius * radius;
2916 if radius - offset.length() > lin_tol {
2917 let exit = |u: f64| {
2918 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2919 let root = h.mul_add(h, -k).sqrt();
2920 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2921 };
2922 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2927 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2928 .collect();
2929 for _ in 0..10 {
2930 let mut refined = Vec::with_capacity(2 * samples.len());
2931 for pair in samples.windows(2) {
2932 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2933 refined.push(pair[0]);
2934 let um = 0.5 * (u0 + u1);
2935 let pm = exit(um);
2936 let chord = (p1 - p0).length();
2937 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2938 refined.push((um, pm));
2939 }
2940 }
2941 refined.extend(samples.last().copied());
2942 if refined.len() == samples.len() {
2943 break;
2944 }
2945 samples = refined;
2946 }
2947 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2948 if let Some(last) = pts.last_mut() {
2949 *last = samples[0].1;
2950 }
2951 return Some(fit_ruling_loops(&[pts], |p| {
2952 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2953 }));
2954 }
2955 let crossing = |h: f64| {
2959 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2960 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2961 };
2962 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2967 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2968 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2969 {
2970 return window_cone_sphere(cone, sphere, cone_first);
2971 }
2972 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2973 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2974 .collect::<Option<_>>()?;
2975 let loops: Vec<Vec<Point3>> = [0, 1]
2976 .iter()
2977 .map(|&j| {
2978 let mut pts: Vec<Point3> = rows
2979 .iter()
2980 .enumerate()
2981 .map(|(i, &(near, far))| {
2982 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2983 })
2984 .collect();
2985 pts.push(pts[0]);
2986 pts
2987 })
2988 .collect();
2989 Some(fit_ruling_loops(&loops, |p| {
2990 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2991 }))
2992}
2993
2994fn window_cone_sphere(
3009 cone: &ConicalSurface,
3010 sphere: &SphericalSurface,
3011 cone_first: bool,
3012) -> Option<Vec<IntersectionCurve>> {
3013 let offset = cone.apex() - sphere.center();
3014 let lin_tol = Tolerance::new().linear;
3015 if offset.length() - sphere.radius() <= lin_tol {
3016 return None;
3017 }
3018 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
3019 let (sin_a, cos_a) = cone.half_angle().sin_cos();
3020 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
3021 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
3022 if a < lin_tol {
3023 return None;
3024 }
3025 let reach = (-k.sqrt() - c) / a;
3026 if reach <= -1.0 {
3027 return Some(Vec::new());
3028 }
3029 if reach >= 1.0 {
3030 return None;
3031 }
3032 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
3033 let half = std::f64::consts::PI - half;
3034 let n = RULING_SAMPLES;
3035 let mut pts: Vec<Point3> = (0..n)
3036 .map(|i| {
3037 #[allow(clippy::cast_precision_loss)]
3038 let theta = TAU * i as f64 / n as f64;
3039 let u = half.mul_add(-theta.cos(), mid);
3040 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
3041 let split = h.mul_add(h, -k).max(0.0).sqrt();
3042 cone.evaluate(u, -h - split.copysign(theta.sin()))
3043 })
3044 .collect();
3045 pts.push(pts[0]);
3046 Some(fit_ruling_loops(&[pts], |p| {
3047 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3048 }))
3049}
3050
3051const WINDOW_SCAN: usize = 16;
3054const WINDOW_MIN_SAMPLES: usize = 8;
3055
3056const RULING_SAMPLES: usize = 128;
3060
3061#[allow(clippy::cast_precision_loss)]
3062fn ruling_u(i: usize) -> f64 {
3063 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
3064}
3065
3066fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
3068 let disc = b * b - 4.0 * quad * c;
3069 let root = disc.max(0.0).sqrt();
3070 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
3071}
3072
3073fn ruling_samples(
3077 sweep: &CylindricalSurface,
3078 roots: &impl Fn(f64) -> (f64, f64, f64),
3079) -> Vec<Option<(Point3, Point3)>> {
3080 let lin_tol = Tolerance::new().linear;
3081 (0..RULING_SAMPLES)
3082 .map(|i| {
3083 let u = ruling_u(i);
3084 let (disc, vp, vm) = roots(u);
3085 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
3086 })
3087 .collect()
3088}
3089
3090fn closed_ruling_loops(
3097 sweep: &CylindricalSurface,
3098 roots: &impl Fn(f64) -> (f64, f64, f64),
3099 samples: &[Option<(Point3, Point3)>],
3100) -> Vec<Vec<Point3>> {
3101 let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3102 if let Some((neck, touching)) = narrowest_ruling(roots) {
3103 let count = 2 * RULING_SAMPLES;
3104 #[allow(clippy::cast_precision_loss)]
3105 (0..count)
3106 .map(|i| {
3107 let t = i as f64 / count as f64;
3108 let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3109 let (_, vp, vm) = roots(u);
3110 if i == 0 && touching {
3111 let at = sweep.evaluate(u, 0.5 * (vp + vm));
3112 (at, at)
3113 } else {
3114 (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3115 }
3116 })
3117 .unzip()
3118 } else {
3119 samples.iter().flatten().copied().unzip()
3120 };
3121 plus.push(plus[0]);
3122 minus.push(minus[0]);
3123 vec![plus, minus]
3124}
3125
3126fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3134 let scan = WINDOW_SCAN * RULING_SAMPLES;
3135 #[allow(clippy::cast_precision_loss)]
3136 let step = TAU / scan as f64;
3137 let gap = |u: f64| {
3138 let (_, vp, vm) = roots(u);
3139 (vp - vm).abs()
3140 };
3141 #[allow(clippy::cast_precision_loss)]
3142 let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3143 let widest = gaps.iter().copied().fold(0.0, f64::max);
3144 let tol = Tolerance::new().linear * (1.0 + widest);
3145 let mut necks = Vec::new();
3146 for k in 0..scan {
3147 let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3148 if here > before || here >= after || here >= 0.25 * widest {
3149 continue;
3150 }
3151 #[allow(clippy::cast_precision_loss)]
3152 let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3153 for _ in 0..100 {
3154 let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3155 if gap(a) < gap(b) {
3156 hi = b;
3157 } else {
3158 lo = a;
3159 }
3160 }
3161 let u = 0.5 * (lo + hi);
3162 necks.push((u, gap(u) <= tol));
3163 }
3164 match necks[..] {
3165 [neck] => Some(neck),
3166 _ => None,
3167 }
3168}
3169
3170fn partial_ruling_loops(
3175 sweep: &CylindricalSurface,
3176 roots: &impl Fn(f64) -> (f64, f64, f64),
3177 samples: &[Option<(Point3, Point3)>],
3178) -> Vec<Vec<Point3>> {
3179 let branch_point = |inside: usize, outside: usize| -> Point3 {
3180 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3181 if (hi - lo).abs() > std::f64::consts::PI {
3182 hi += if hi < lo { TAU } else { -TAU };
3183 }
3184 for _ in 0..60 {
3185 let mid = 0.5 * (lo + hi);
3186 if roots(mid).0 >= 0.0 {
3187 lo = mid;
3188 } else {
3189 hi = mid;
3190 }
3191 }
3192 let (_, vp, vm) = roots(lo);
3193 sweep.evaluate(lo, 0.5 * (vp + vm))
3194 };
3195 let Some(first_gap) = samples.iter().position(Option::is_none) else {
3196 return Vec::new();
3197 };
3198 let mut loops = Vec::new();
3199 let mut k = 0;
3200 while k < RULING_SAMPLES {
3201 let i = (first_gap + k) % RULING_SAMPLES;
3202 if samples[i].is_none() {
3203 k += 1;
3204 continue;
3205 }
3206 let start = i;
3207 let mut run = Vec::new();
3208 while k < RULING_SAMPLES {
3209 let j = (first_gap + k) % RULING_SAMPLES;
3210 let Some(pair) = samples[j] else { break };
3211 run.push(pair);
3212 k += 1;
3213 }
3214 let end = (start + run.len() - 1) % RULING_SAMPLES;
3215 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3216 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3217 let mut pts = vec![head];
3218 pts.extend(run.iter().map(|p| p.0));
3219 pts.push(tail);
3220 pts.extend(run.iter().rev().map(|p| p.1));
3221 pts.push(head);
3222 loops.push(pts);
3223 }
3224 loops
3225}
3226
3227fn fit_ruling_loops(
3230 loops: &[Vec<Point3>],
3231 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3232) -> Vec<IntersectionCurve> {
3233 let mut curves = Vec::new();
3234 for pts in loops {
3235 if pts.len() < 4 {
3236 continue;
3237 }
3238 let ipts: Vec<IntersectionPoint> = pts
3239 .iter()
3240 .map(|&p| {
3241 let (param1, param2) = params(p);
3242 IntersectionPoint {
3243 point: p,
3244 param1,
3245 param2,
3246 }
3247 })
3248 .collect();
3249 let degree = 3.min(pts.len() - 1);
3250 if let Ok(curve) = interpolate(pts, degree) {
3251 curves.push(IntersectionCurve {
3252 curve,
3253 points: ipts,
3254 });
3255 }
3256 }
3257 curves
3258}
3259
3260#[allow(clippy::unnecessary_wraps)]
3286fn algebraic_parallel_cone_cylinder(
3287 cone: &ConicalSurface,
3288 cyl: &CylindricalSurface,
3289 v_range_cone: Option<(f64, f64)>,
3290 v_range_cyl: Option<(f64, f64)>,
3291) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3292 let axis = cone.axis();
3293 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3294 return Ok(None); }
3296
3297 let apex = cone.apex();
3298 let delta = cyl.origin() - apex;
3299 let along = delta.dot(axis);
3300 let perp = delta - axis * along;
3301 let d = perp.length();
3302 if d < 1e-9 {
3303 return Ok(None); }
3305
3306 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3307 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3308
3309 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3310 if cos_t < 1e-12 || sin_t < 1e-12 {
3311 return Ok(None);
3312 }
3313 let r = cyl.radius();
3314
3315 let mut v_min = (d - r).abs() / cos_t;
3317 let mut v_max = (d + r) / cos_t;
3318 if v_max <= v_min {
3319 return Ok(Some(vec![]));
3320 }
3321
3322 let mut lo = v_min;
3328 let mut hi = v_max;
3329 if let Some((a, b)) = v_range_cone {
3334 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3335 lo = lo.max(a);
3336 hi = hi.min(b);
3337 }
3338 if let Some((a, b)) = v_range_cyl {
3339 let flip = cyl.axis().dot(axis);
3342 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3343 let (a, b) = (to_cone_v(a), to_cone_v(b));
3344 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3345 lo = lo.max(a);
3346 hi = hi.min(b);
3347 }
3348 let (turn_lo, turn_hi) = (v_min, v_max);
3349 v_min = lo.max(v_min);
3350 v_max = hi.min(v_max);
3351 if v_max - v_min <= 1e-12 {
3352 return Ok(Some(vec![]));
3353 }
3354 let slack = Tolerance::new().linear;
3362 #[allow(clippy::cast_precision_loss)]
3363 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3364 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3365 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3366 .map(|i| {
3367 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3368 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3369 let off = foot - apex;
3370 let across = off - axis * off.dot(axis);
3371 apex + across + axis * (across.length() * sin_t / cos_t)
3372 })
3373 .collect();
3374 pts.push(pts[0]);
3375 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3376 (cone.project_point(p), cyl.project_point(p))
3377 })));
3378 }
3379
3380 let n_samples = 128;
3381 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3382 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3383 #[allow(clippy::cast_precision_loss)]
3384 for i in 0..=n_samples {
3385 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3386 let rho = v * cos_t;
3387 if rho < 1e-12 {
3388 if (d - r).abs() < 1e-12 {
3396 let apex = cone.evaluate(phi0, v);
3397 plus.push(apex);
3398 minus.push(apex);
3399 }
3400 continue;
3401 }
3402 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3403 let alpha = cos_alpha.acos();
3404 plus.push(cone.evaluate(phi0 + alpha, v));
3405 minus.push(cone.evaluate(phi0 - alpha, v));
3406 }
3407
3408 let mut curves = Vec::new();
3409 for pts in [&plus, &minus] {
3410 if pts.len() < 4 {
3413 continue;
3414 }
3415 let ipts: Vec<IntersectionPoint> = pts
3416 .iter()
3417 .map(|&p| IntersectionPoint {
3418 point: p,
3419 param1: cone.project_point(p),
3420 param2: cyl.project_point(p),
3421 })
3422 .collect();
3423 let degree = 3.min(pts.len() - 1);
3424 match interpolate(pts, degree) {
3425 Ok(curve) => curves.push(IntersectionCurve {
3426 curve,
3427 points: ipts,
3428 }),
3429 Err(_) => return Ok(None),
3434 }
3435 }
3436
3437 Ok(Some(curves))
3438}
3439
3440fn algebraic_sphere_sphere(
3448 s1: &SphericalSurface,
3449 s2: &SphericalSurface,
3450) -> Result<Vec<IntersectionCurve>, MathError> {
3451 let c1 = s1.center();
3452 let c2 = s2.center();
3453 let r1 = s1.radius();
3454 let r2 = s2.radius();
3455
3456 let delta = c2 - c1;
3457 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3458 let d = d_sq.sqrt();
3459
3460 if d < 1e-12 {
3461 return Ok(vec![]);
3463 }
3464
3465 if d > r1 + r2 + 1e-10 {
3467 return Ok(vec![]); }
3469 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3470 return Ok(vec![]); }
3472
3473 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3475
3476 let r_circle_sq = r1 * r1 - d1 * d1;
3478 if r_circle_sq < 0.0 {
3479 if r_circle_sq > -1e-10 {
3481 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3483 let tangent_pt = Point3::new(
3484 c1.x() + axis.x() * d1,
3485 c1.y() + axis.y() * d1,
3486 c1.z() + axis.z() * d1,
3487 );
3488 let ipt = IntersectionPoint {
3489 point: tangent_pt,
3490 param1: (0.0, 0.0),
3491 param2: (0.0, 0.0),
3492 };
3493 return Ok(vec![IntersectionCurve {
3495 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3496 points: vec![ipt],
3497 }]);
3498 }
3499 return Ok(vec![]);
3500 }
3501
3502 let r_circle = r_circle_sq.sqrt();
3503 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3504 let center = Point3::new(
3505 c1.x() + axis.x() * d1,
3506 c1.y() + axis.y() * d1,
3507 c1.z() + axis.z() * d1,
3508 );
3509
3510 let basis = Frame3::from_normal(center, axis)?;
3512 let u_dir = basis.x;
3513 let v_dir = basis.y;
3514
3515 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3518 let mut positions = Vec::with_capacity(n_samples);
3519 #[allow(clippy::cast_precision_loss)]
3520 for i in 0..n_samples {
3521 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3522 let (sin_t, cos_t) = theta.sin_cos();
3523 let pt = Point3::new(
3524 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3525 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3526 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3527 );
3528 positions.push(pt);
3529 points.push(IntersectionPoint {
3530 point: pt,
3531 param1: (0.0, 0.0),
3532 param2: (0.0, 0.0),
3533 });
3534 }
3535
3536 let degree = 3.min(positions.len() - 1);
3537 let curve = interpolate(&positions, degree)?;
3538
3539 Ok(vec![IntersectionCurve { curve, points }])
3540}
3541
3542#[allow(clippy::too_many_arguments)]
3548fn correct_to_intersection(
3549 a: &AnalyticSurface<'_>,
3550 b: &AnalyticSurface<'_>,
3551 surf_a: &dyn Fn(f64, f64) -> Point3,
3552 norm_a: &dyn Fn(f64, f64) -> Vec3,
3553 surf_b: &dyn Fn(f64, f64) -> Point3,
3554 norm_b: &dyn Fn(f64, f64) -> Vec3,
3555 point: Point3,
3556 u_range_a: (f64, f64),
3557 v_range_a: (f64, f64),
3558 u_range_b: (f64, f64),
3559 v_range_b: (f64, f64),
3560 max_iters: usize,
3561) -> Point3 {
3562 let mut p = point;
3563 for _ in 0..max_iters {
3564 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3565 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3566 let pa = surf_a(ua, va);
3567 let pb = surf_b(ub, vb);
3568 let na = norm_a(ua, va);
3569 let nb = norm_b(ub, vb);
3570 let pv = Vec3::new(p.x(), p.y(), p.z());
3571
3572 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3573 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3574
3575 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3576 break;
3577 }
3578
3579 let t = na.cross(nb);
3580 let t_len = t.length();
3581 if t_len < 1e-10 {
3582 return Point3::new(
3584 (pa.x() + pb.x()) * 0.5,
3585 (pa.y() + pb.y()) * 0.5,
3586 (pa.z() + pb.z()) * 0.5,
3587 );
3588 }
3589 let t_hat = t * (1.0 / t_len);
3590
3591 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3593 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3594 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3595 if det.abs() < 1e-15 {
3596 return Point3::new(
3597 (pa.x() + pb.x()) * 0.5,
3598 (pa.y() + pb.y()) * 0.5,
3599 (pa.z() + pb.z()) * 0.5,
3600 );
3601 }
3602 let inv = 1.0 / det;
3603 let dx = inv
3605 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3606 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3607 let dy = inv
3608 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3609 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3610 let dz = inv
3611 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3612 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3613 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3614
3615 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3618 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3619 let pc_a = surf_a(uc, vc);
3620 let pc_b = surf_b(ud, vd);
3621 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3622 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3623 .dot(norm_a(uc, vc))
3624 .abs();
3625 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3626 .dot(norm_b(ud, vd))
3627 .abs();
3628 if da_new > da.abs() && db_new > db.abs() {
3629 return p;
3630 }
3631
3632 p = candidate;
3633 }
3634 p
3635}
3636
3637#[allow(clippy::too_many_arguments)]
3643fn march_analytic_intersection(
3644 a: &AnalyticSurface<'_>,
3645 b: &AnalyticSurface<'_>,
3646 surf_a: &dyn Fn(f64, f64) -> Point3,
3647 norm_a: &dyn Fn(f64, f64) -> Vec3,
3648 surf_b: &dyn Fn(f64, f64) -> Point3,
3649 norm_b: &dyn Fn(f64, f64) -> Vec3,
3650 seed: Point3,
3651 u_range_a: (f64, f64),
3652 v_range_a: (f64, f64),
3653 u_range_b: (f64, f64),
3654 v_range_b: (f64, f64),
3655 initial_step: f64,
3656 u_periodic_a: bool,
3657 u_periodic_b: bool,
3658 region: Option<Aabb3>,
3659) -> Vec<Point3> {
3660 let max_steps = 500;
3661 let h_min = 1e-6;
3662 let h_max = initial_step * 4.0;
3663 let closure_dist = initial_step * 5.0;
3667 let max_angle = 10.0_f64.to_radians();
3669 let min_angle = 2.0_f64.to_radians();
3670
3671 let mut forward = Vec::new();
3673 let mut backward = Vec::new();
3675
3676 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3677 let mut current = seed;
3678 let mut h = initial_step;
3679 let mut prev_tangent: Option<Vec3> = None;
3680
3681 for _ in 0..max_steps {
3682 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3683 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3684
3685 let na = norm_a(ua, va);
3686 let nb = norm_b(ub, vb);
3687
3688 let tangent = na.cross(nb);
3689 let t_len = tangent.length();
3690 if t_len < 1e-10 {
3691 break;
3692 }
3693 let t_dir = tangent * (direction / t_len);
3694
3695 if let Some(prev_t) = prev_tangent {
3697 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3698 let angle = cos_angle.acos();
3699 if angle > max_angle && h > h_min {
3700 h = (h * 0.5).max(h_min);
3701 } else if angle < min_angle {
3702 h = (h * 2.0).min(h_max);
3703 }
3704 }
3705 prev_tangent = Some(t_dir);
3706
3707 let next = Point3::new(
3708 h.mul_add(t_dir.x(), current.x()),
3709 h.mul_add(t_dir.y(), current.y()),
3710 h.mul_add(t_dir.z(), current.z()),
3711 );
3712
3713 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3714 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3715
3716 let pa = surf_a(ua2, va2);
3717 let pb = surf_b(ub2, vb2);
3718 let mid = Point3::new(
3719 (pa.x() + pb.x()) * 0.5,
3720 (pa.y() + pb.y()) * 0.5,
3721 (pa.z() + pb.z()) * 0.5,
3722 );
3723 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3724 || va2 <= v_range_a.0
3725 || va2 >= v_range_a.1;
3726 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3727 || vb2 <= v_range_b.0
3728 || vb2 >= v_range_b.1;
3729
3730 if out_a || out_b {
3731 break;
3732 }
3733 if region.is_some_and(|r| !r.contains_point(mid)) {
3734 points.push(mid);
3735 break;
3736 }
3737
3738 let dist_to_seed = (mid - seed).length();
3742 if points.len() > 10 && dist_to_seed < closure_dist {
3743 points.push(seed);
3744 break;
3745 }
3746
3747 points.push(mid);
3748 current = mid;
3749 }
3750 }
3751
3752 backward.reverse();
3754 let mut result = backward;
3755 result.push(seed);
3756 result.append(&mut forward);
3757
3758 for pt in &mut result {
3760 *pt = correct_to_intersection(
3761 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3762 5,
3763 );
3764 }
3765
3766 result
3767}
3768
3769fn project_analytic(
3773 surface: &AnalyticSurface<'_>,
3774 point: Point3,
3775 u_range: (f64, f64),
3776 v_range: (f64, f64),
3777) -> (f64, f64) {
3778 match surface {
3779 AnalyticSurface::Cylinder(cyl) => {
3780 let (u, v) = cyl.project_point(point);
3781 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3782 }
3783 AnalyticSurface::Sphere(sphere) => {
3784 let (u, v) = sphere.project_point(point);
3785 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3786 }
3787 AnalyticSurface::Cone(cone) => {
3788 let (u, v) = cone.project_point(point);
3789 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3790 }
3791 AnalyticSurface::Torus(torus) => {
3792 let (u, v) = torus.project_point(point);
3793 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3794 }
3795 }
3796}
3797
3798fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3802 matches!(
3803 surface,
3804 AnalyticSurface::Cylinder(_)
3805 | AnalyticSurface::Cone(_)
3806 | AnalyticSurface::Sphere(_)
3807 | AnalyticSurface::Torus(_)
3808 )
3809}
3810
3811#[allow(clippy::type_complexity)]
3813fn surface_closures<'a>(
3814 surface: &'a AnalyticSurface<'a>,
3815) -> (
3816 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3817 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3818 (f64, f64),
3819 (f64, f64),
3820) {
3821 match surface {
3822 AnalyticSurface::Cylinder(cyl) => (
3823 Box::new(|u, v| cyl.evaluate(u, v)),
3824 Box::new(|u, v| cyl.normal(u, v)),
3825 (0.0, TAU),
3826 (-1.0, 1.0),
3827 ),
3828 AnalyticSurface::Cone(cone) => (
3829 Box::new(|u, v| cone.evaluate(u, v)),
3830 Box::new(|u, v| cone.normal(u, v)),
3831 (0.0, TAU),
3832 (0.01, 2.0),
3833 ),
3834 AnalyticSurface::Sphere(sphere) => (
3835 Box::new(|u, v| sphere.evaluate(u, v)),
3836 Box::new(|u, v| sphere.normal(u, v)),
3837 (0.0, TAU),
3838 (-FRAC_PI_2, FRAC_PI_2),
3839 ),
3840 AnalyticSurface::Torus(torus) => (
3841 Box::new(|u, v| torus.evaluate(u, v)),
3842 Box::new(|u, v| torus.normal(u, v)),
3843 (0.0, TAU),
3844 (0.0, TAU),
3845 ),
3846 }
3847}
3848
3849#[cfg(test)]
3850#[allow(clippy::unwrap_used, clippy::expect_used)]
3851mod tests {
3852 use super::*;
3853 use crate::tolerance::Tolerance;
3854
3855 #[test]
3859 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3860 let half_angle = 1.1_f64;
3861 let cone = ConicalSurface::new(
3862 Point3::new(0.0, 0.0, 0.0),
3863 Vec3::new(0.0, 0.0, 1.0),
3864 half_angle,
3865 )
3866 .unwrap();
3867 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3868 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3869 let chains =
3870 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3871 .unwrap();
3872 let chain = chains
3873 .iter()
3874 .find_map(|c| match c {
3875 ExactIntersectionCurve::Points(chain) => Some(chain),
3876 _ => None,
3877 })
3878 .expect("a parabola or hyperbola section is sampled");
3879 let (from, to) = (chain[2], chain[chain.len() - 3]);
3880 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3881 .unwrap()
3882 .expect("an exact arc");
3883 let (t0, t1) = arc.domain();
3884 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3885 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3886 for i in 0..=200 {
3887 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3888 let w = q - Point3::new(0.0, 0.0, 0.0);
3889 let off_plane = (normal.dot(w) - d).abs();
3890 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3891 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3892 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3893 }
3894 }
3895 }
3896
3897 #[test]
3901 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3902 let half_angle = 1.1_f64;
3903 let cone = ConicalSurface::new(
3904 Point3::new(0.0, 0.0, 0.0),
3905 Vec3::new(0.0, 0.0, 1.0),
3906 half_angle,
3907 )
3908 .unwrap();
3909 for shortfall in [1e-10, 3e-10, 8e-10] {
3910 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3911 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3912 let chains =
3913 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3914 .unwrap();
3915 let Some(chain) = chains.iter().find_map(|c| match c {
3916 ExactIntersectionCurve::Points(chain) => Some(chain),
3917 _ => None,
3918 }) else {
3919 continue;
3920 };
3921 let (from, to) = (chain[2], chain[chain.len() - 3]);
3922 assert!(
3923 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3924 .unwrap()
3925 .is_none(),
3926 "coincident ends"
3927 );
3928 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3929 continue;
3930 };
3931 let (t0, t1) = arc.domain();
3932 for i in 0..=200 {
3933 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3934 - Point3::new(0.0, 0.0, 0.0);
3935 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3936 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3937 }
3938 }
3939 }
3940
3941 #[test]
3942 fn plane_cylinder_perpendicular() {
3943 let cyl =
3944 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3945 .unwrap();
3946
3947 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3949 assert!(!curves.is_empty(), "should find intersection curve");
3950 assert!(
3951 curves[0].points.len() > 10,
3952 "should have many sample points"
3953 );
3954
3955 let tol = Tolerance::loose();
3956 for pt in &curves[0].points {
3957 assert!(
3958 tol.approx_eq(pt.point.z(), 3.0),
3959 "z should be ~3.0, got {}",
3960 pt.point.z()
3961 );
3962 let r = pt.point.x().hypot(pt.point.y());
3963 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3964 }
3965 }
3966
3967 #[test]
3968 fn plane_sphere_equator() {
3969 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3970
3971 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3972 assert!(!curves.is_empty());
3973
3974 let tol = Tolerance::loose();
3975 for pt in &curves[0].points {
3976 assert!(
3977 tol.approx_eq(pt.point.z(), 0.0),
3978 "z should be ~0, got {}",
3979 pt.point.z()
3980 );
3981 let r = pt.point.x().hypot(pt.point.y());
3982 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3983 }
3984 }
3985
3986 #[test]
3987 fn plane_sphere_no_intersection() {
3988 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3989
3990 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3991 assert!(curves.is_empty());
3992 }
3993
3994 #[test]
3995 fn plane_cone_cross_section() {
3996 let cone = ConicalSurface::new(
3997 Point3::new(0.0, 0.0, 0.0),
3998 Vec3::new(0.0, 0.0, 1.0),
3999 std::f64::consts::FRAC_PI_4,
4000 )
4001 .unwrap();
4002
4003 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
4004 assert!(!curves.is_empty(), "should find intersection with cone");
4005 }
4006
4007 #[test]
4014 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
4015 let c1 = ConicalSurface::new(
4016 Point3::new(
4017 -16.999_999_999_999_975,
4018 -16.999_999_999_999_975,
4019 5.849_999_999_999_951,
4020 ),
4021 Vec3::new(0.0, 0.0, -1.0),
4022 0.785_398_163_397_433_5,
4023 )
4024 .unwrap();
4025 let c2 = ConicalSurface::new(
4026 Point3::new(
4027 -16.750_000_000_000_036,
4028 -16.750_000_000_000_018,
4029 0.749_999_999_999_881,
4030 ),
4031 Vec3::new(0.0, 0.0, 1.0),
4032 0.785_398_163_397_467_6,
4033 )
4034 .unwrap();
4035
4036 let curves = exact_cone_cone(&c1, &c2)
4037 .unwrap()
4038 .expect("offset parallel equal-angle cones must take the radical-plane path");
4039 assert_eq!(curves.len(), 1, "expected exactly one section conic");
4040 assert!(
4041 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
4042 "expected an ellipse section, got {:?}",
4043 curves[0]
4044 );
4045 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
4046 return;
4047 };
4048
4049 for i in 0..16 {
4053 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
4054 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
4055 let rel = p - cone.apex();
4056 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
4057 let axial = rel_v.dot(cone.axis());
4058 let radial = (rel_v - cone.axis() * axial).length();
4059 assert!(
4060 axial > 0.0,
4061 "{label}: sample on phantom nappe (axial {axial})"
4062 );
4063 let expect = cone.half_angle().tan() * axial;
4064 assert!(
4065 (radial - expect).abs() < 1e-9,
4066 "{label}: sample off surface by {}",
4067 (radial - expect).abs()
4068 );
4069 }
4070 }
4071 }
4072
4073 #[test]
4077 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
4078 let c1 = ConicalSurface::new(
4079 Point3::new(0.0, 0.0, 5.0),
4080 Vec3::new(0.0, 0.0, -1.0),
4081 std::f64::consts::FRAC_PI_4,
4082 )
4083 .unwrap();
4084 let c2 = ConicalSurface::new(
4085 Point3::new(0.25, 0.25, 20.0),
4086 Vec3::new(0.0, 0.0, 1.0),
4087 std::f64::consts::FRAC_PI_4,
4088 )
4089 .unwrap();
4090 let curves = exact_cone_cone(&c1, &c2)
4091 .unwrap()
4092 .expect("radical-plane path");
4093 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
4094 }
4095
4096 #[test]
4099 fn offset_parallel_cones_with_unequal_angles_defer() {
4100 let c1 = ConicalSurface::new(
4101 Point3::new(0.0, 0.0, 5.0),
4102 Vec3::new(0.0, 0.0, -1.0),
4103 std::f64::consts::FRAC_PI_4,
4104 )
4105 .unwrap();
4106 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4107 .unwrap();
4108 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4109 }
4110
4111 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4115 let cone = ConicalSurface::new(
4116 Point3::new(0.0, 0.0, 0.0),
4117 Vec3::new(0.0, 0.0, 1.0),
4118 std::f64::consts::FRAC_PI_4,
4119 )
4120 .unwrap();
4121 let (s, c) = 40.0_f64.to_radians().sin_cos();
4122 let tube =
4123 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4124 (cone, tube)
4125 }
4126
4127 #[test]
4128 fn marcher_keeps_to_its_region() {
4129 let (cone, tube) = cone_and_tilted_tube();
4130 let run = |region: Option<Aabb3>| {
4131 let (a, b) = (
4132 AnalyticSurface::Cone(&cone),
4133 AnalyticSurface::Cylinder(&tube),
4134 );
4135 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4136 match region {
4137 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4138 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4139 }
4140 .unwrap()
4141 };
4142 assert!(!run(None).is_empty());
4143 let near = Aabb3 {
4144 min: Point3::new(-0.5, -2.0, 0.8),
4145 max: Point3::new(0.7, -0.7, 2.0),
4146 };
4147 let curves = run(Some(near));
4148 assert!(!curves.is_empty(), "the loop through the region is kept");
4149 let reach = near.expanded(0.6);
4151 for curve in &curves {
4152 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4153 }
4154 let away = Aabb3 {
4155 min: Point3::new(5.0, 5.0, 5.0),
4156 max: Point3::new(6.0, 6.0, 6.0),
4157 };
4158 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4159 }
4160
4161 #[test]
4162 fn coaxial_cones_cross_at_single_circle() {
4163 let outer = ConicalSurface::new(
4168 Point3::new(0.0, 0.0, 50.0),
4169 Vec3::new(0.0, 0.0, -1.0),
4170 5.0_f64.atan(),
4171 )
4172 .unwrap();
4173 let inner = ConicalSurface::new(
4174 Point3::new(0.0, 0.0, 90.0),
4175 Vec3::new(0.0, 0.0, -1.0),
4176 10.0_f64.atan(),
4177 )
4178 .unwrap();
4179
4180 let curves = intersect_analytic_analytic_bounded(
4181 AnalyticSurface::Cone(&outer),
4182 AnalyticSurface::Cone(&inner),
4183 32,
4184 None,
4185 None,
4186 )
4187 .unwrap();
4188
4189 assert_eq!(
4190 curves.len(),
4191 1,
4192 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4193 curves.len()
4194 );
4195 for p in &curves[0].points {
4196 let r = p.point.x().hypot(p.point.y());
4197 assert!(
4198 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4199 "intersection point off the expected z=10,r=8 circle: {:?}",
4200 p.point
4201 );
4202 }
4203 }
4204
4205 #[test]
4206 fn plane_torus_cross_section() {
4207 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4208
4209 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4210 assert!(
4211 !curves.is_empty(),
4212 "should find intersection curves with torus"
4213 );
4214 }
4215
4216 #[test]
4219 fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4220 for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4221 let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4222 for z in [3.0 - minor, 3.0 + minor] {
4223 let exact = exact_plane_analytic(
4224 AnalyticSurface::Torus(&torus),
4225 Vec3::new(0.0, 0.0, 1.0),
4226 z,
4227 )
4228 .unwrap();
4229 assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4230 let circle = match &exact[0] {
4231 ExactIntersectionCurve::Circle(c) => Some(c),
4232 _ => None,
4233 };
4234 let c = circle.expect("a circle");
4235 assert!((c.radius() - major).abs() < 1e-12);
4236 assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4237
4238 let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4239 assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4240 }
4241 let single = |normal: Vec3, d: f64| {
4243 matches!(
4244 exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4245 .unwrap()
4246 .as_slice(),
4247 [ExactIntersectionCurve::Circle(_)]
4248 )
4249 };
4250 assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4251 }
4252 }
4253
4254 #[test]
4258 fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4259 let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4260 let level = Vec3::new(0.0, 0.0, 1.0);
4261 let curves =
4262 |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4263 assert!(matches!(
4264 curves(3.0 + 100.1).as_slice(),
4265 [ExactIntersectionCurve::Circle(_)]
4266 ));
4267 let inside = curves(3.0 + 100.1 - 4e-7);
4268 assert!(!matches!(
4269 inside.as_slice(),
4270 [ExactIntersectionCurve::Circle(_)]
4271 ));
4272 }
4273
4274 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4277 let rho = p.x().hypot(p.y());
4278 ((rho - major).hypot(p.z())) - minor
4279 }
4280
4281 #[test]
4287 fn oblique_cone_cylinder_traces_curves_on_both() {
4288 use crate::traits::ParametricCurve;
4289 let cone = ConicalSurface::new(
4293 Point3::new(0.0, 0.0, 3.0),
4294 Vec3::new(0.0, 0.0, -1.0),
4295 2.0_f64.atan(),
4296 )
4297 .unwrap();
4298 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4299 let cyl =
4300 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4301 .unwrap();
4302 for cone_first in [true, false] {
4303 let (a, b) = if cone_first {
4304 (
4305 AnalyticSurface::Cone(&cone),
4306 AnalyticSurface::Cylinder(&cyl),
4307 )
4308 } else {
4309 (
4310 AnalyticSurface::Cylinder(&cyl),
4311 AnalyticSurface::Cone(&cone),
4312 )
4313 };
4314 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4315 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4316 for c in &curves {
4317 let (t0, t1) = c.curve.domain();
4318 for k in 0..=64 {
4319 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4320 let p = ParametricCurve::evaluate(&c.curve, t);
4321 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4324 assert!(
4325 (rod - 0.6).abs() < 1e-4,
4326 "x0 {x0}: off the rod by {}",
4327 rod - 0.6
4328 );
4329 let cone_r = p.x().hypot(p.y());
4330 assert!(
4331 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4332 "x0 {x0}: off the cone at {p:?}"
4333 );
4334 }
4335 }
4336 }
4337 }
4338 }
4339
4340 #[test]
4341 fn a_rod_through_a_rings_tube_traces_four_loops() {
4342 use crate::traits::ParametricCurve;
4343 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4344 let rod =
4347 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4348 .unwrap();
4349 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4350 assert_eq!(curves.len(), 4);
4351 for c in &curves {
4352 let (t0, t1) = c.curve.domain();
4353 for k in 0..=64 {
4354 let p =
4355 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4356 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4357 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4358 assert!(
4359 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4360 "off by {on_rod}, {on_ring}"
4361 );
4362 }
4363 }
4364 let high =
4366 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4367 .unwrap();
4368 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4369 let grazing =
4371 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4372 .unwrap();
4373 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4374 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4376 let thin =
4377 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4378 .unwrap();
4379 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4380 }
4381
4382 #[test]
4383 fn a_pin_through_a_ball_traces_two_loops() {
4384 use crate::traits::ParametricCurve;
4385 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4386 let half = 0.08_f64.atan();
4389 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4390 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4391 for cone_first in [true, false] {
4392 let (a, b) = if cone_first {
4393 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4394 } else {
4395 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4396 };
4397 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4398 assert_eq!(curves.len(), 2, "entry and exit loops");
4399 for c in &curves {
4400 let (t0, t1) = c.curve.domain();
4401 for k in 0..=64 {
4402 let p = ParametricCurve::evaluate(
4403 &c.curve,
4404 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4405 );
4406 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4407 let axial = apex.z() - p.z();
4408 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4409 assert!(
4410 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4411 "off by {on_ball}, {on_pin}"
4412 );
4413 }
4414 }
4415 }
4416 let coaxial =
4421 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4422 .unwrap();
4423 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4424 let aside = ConicalSurface::new(
4425 Point3::new(2.8, 0.0, 10.0),
4426 Vec3::new(0.0, 0.0, -1.0),
4427 FRAC_PI_2 - half,
4428 )
4429 .unwrap();
4430 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4431 let holding = ConicalSurface::new(
4432 Point3::new(1.0, 0.5, 1.0),
4433 Vec3::new(0.0, 0.0, -1.0),
4434 FRAC_PI_2 - half,
4435 )
4436 .unwrap();
4437 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4438 let away = ConicalSurface::new(
4439 Point3::new(1.0, 0.5, 10.0),
4440 Vec3::new(0.0, 0.0, 1.0),
4441 FRAC_PI_2 - half,
4442 )
4443 .unwrap();
4444 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4445 let step = TAU / 2048.0;
4449 let grazed =
4450 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4451 .unwrap();
4452 let wide =
4453 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4454 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4455 }
4456
4457 #[test]
4458 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4459 use crate::traits::ParametricCurve;
4460 let cone = ConicalSurface::new(
4462 Point3::new(0.0, 0.0, 3.0),
4463 Vec3::new(0.0, 0.0, -1.0),
4464 2.0_f64.atan(),
4465 )
4466 .unwrap();
4467 for (centre, radius) in [
4468 (Point3::new(1.0, 0.8, 1.2), 1.1),
4469 (Point3::new(1.5, 0.0, 0.0), 0.8),
4470 ] {
4471 let ball = SphericalSurface::new(centre, radius).unwrap();
4472 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4473 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4474 let (t0, t1) = curves[0].curve.domain();
4475 for k in 0..=64 {
4476 let p = ParametricCurve::evaluate(
4477 &curves[0].curve,
4478 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4479 );
4480 let on_ball = (p - centre).length() - radius;
4481 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4482 assert!(
4483 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4484 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4485 );
4486 }
4487 }
4488 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4490 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4491 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4492 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4493 }
4494
4495 #[test]
4496 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4497 use crate::traits::ParametricCurve;
4498 let cone = ConicalSurface::new(
4499 Point3::new(0.0, 0.0, 3.0),
4500 Vec3::new(0.0, 0.0, -1.0),
4501 2.0_f64.atan(),
4502 )
4503 .unwrap();
4504 for (centre, radius) in [
4507 (Point3::new(0.5, 0.0, 2.5), 2.0),
4508 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4509 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4510 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4511 ] {
4512 let ball = SphericalSurface::new(centre, radius).unwrap();
4513 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4514 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4515 let (t0, t1) = curves[0].curve.domain();
4516 for k in 0..=4096 {
4517 let p = ParametricCurve::evaluate(
4518 &curves[0].curve,
4519 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4520 );
4521 let on_ball = (p - centre).length() - radius;
4522 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4523 assert!(
4524 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4525 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4526 );
4527 }
4528 }
4529 }
4530
4531 #[test]
4532 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4533 let t = 2.0_f64.atan();
4534 let cone =
4535 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4536 let through_apex =
4538 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4539 .unwrap();
4540 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4541 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4543 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4544 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4545 let pin =
4548 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4549 let tube =
4550 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4551 .unwrap();
4552 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4553 }
4554
4555 #[test]
4556 fn parallel_cone_cylinder_gives_two_exact_branches() {
4557 use crate::traits::ParametricCurve;
4558 let cone = ConicalSurface::new(
4559 Point3::new(-5.45, -36.55, -4.85),
4560 Vec3::new(0.0, 0.0, 1.0),
4561 std::f64::consts::FRAC_PI_4,
4562 )
4563 .unwrap();
4564 let cyl = CylindricalSurface::new(
4565 Point3::new(-8.0, -34.0, -5.0),
4566 Vec3::new(0.0, 0.0, 1.0),
4567 4.45,
4568 )
4569 .unwrap();
4570 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4572 let curves = intersect_analytic_analytic_bounded(
4573 AnalyticSurface::Cone(&cone),
4574 AnalyticSurface::Cylinder(&cyl),
4575 32,
4576 Some(v_hint),
4577 Some((0.0, 2.5)),
4578 )
4579 .unwrap();
4580
4581 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4582 for c in &curves {
4583 let (t0, t1) = c.curve.domain();
4584 for k in 0..=32 {
4585 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4586 let p = ParametricCurve::evaluate(&c.curve, t);
4587 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4589 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4590 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4592 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4593 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4595 }
4596 }
4597 }
4598
4599 #[test]
4600 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4601 use crate::traits::ParametricCurve;
4602 let cone = ConicalSurface::new(
4604 Point3::new(0.0, 0.0, 3.0),
4605 Vec3::new(0.0, 0.0, -1.0),
4606 2.0_f64.atan(),
4607 )
4608 .unwrap();
4609 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4611 let rod =
4612 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4613 .unwrap();
4614 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4615 .unwrap()
4616 .unwrap();
4617 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4618 let (t0, t1) = curves[0].curve.domain();
4619 let (first, last) = (
4620 ParametricCurve::evaluate(&curves[0].curve, t0),
4621 ParametricCurve::evaluate(&curves[0].curve, t1),
4622 );
4623 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4624 for k in 0..=64 {
4625 let p = ParametricCurve::evaluate(
4626 &curves[0].curve,
4627 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4628 );
4629 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4630 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4631 assert!(
4632 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4633 "({x}, {y}): off by {on_rod}, {on_cone}"
4634 );
4635 }
4636 }
4637 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4640 let rod =
4641 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4642 .unwrap();
4643 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4644 .unwrap()
4645 .unwrap();
4646 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4647 }
4648 }
4649
4650 #[test]
4653 fn coaxial_cone_cylinder_defers_to_other_paths() {
4654 let cone = ConicalSurface::new(
4655 Point3::new(0.0, 0.0, 0.0),
4656 Vec3::new(0.0, 0.0, 1.0),
4657 std::f64::consts::FRAC_PI_4,
4658 )
4659 .unwrap();
4660 let cyl =
4661 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4662 .unwrap();
4663 assert!(
4664 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4665 .unwrap()
4666 .is_none()
4667 );
4668 }
4669
4670 #[test]
4671 fn oblique_cone_cylinder_defers_to_other_paths() {
4672 let cone = ConicalSurface::new(
4673 Point3::new(0.0, 0.0, 0.0),
4674 Vec3::new(0.0, 0.0, 1.0),
4675 std::f64::consts::FRAC_PI_4,
4676 )
4677 .unwrap();
4678 let cyl =
4679 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4680 .unwrap();
4681 assert!(
4682 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4683 .unwrap()
4684 .is_none()
4685 );
4686 }
4687
4688 #[test]
4689 fn plane_torus_lobe_closes_and_stays_on_surface() {
4690 use crate::traits::ParametricCurve;
4691 let (major, minor) = (10.0, 3.0);
4692 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4693
4694 for (n, d) in [
4698 (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), ] {
4702 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4703 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4704 for c in &curves {
4705 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4706 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4707 assert!(
4708 (p0 - p1).length() < 1e-7,
4709 "lobe not closed: gap={} (n={n:?} d={d})",
4710 (p0 - p1).length()
4711 );
4712 for k in 0..=64 {
4714 let t = f64::from(k) / 64.0;
4715 let p = ParametricCurve::evaluate(&c.curve, t);
4716 assert!(
4717 torus_implicit(p, major, minor).abs() < 1e-2,
4718 "off-surface point {p:?} implicit={}",
4719 torus_implicit(p, major, minor)
4720 );
4721 }
4722 }
4723 }
4724 }
4725
4726 #[test]
4731 fn plane_torus_inner_tangent_figure_eight_is_two_loops_through_its_node() {
4732 let (major, minor) = (10.0, 3.0);
4733 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4734 let curves =
4735 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4736 assert_eq!(curves.len(), 2, "one loop per lobe");
4737 let node = Point3::new(major - minor, 0.0, 0.0);
4738 let mut sides = Vec::new();
4739 for c in &curves {
4740 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
4741 assert!((first - node).length() < 1e-6, "starts at {first:?}");
4742 assert_eq!(first, last, "closes on the node");
4743 for p in c.points.iter().map(|p| p.point) {
4744 assert!((p.x() - node.x()).abs() < 1e-9, "off the plane at {p:?}");
4745 assert!(
4746 torus_implicit(p, major, minor).abs() < 1e-6,
4747 "off the torus at {p:?}"
4748 );
4749 }
4750 let y: f64 = c.points.iter().map(|p| p.point.y()).sum();
4751 sides.push(y > 0.0);
4752 assert!(
4753 c.points.iter().all(|p| p.point.y() * y >= -1e-9),
4754 "a lobe crosses y = 0"
4755 );
4756 }
4757 assert_ne!(sides[0], sides[1], "the lobes lie either side of y = 0");
4758 }
4759
4760 #[test]
4766 fn a_wall_just_off_the_inner_tangent_cuts_no_node() {
4767 let (major, minor) = (3.0, 1.0);
4768 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4769 for (x, loops) in [(2.0001, 1), (1.9999, 2)] {
4770 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), x).unwrap();
4771 assert_eq!(curves.len(), loops, "x = {x}");
4772 for c in &curves {
4773 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
4774 assert_eq!(first, last, "x = {x}: the loop closes");
4775 for p in c.points.iter().map(|p| p.point) {
4776 assert!((p.x() - x).abs() < 1e-9, "x = {x}: off the wall at {p:?}");
4777 assert!(
4778 torus_implicit(p, major, minor).abs() < 1e-6,
4779 "x = {x}: off the torus at {p:?}"
4780 );
4781 }
4782 }
4783 }
4784 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), 2.0001).unwrap();
4786 let low = curves[0]
4787 .points
4788 .iter()
4789 .map(|p| p.point)
4790 .filter(|p| p.y().abs() < 1e-6)
4791 .map(Point3::z)
4792 .collect::<Vec<_>>();
4793 let gap = (2.0001_f64 - 2.0)
4794 .mul_add(2.0, -(2.0001_f64 - 2.0).powi(2))
4795 .sqrt();
4796 assert!(
4797 low.iter().any(|z| (z - gap).abs() < 1e-6)
4798 && low.iter().any(|z| (z + gap).abs() < 1e-6),
4799 "turns at {low:?}, not at z = ±{gap}"
4800 );
4801 }
4802
4803 #[test]
4807 fn a_tilted_plane_tangent_inside_the_tube_cuts_two_loops_through_its_node() {
4808 let (major, minor) = (3.0, 1.0);
4809 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4810 for v0 in [1.7_f64, 2.0, 2.5, 4.0] {
4811 let normal = Vec3::new(v0.cos(), 0.0, v0.sin());
4812 let node = torus.evaluate(0.0, v0);
4813 let d = normal.dot(Vec3::new(node.x(), node.y(), node.z()));
4814 let curves = intersect_plane_torus(&torus, normal, d).unwrap();
4815 assert_eq!(curves.len(), 2, "v0 {v0}: one loop per lobe");
4816 for c in &curves {
4817 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
4818 assert!(
4819 (first - node).length() < 1e-6,
4820 "v0 {v0}: starts at {first:?}, not {node:?}"
4821 );
4822 assert_eq!(first, last, "v0 {v0}: closes on the node");
4823 for p in c.points.iter().map(|p| p.point) {
4824 let off = normal.dot(Vec3::new(p.x(), p.y(), p.z())) - d;
4825 assert!(off.abs() < 1e-9, "v0 {v0}: off the plane at {p:?}");
4826 assert!(
4827 torus_implicit(p, major, minor).abs() < 1e-6,
4828 "v0 {v0}: off the torus at {p:?}"
4829 );
4830 }
4831 }
4832 }
4833 }
4834
4835 #[test]
4840 fn plane_torus_wall_sections_close_into_their_loops() {
4841 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4842 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4843 for k in 1..200 {
4844 let (d, want) = match k.cmp(&100) {
4845 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4847 std::cmp::Ordering::Greater => (
4849 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4850 1,
4851 ),
4852 std::cmp::Ordering::Equal => continue,
4853 };
4854 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4855 let closed = loops
4856 .iter()
4857 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4858 .count();
4859 assert_eq!(
4860 (loops.len(), closed),
4861 (want, want),
4862 "R {major} r {minor}, wall at {d}"
4863 );
4864 }
4865 }
4866 }
4867
4868 #[test]
4872 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4873 let (major, minor) = (4.0, 1.5);
4874 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4875 for tilt in [0.03_f64, 0.08, 0.2] {
4876 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4877 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4878 assert_eq!(curves.len(), 2, "tilt {tilt}");
4879 for c in &curves {
4880 let (t0, t1) = c.curve.domain();
4881 let off = (0..=400)
4882 .map(|k| {
4883 let p = c
4884 .curve
4885 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4886 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4887 })
4888 .fold(0.0_f64, |m, e| m.max(e.abs()));
4889 assert!(
4890 off < 1e-6,
4891 "tilt {tilt}: fitted section {off} off the torus"
4892 );
4893 }
4894 }
4895 }
4896
4897 #[test]
4898 fn line_torus_box_edge_crossing_is_exact() {
4899 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4902 let ts = intersect_line_torus(
4903 &torus,
4904 Point3::new(6.0, -4.0, -5.0),
4905 Vec3::new(0.0, 0.0, 1.0),
4906 );
4907 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4909 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4910 let rho = 6.0_f64.hypot(4.0);
4911 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4912 assert!(
4913 (zs[0] - (-z_exp)).abs() < 1e-9,
4914 "z0={} exp={}",
4915 zs[0],
4916 -z_exp
4917 );
4918 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4919 for &t in &ts {
4921 let p = Point3::new(6.0, -4.0, -5.0 + t);
4922 let rho = p.x().hypot(p.y());
4923 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4924 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4925 }
4926 }
4927
4928 #[test]
4933 fn line_spindle_torus_roots_lie_on_their_tubes() {
4934 let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
4935 let origin = Point3::new(42.36, 18.31, 3.4);
4936 let dir = Vec3::new(
4937 0.573_576_436_351_046,
4938 0.740_535_693_464_567_5,
4939 0.350_889_803_483_932_2,
4940 );
4941 let ts = intersect_line_torus(&torus, origin, dir);
4942 let tube = |t: f64, across: f64| {
4943 let q = origin + dir * t;
4944 ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
4945 };
4946 for &t in &ts {
4947 assert!(
4948 tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
4949 "root {t} off both tubes in {ts:?}"
4950 );
4951 }
4952 for want in [0.124, 0.398] {
4953 assert!(
4954 ts.iter().any(|&t| (t - want).abs() < 1e-3),
4955 "no root near {want} in {ts:?}"
4956 );
4957 }
4958 }
4959
4960 #[test]
4961 fn line_torus_miss_and_tangent() {
4962 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4963 let miss = intersect_line_torus(
4965 &torus,
4966 Point3::new(20.0, 0.0, 0.0),
4967 Vec3::new(0.0, 0.0, 1.0),
4968 );
4969 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4970 let axis =
4972 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4973 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4974 }
4975
4976 #[test]
4977 fn dispatch_via_analytic_surface() {
4978 let cyl =
4979 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4980 .unwrap();
4981 let curves = intersect_plane_analytic(
4982 AnalyticSurface::Cylinder(&cyl),
4983 Vec3::new(0.0, 0.0, 1.0),
4984 0.0,
4985 )
4986 .unwrap();
4987 assert!(!curves.is_empty());
4988 }
4989
4990 #[test]
4991 fn perpendicular_cylinders_intersect() {
4992 let cyl_z =
4993 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4994 .unwrap();
4995 let cyl_x =
4996 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4997 .unwrap();
4998
4999 let curves = intersect_analytic_analytic(
5000 AnalyticSurface::Cylinder(&cyl_z),
5001 AnalyticSurface::Cylinder(&cyl_x),
5002 16,
5003 )
5004 .unwrap();
5005
5006 assert!(
5007 !curves.is_empty(),
5008 "perpendicular cylinders should intersect"
5009 );
5010
5011 for c in &curves {
5012 assert!(
5013 c.points.len() >= 2,
5014 "intersection curve should have >= 2 points, got {}",
5015 c.points.len()
5016 );
5017 }
5018 }
5019
5020 #[test]
5023 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
5024 let cyl_z =
5025 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5026 .unwrap();
5027 let cyl_x =
5028 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
5029 .unwrap();
5030 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5031 .unwrap()
5032 .unwrap();
5033 assert_eq!(curves.len(), 1);
5034 let curve = &curves[0].curve;
5035 let (t0, t1) = curve.domain();
5036 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
5037 let off = |p: Point3| {
5038 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
5039 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
5040 on_z.max(on_x)
5041 };
5042 let worst = (0..=400)
5043 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
5044 .fold(0.0, f64::max);
5045 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
5046 }
5047
5048 #[test]
5052 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
5053 let cyl_z =
5054 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5055 .unwrap();
5056 let cyl_x =
5057 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
5058 .unwrap();
5059 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5060 .unwrap()
5061 .expect("the thin cylinder's sweep finds the loop");
5062 assert_eq!(curves.len(), 1);
5063 }
5064
5065 #[test]
5066 fn sphere_cylinder_intersect() {
5067 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5068 let cyl =
5069 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5070 .unwrap();
5071
5072 let curves = intersect_analytic_analytic(
5073 AnalyticSurface::Sphere(&sphere),
5074 AnalyticSurface::Cylinder(&cyl),
5075 16,
5076 )
5077 .unwrap();
5078
5079 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
5083 }
5084
5085 #[test]
5086 fn exact_sphere_cylinder_coaxial_two_circles() {
5087 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5090 let cyl =
5091 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5092 .unwrap();
5093 let circles = exact_sphere_cylinder(&sphere, &cyl)
5094 .unwrap()
5095 .expect("coaxial case returns Some");
5096 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
5097 let mut zs: Vec<f64> = circles
5098 .iter()
5099 .filter_map(|c| match c {
5100 ExactIntersectionCurve::Circle(circle) => {
5101 assert!(
5102 (circle.radius() - 3.0).abs() < 1e-9,
5103 "rim radius == cyl radius"
5104 );
5105 Some(circle.center().z())
5106 }
5107 _ => None,
5108 })
5109 .collect();
5110 assert_eq!(zs.len(), 2, "both sections must be exact circles");
5111 zs.sort_by(f64::total_cmp);
5112 let z = 27.0_f64.sqrt();
5113 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
5114 }
5115
5116 #[test]
5117 fn exact_sphere_cylinder_non_coaxial_defers() {
5118 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5120 let cyl =
5121 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5122 .unwrap();
5123 assert!(
5124 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
5125 "non-coaxial sphere/cylinder defers to the marcher"
5126 );
5127 }
5128
5129 #[test]
5130 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
5131 let cone = ConicalSurface::new(
5133 Point3::new(0.0, 0.0, 3.0),
5134 Vec3::new(0.0, 0.0, -1.0),
5135 2.0_f64.atan(),
5136 )
5137 .unwrap();
5138 for (height, radius, count) in [
5139 (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), ] {
5145 let centre = Point3::new(0.0, 0.0, height);
5146 let ball = SphericalSurface::new(centre, radius).unwrap();
5147 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
5148 let circles = circles_of(&curves);
5149 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
5150 for circle in circles {
5151 for k in 0..16 {
5152 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5153 let on_ball = (p - centre).length() - radius;
5154 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
5155 assert!(
5156 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
5157 "ball at {height}: off by {on_ball}, {on_cone}"
5158 );
5159 }
5160 }
5161 }
5162 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
5163 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
5164 let wide =
5167 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
5168 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
5169 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
5170 let circles = circles_of(&curves);
5171 assert_eq!(circles.len(), 1, "the touch");
5172 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
5173 assert!(
5174 (circles[0].radius() - touch).abs() < 1e-3,
5175 "{}",
5176 circles[0].radius()
5177 );
5178 }
5179
5180 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5182 curves
5183 .iter()
5184 .filter_map(|c| match c {
5185 ExactIntersectionCurve::Circle(circle) => Some(circle),
5186 _ => None,
5187 })
5188 .collect()
5189 }
5190
5191 fn worst_off(
5194 circles: &[&Circle3D],
5195 torus: &ToroidalSurface,
5196 other: impl Fn(Point3) -> f64,
5197 ) -> f64 {
5198 let mut worst = 0.0_f64;
5199 for circle in circles {
5200 for k in 0..16 {
5201 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5202 let q = p - torus.center();
5203 let along = q.dot(torus.z_axis());
5204 let rho = (q - torus.z_axis() * along).length();
5205 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5206 worst = worst.max(off).max(other(p).abs());
5207 }
5208 }
5209 worst
5210 }
5211
5212 #[test]
5213 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5214 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5215 for height in [0.0, 1.0] {
5216 let centre = Point3::new(0.0, 0.0, height);
5217 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5218 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5219 let circles = circles_of(&curves);
5220 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5221 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5222 assert!(worst < 1e-9, "height {height}: {worst}");
5223 }
5224 }
5225
5226 #[test]
5227 fn exact_sphere_torus_misses_touches_and_defers() {
5228 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5229 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5230 assert!(
5231 exact_sphere_torus(&ball(0.0, 1.0), &torus)
5232 .unwrap()
5233 .unwrap()
5234 .is_empty(),
5235 "a small ball in the hole misses"
5236 );
5237 assert!(
5238 exact_sphere_torus(&ball(0.0, 2.5), &torus)
5239 .unwrap()
5240 .is_none(),
5241 "a ball touching the inner equator defers"
5242 );
5243 assert!(
5244 exact_sphere_torus(&ball(1.0, 3.0), &torus)
5245 .unwrap()
5246 .is_none(),
5247 "a ball off the axis defers"
5248 );
5249 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5250 Point3::new(0.0, 0.0, 0.0),
5251 1.0,
5252 2.0,
5253 Vec3::new(0.0, 0.0, 1.0),
5254 Vec3::new(1.0, 0.0, 0.0),
5255 )
5256 .unwrap();
5257 assert!(
5258 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5259 .unwrap()
5260 .is_none()
5261 );
5262 }
5263
5264 #[test]
5265 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5266 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5267 let z = Vec3::new(0.0, 0.0, 1.0);
5268 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5269 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5270 let circles = circles_of(&curves);
5271 assert_eq!((curves.len(), circles.len()), (2, 2));
5272 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5273 assert!(worst < 1e-9, "{worst}");
5274 assert!(
5275 exact_cylinder_torus(&rod(2.0), &torus)
5276 .unwrap()
5277 .unwrap()
5278 .is_empty(),
5279 "a rod clear in the hole misses"
5280 );
5281 for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5282 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5283 let circles = circles_of(&curves);
5284 assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5285 assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5286 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5287 assert!(worst < 1e-9, "{label}: {worst}");
5288 }
5289 assert!(
5290 exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5291 .unwrap()
5292 .unwrap()
5293 .is_empty(),
5294 "a wall clear of the tube by more than the tolerance misses it"
5295 );
5296 assert_eq!(
5297 exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5298 .unwrap()
5299 .unwrap()
5300 .len(),
5301 2,
5302 "a wall into the tube by more than the tolerance crosses it twice"
5303 );
5304 for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5308 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5309 assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5310 }
5311 let tilted =
5312 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5313 .unwrap();
5314 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5315 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5316 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5317 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5318 Point3::new(0.0, 0.0, 0.0),
5319 1.0,
5320 2.0,
5321 z,
5322 Vec3::new(1.0, 0.0, 0.0),
5323 )
5324 .unwrap();
5325 assert!(
5326 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5327 "a spindle torus's inner lemon also meets the rod"
5328 );
5329 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5332 Point3::new(0.0, 0.0, 4.7),
5333 0.1,
5334 2.45,
5335 z,
5336 Vec3::new(1.0, 0.0, 0.0),
5337 )
5338 .unwrap();
5339 let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5340 let circles = circles_of(&curves);
5341 assert_eq!(circles.len(), 1);
5342 assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5343 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5344 assert!(worst < 1e-9, "{worst}");
5345 let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5346 let circles = circles_of(&curves);
5347 assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5348 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5349 assert!(worst < 1e-9, "{worst}");
5350 }
5351
5352 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5355 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5356 let cyl =
5357 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5358 .unwrap();
5359 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5360 .unwrap()
5361 .unwrap();
5362 let mut worst: f64 = 0.0;
5363 for c in &curves {
5364 for ip in &c.points {
5365 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5366 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5367 worst = worst
5368 .max((on_sphere - ip.point).length())
5369 .max((on_cylinder - ip.point).length());
5370 }
5371 let (t0, t1) = c.curve.domain();
5372 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5373 for k in 0..=400 {
5374 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5375 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5376 let on_cylinder = ((p.x() - cylinder_origin.x())
5377 .hypot(p.y() - cylinder_origin.y())
5378 - cylinder_radius)
5379 .abs();
5380 worst = worst.max(on_sphere).max(on_cylinder);
5381 }
5382 }
5383 (curves.len(), worst)
5384 }
5385
5386 #[test]
5389 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5390 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5391 assert_eq!(count, 2);
5392 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5393 }
5394
5395 #[test]
5397 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5398 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5399 assert_eq!(count, 1);
5400 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5401 }
5402
5403 #[test]
5404 fn disjoint_cylinders_no_intersection() {
5405 let cyl_a =
5406 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5407 .unwrap();
5408 let cyl_b =
5409 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5410 .unwrap();
5411
5412 let curves = intersect_analytic_analytic(
5413 AnalyticSurface::Cylinder(&cyl_a),
5414 AnalyticSurface::Cylinder(&cyl_b),
5415 16,
5416 )
5417 .unwrap();
5418
5419 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5420 }
5421
5422 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5426 use crate::traits::ParametricCurve;
5427 match curve {
5428 ExactIntersectionCurve::Circle(c) => (0..=64)
5429 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5430 .collect(),
5431 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5432 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5433 .collect(),
5434 ExactIntersectionCurve::Points(pts) => pts.clone(),
5435 }
5436 }
5437
5438 fn assert_on_plane_and_cone(
5441 curves: &[ExactIntersectionCurve],
5442 cone: &ConicalSurface,
5443 n: Vec3,
5444 d: f64,
5445 z_bound: (f64, f64),
5446 ) {
5447 assert!(!curves.is_empty(), "expected at least one section curve");
5448 let mut total = 0;
5449 for curve in curves {
5450 for p in collect_points(curve) {
5451 total += 1;
5452 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5453 assert!(
5454 plane_err < 1e-9,
5455 "point off plane by {plane_err:.2e}: {p:?}"
5456 );
5457 let (u, v) = cone.project_point(p);
5458 let q = cone.evaluate(u, v);
5459 let cone_err =
5460 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5461 .sqrt();
5462 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5463 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5464 assert!(
5465 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5466 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5467 p.z()
5468 );
5469 }
5470 }
5471 assert!(total >= 8, "too few section points ({total})");
5472 }
5473
5474 #[test]
5475 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5476 let cone = ConicalSurface::new(
5480 Point3::new(0.0, 0.0, 0.0),
5481 Vec3::new(0.0, 0.0, 1.0),
5482 std::f64::consts::FRAC_PI_4,
5483 )
5484 .unwrap();
5485 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5486 let d = n.z() * 5.0;
5488 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5489 assert!(
5490 curves
5491 .iter()
5492 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5493 "oblique steep plane × cone must yield an exact Ellipse"
5494 );
5495 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5497 }
5498
5499 #[test]
5500 fn oblique_plane_cone_wrong_nappe_is_empty() {
5501 let cone = ConicalSurface::new(
5505 Point3::new(0.0, 0.0, 0.0),
5506 Vec3::new(0.0, 0.0, 1.0),
5507 std::f64::consts::FRAC_PI_4,
5508 )
5509 .unwrap();
5510 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5511 let d = n.z() * -5.0;
5512 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5513 assert!(
5514 curves.is_empty(),
5515 "plane on the phantom-nappe side must yield no real curve, got {}",
5516 curves.len()
5517 );
5518 }
5519
5520 #[test]
5521 fn oblique_plane_cone_parabola_on_both_single_branch() {
5522 let cone = ConicalSurface::new(
5525 Point3::new(0.0, 0.0, 0.0),
5526 Vec3::new(0.0, 0.0, 1.0),
5527 std::f64::consts::FRAC_PI_4,
5528 )
5529 .unwrap();
5530 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5531 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5533 assert_eq!(
5534 curves.len(),
5535 1,
5536 "a parabola is a single branch, got {}",
5537 curves.len()
5538 );
5539 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5541 }
5542
5543 #[test]
5544 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5545 let cone = ConicalSurface::new(
5553 Point3::new(-59.0, -59.0, 15.85),
5554 Vec3::new(0.0, 0.0, -1.0),
5555 std::f64::consts::FRAC_PI_4,
5556 )
5557 .unwrap();
5558 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5559 let d = -58.360_56;
5560 let cos_theta = n.dot(cone.axis()).abs();
5561 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5562 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5563 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5566 for c in &curves {
5568 assert!(
5569 matches!(c, ExactIntersectionCurve::Points(_)),
5570 "hyperbola must be sampled Points, not a closed conic"
5571 );
5572 }
5573 }
5574
5575 #[test]
5579 fn sphere_sphere_meets_in_a_circle_on_both() {
5580 let ball = |x: f64, y: f64, z: f64, r: f64| {
5581 SphericalSurface::new(Point3::new(x, y, z), r).unwrap()
5582 };
5583 let (a, b) = (ball(0.0, 0.0, 0.0, 2.0), ball(1.6, 0.3, 0.2, 1.5));
5584 let curves = exact_sphere_sphere(&a, &b).unwrap().unwrap();
5585 assert_eq!(curves.len(), 1);
5586 let circle = match &curves[0] {
5587 ExactIntersectionCurve::Circle(circle) => circle,
5588 other => unreachable!("expected a circle, got {other:?}"),
5589 };
5590 for k in 0..16 {
5591 let p = circle.evaluate(std::f64::consts::TAU * f64::from(k) / 16.0);
5592 assert!(((p - a.center()).length() - 2.0).abs() < 1e-12);
5593 assert!(((p - b.center()).length() - 1.5).abs() < 1e-12);
5594 }
5595 assert!(
5596 exact_sphere_sphere(&a, &ball(4.0, 0.0, 0.0, 1.5))
5597 .unwrap()
5598 .unwrap()
5599 .is_empty()
5600 );
5601 assert!(
5602 exact_sphere_sphere(&a, &ball(0.2, 0.0, 0.0, 1.0))
5603 .unwrap()
5604 .unwrap()
5605 .is_empty()
5606 );
5607 assert!(
5608 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 1.0))
5609 .unwrap()
5610 .unwrap()
5611 .is_empty()
5612 );
5613 assert!(
5614 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 2.0))
5615 .unwrap()
5616 .is_none()
5617 );
5618
5619 let radius = |p: &SphericalSurface, q: &SphericalSurface| -> f64 {
5622 match exact_sphere_sphere(p, q).unwrap().unwrap().as_slice() {
5623 [ExactIntersectionCurve::Circle(c)] => c.radius(),
5624 other => unreachable!("expected one circle, got {other:?}"),
5625 }
5626 };
5627 let tiny = ball(2.0, 0.0, 0.0, 1e-5);
5628 assert!((radius(&a, &tiny) - 1e-5).abs() < 1e-9);
5629 assert!((radius(&tiny, &a) - 1e-5).abs() < 1e-9);
5630 let near = radius(&ball(0.0, 0.0, 0.0, 2.0), &ball(3.5, 0.0, 0.0, 1.500_001));
5631 let far = radius(
5632 &ball(1e10, 0.0, 0.0, 2.0),
5633 &ball(1e10 + 3.5, 0.0, 0.0, 1.500_001),
5634 );
5635 assert!(near > 1e-3);
5636 assert!((far - near).abs() < 1e-9 * near.max(1.0));
5637 }
5638}