1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::aabb::Aabb3;
11use crate::curves::{Circle3D, Ellipse3D, Line3D};
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
31#[derive(Debug, Clone)]
33#[non_exhaustive]
34pub enum PlaneSection {
35 Circle(Circle3D),
37 Ellipse(Ellipse3D),
39 Line {
43 line: Line3D,
45 ray: bool,
47 },
48 Point(Point3),
50 Points(Vec<Point3>),
52}
53
54impl From<ExactIntersectionCurve> for PlaneSection {
55 fn from(curve: ExactIntersectionCurve) -> Self {
56 match curve {
57 ExactIntersectionCurve::Circle(circle) => Self::Circle(circle),
58 ExactIntersectionCurve::Ellipse(ellipse) => Self::Ellipse(ellipse),
59 ExactIntersectionCurve::Points(points) => Self::Points(points),
60 }
61 }
62}
63
64pub fn exact_plane_section(
77 surface: AnalyticSurface<'_>,
78 normal: Vec3,
79 d: f64,
80) -> Result<Vec<PlaneSection>, MathError> {
81 match surface {
82 AnalyticSurface::Cylinder(cyl) if normal.dot(cyl.axis()).abs() < 1e-10 => {
83 return plane_cylinder_rulings(cyl, normal, d);
84 }
85 AnalyticSurface::Cone(cone) if plane_holds_apex(cone, normal, d) => {
86 return plane_cone_apex_rulings(cone, normal);
87 }
88 AnalyticSurface::Sphere(sphere) => {
89 let h = dot_np(normal, sphere.center()) - d;
90 let r = sphere.radius();
91 if (r - h.abs()).abs() <= TOUCH * r {
92 return Ok(vec![PlaneSection::Point(Point3::new(
93 h.mul_add(-normal.x(), sphere.center().x()),
94 h.mul_add(-normal.y(), sphere.center().y()),
95 h.mul_add(-normal.z(), sphere.center().z()),
96 ))]);
97 }
98 }
99 AnalyticSurface::Cylinder(_) | AnalyticSurface::Cone(_) | AnalyticSurface::Torus(_) => {}
100 }
101 Ok(exact_plane_analytic(surface, normal, d)?
102 .into_iter()
103 .map(PlaneSection::from)
104 .collect())
105}
106
107const TOUCH: f64 = 4.0 * f64::EPSILON;
110
111fn plane_cylinder_rulings(
114 cyl: &CylindricalSurface,
115 normal: Vec3,
116 d: f64,
117) -> Result<Vec<PlaneSection>, MathError> {
118 let (a, b) = (normal.dot(cyl.x_axis()), normal.dot(cyl.y_axis()));
121 let amp = a.hypot(b);
122 if amp < 1e-12 {
123 return Ok(vec![]);
124 }
125 let ratio = (d - dot_np(normal, cyl.origin())) / (cyl.radius() * amp);
126 if ratio.abs() > 1.0 + TOUCH {
127 return Ok(vec![]);
128 }
129 let phi = b.atan2(a);
130 let spread = ratio.clamp(-1.0, 1.0).acos();
131 let at = if ratio.abs() >= 1.0 - TOUCH {
132 vec![phi + spread]
133 } else {
134 vec![phi + spread, phi - spread]
135 };
136 at.into_iter()
137 .map(|u| {
138 Ok(PlaneSection::Line {
139 line: Line3D::new(cyl.evaluate(u, 0.0), cyl.axis())?,
140 ray: false,
141 })
142 })
143 .collect()
144}
145
146fn plane_holds_apex(cone: &ConicalSurface, normal: Vec3, d: f64) -> bool {
148 let apex = cone.apex() - Point3::new(0.0, 0.0, 0.0);
149 (normal.dot(apex) - d).abs() <= TOUCH * (1.0 + apex.length() + d.abs())
150}
151
152fn plane_cone_apex_rulings(
156 cone: &ConicalSurface,
157 normal: Vec3,
158) -> Result<Vec<PlaneSection>, MathError> {
159 let (sin_a, cos_a) = cone.half_angle().sin_cos();
162 let a = cos_a * normal.dot(cone.x_axis());
163 let b = cos_a * normal.dot(cone.y_axis());
164 let c = sin_a * normal.dot(cone.axis());
165 let amp = a.hypot(b);
166 let apex = cone.apex();
167 if amp < 1e-12 * c.abs().max(1.0) {
168 return Ok(vec![PlaneSection::Point(apex)]);
169 }
170 let ratio = -c / amp;
171 if ratio.abs() > 1.0 + TOUCH {
172 return Ok(vec![PlaneSection::Point(apex)]);
173 }
174 let phi = b.atan2(a);
175 let spread = ratio.clamp(-1.0, 1.0).acos();
176 let at = if ratio.abs() >= 1.0 - TOUCH {
177 vec![phi + spread]
178 } else {
179 vec![phi + spread, phi - spread]
180 };
181 at.into_iter()
182 .map(|u| {
183 Ok(PlaneSection::Line {
184 line: Line3D::new(apex, cone.evaluate(u, 1.0) - apex)?,
185 ray: true,
186 })
187 })
188 .collect()
189}
190
191pub fn exact_plane_analytic(
206 surface: AnalyticSurface<'_>,
207 plane_normal: Vec3,
208 plane_d: f64,
209) -> Result<Vec<ExactIntersectionCurve>, MathError> {
210 exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
211}
212
213pub fn exact_plane_analytic_reaching(
221 surface: AnalyticSurface<'_>,
222 plane_normal: Vec3,
223 plane_d: f64,
224 reach: f64,
225) -> Result<Vec<ExactIntersectionCurve>, MathError> {
226 match surface {
227 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
228 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
229 AnalyticSurface::Cone(cone) if plane_holds_apex(cone, plane_normal, plane_d) => Ok(vec![]),
230 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
231 AnalyticSurface::Torus(torus) => {
232 if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
233 return Ok(circles);
234 }
235 if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
236 return Ok(loops
237 .into_iter()
238 .map(ExactIntersectionCurve::Points)
239 .collect());
240 }
241 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
243 Ok(chains
244 .into_iter()
245 .map(ExactIntersectionCurve::Points)
246 .collect())
247 }
248 }
249}
250
251fn exact_plane_torus(
265 torus: &ToroidalSurface,
266 normal: Vec3,
267 d: f64,
268) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
269 let len = normal.length();
270 let n = normal.normalize()?;
271 let d = d / len;
272 let axis = torus.z_axis();
273 let center = torus.center();
274 let (big, small) = (torus.major_radius(), torus.minor_radius());
275 let height = d - dot_np(n, center);
276 let along = n.dot(axis);
277 if along.abs() > 1.0 - 1e-10 {
278 if height.abs() >= small - 1e-10 * small {
279 if height.abs() > small + 1e-10 * small {
280 return Ok(Some(Vec::new()));
281 }
282 if big <= 1e-10 * small
289 || axis.cross(n).length() > 1e-12
290 || (small - height.abs()).abs() > crate::tolerance::Tolerance::default().linear
291 {
292 return Ok(None);
293 }
294 let middle = center + n * height;
295 return Ok(Some(vec![ExactIntersectionCurve::Circle(Circle3D::new(
296 middle, n, big,
297 )?)]));
298 }
299 let reach = small.mul_add(small, -(height * height)).sqrt();
300 if big - reach <= 1e-10 * big {
301 return Ok(None);
302 }
303 let middle = center + n * height;
304 return Ok(Some(vec![
305 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
306 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
307 ]));
308 }
309 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
310 let out = axis.cross(n).normalize()?;
311 return Ok(Some(vec![
312 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
313 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
314 ]));
315 }
316 Ok(None)
317}
318
319fn exact_plane_cylinder(
325 cyl: &CylindricalSurface,
326 normal: Vec3,
327 d: f64,
328) -> Result<Vec<ExactIntersectionCurve>, MathError> {
329 let axis = cyl.axis();
330 let cos_theta = normal.dot(axis).abs();
331 let r = cyl.radius();
332
333 if cos_theta < 1e-10 {
334 return Ok(vec![]);
337 }
338
339 let n_dot_axis = normal.dot(axis);
342 let n_dot_origin = dot_np(normal, cyl.origin());
343 let t = (d - n_dot_origin) / n_dot_axis;
344 let center_on_axis = Point3::new(
345 cyl.origin().x() + t * axis.x(),
346 cyl.origin().y() + t * axis.y(),
347 cyl.origin().z() + t * axis.z(),
348 );
349
350 if cos_theta > 1.0 - 1e-10 {
351 let circle = Circle3D::new(center_on_axis, normal, r)?;
353 Ok(vec![ExactIntersectionCurve::Circle(circle)])
354 } else {
355 let semi_minor = r;
359 let semi_major = r / cos_theta;
360
361 let axis_proj = Vec3::new(
365 axis.x() - n_dot_axis * normal.x(),
366 axis.y() - n_dot_axis * normal.y(),
367 axis.z() - n_dot_axis * normal.z(),
368 );
369 let u_axis = axis_proj.normalize()?;
370 let v_axis = normal.cross(u_axis);
371
372 let ellipse = Ellipse3D::with_axes(
373 center_on_axis,
374 normal,
375 semi_major,
376 semi_minor,
377 u_axis,
378 v_axis,
379 )?;
380 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
381 }
382}
383
384fn exact_plane_sphere(
388 sphere: &SphericalSurface,
389 normal: Vec3,
390 d: f64,
391) -> Result<Vec<ExactIntersectionCurve>, MathError> {
392 let h = dot_np(normal, sphere.center()) - d;
393 let r = sphere.radius();
394
395 if r - h.abs() <= TOUCH * r {
398 return Ok(vec![]);
399 }
400
401 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
402 let circle_center = Point3::new(
403 h.mul_add(-normal.x(), sphere.center().x()),
404 h.mul_add(-normal.y(), sphere.center().y()),
405 h.mul_add(-normal.z(), sphere.center().z()),
406 );
407
408 let circle = Circle3D::new(circle_center, normal, circle_r)?;
409 Ok(vec![ExactIntersectionCurve::Circle(circle)])
410}
411
412pub fn exact_sphere_sphere(
424 s1: &SphericalSurface,
425 s2: &SphericalSurface,
426) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
427 let delta = s2.center() - s1.center();
428 let d = delta.length();
429 let (r1, r2) = (s1.radius(), s2.radius());
430 let scale = r1.max(r2);
431 if d <= 1e-12 * scale {
432 return Ok(((r1 - r2).abs() > 1e-12 * scale).then(Vec::new));
434 }
435 let normal = delta * (1.0 / d);
436 let (centre, toward, r_small, r_big) = if r1 <= r2 {
439 (s1.center(), normal, r1, r2)
440 } else {
441 (s2.center(), -normal, r2, r1)
442 };
443 let along = d.mul_add(d, r_small.mul_add(r_small, -(r_big * r_big))) / (2.0 * d);
444 if along.abs() > r_small - 1e-10 * scale.max(1.0) {
445 return Ok(Some(Vec::new()));
446 }
447 let radius = r_small.mul_add(r_small, -(along * along)).sqrt();
448 let circle = Circle3D::new(centre + toward * along, normal, radius)?;
449 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
450}
451
452fn exact_plane_cone(
461 cone: &ConicalSurface,
462 normal: Vec3,
463 d: f64,
464 reach: f64,
465) -> Result<Vec<ExactIntersectionCurve>, MathError> {
466 let axis = cone.axis();
467 let cos_theta = normal.dot(axis).abs();
468 let half_angle = cone.half_angle();
469
470 if cos_theta > 1.0 - 1e-10 {
471 let n_dot_axis = normal.dot(axis);
474 let n_dot_apex = dot_np(normal, cone.apex());
475 let t = (d - n_dot_apex) / n_dot_axis;
476
477 if t.abs() < 1e-10 {
482 return Ok(vec![]);
483 }
484
485 let center = Point3::new(
486 cone.apex().x() + t * axis.x(),
487 cone.apex().y() + t * axis.y(),
488 cone.apex().z() + t * axis.z(),
489 );
490 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
494 if circle_r < 1e-15 {
495 return Ok(vec![]);
496 }
497
498 let circle = Circle3D::new(center, normal, circle_r)?;
499 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
500 }
501
502 let c = normal.dot(axis);
514 let p2 = (1.0 - c * c).max(0.0);
515 let p = p2.sqrt();
516 let k = half_angle.sin().powi(2);
517 let a_coeff = p2 - k;
518
519 let m = Vec3::new(
521 axis.x() - c * normal.x(),
522 axis.y() - c * normal.y(),
523 axis.z() - c * normal.z(),
524 );
525 let m_len = m.length();
526 if m_len < 1e-12 {
527 let chains = sample_plane_cone(cone, normal, d, reach)?;
530 return Ok(chains
531 .into_iter()
532 .map(ExactIntersectionCurve::Points)
533 .collect());
534 }
535 let e1 = m * (1.0 / m_len);
536 let e2 = normal.cross(e1);
537 let apex = cone.apex();
538 let e = d - dot_np(normal, apex);
539
540 if a_coeff < -1e-9 {
543 let abs_a = -a_coeff; if e * c < 0.0 {
550 return Ok(vec![]);
551 }
552 let s_c = e * c * p / abs_a;
555 let rhs = e * e * k * (1.0 - k) / abs_a;
556 if rhs <= 0.0 {
557 return Ok(vec![]);
558 }
559 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
562 return Ok(vec![]);
563 }
564 let center = apex + normal * e + e1 * s_c;
565 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
566 (semi_s, semi_t, e1, e2)
567 } else {
568 (semi_t, semi_s, e2, e1)
569 };
570 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
571 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
572 }
573
574 let chains = sample_plane_cone(cone, normal, d, reach)?;
577 Ok(chains
578 .into_iter()
579 .map(ExactIntersectionCurve::Points)
580 .collect())
581}
582
583#[allow(clippy::many_single_char_names)]
598pub fn plane_cone_conic_arc(
599 cone: &ConicalSurface,
600 normal: Vec3,
601 d: f64,
602 from: Point3,
603 to: Point3,
604) -> Result<Option<NurbsCurve>, MathError> {
605 let len = normal.length();
606 if len < 1e-15 {
607 return Err(MathError::ZeroVector);
608 }
609 let (normal, d) = (normal * (1.0 / len), d / len);
610 let axis = cone.axis();
611 let c = normal.dot(axis);
612 let p2 = (1.0 - c * c).max(0.0);
613 let p = p2.sqrt();
614 let k = cone.half_angle().sin().powi(2);
615 let a_coeff = p2 - k;
616 let m = Vec3::new(
617 axis.x() - c * normal.x(),
618 axis.y() - c * normal.y(),
619 axis.z() - c * normal.z(),
620 );
621 let m_len = m.length();
622 if m_len < 1e-12 || a_coeff < -1e-9 {
623 return Ok(None);
624 }
625 let e1 = m * (1.0 / m_len);
626 let e2 = normal.cross(e1);
627 let apex = cone.apex();
628 let e = d - dot_np(normal, apex);
629 let origin = apex + normal * e;
630 let plane_st = |q: Point3| {
631 let w = q - origin;
632 (w.dot(e1), w.dot(e2))
633 };
634 let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
635 let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
636 if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
637 return Ok(None);
638 }
639 let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
640 let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
641 let (control, weights) = if a_coeff.abs() <= 1e-9 {
642 let lin = 2.0 * e * c * p;
644 if lin.abs() < 1e-12 * scale {
645 return Ok(None);
646 }
647 let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
648 if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
649 || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
650 {
651 return Ok(None);
652 }
653 let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
654 (vec![from, mid, to], vec![1.0; 3])
655 } else {
656 let s_c = -e * c * p / a_coeff;
658 let r = e * e * k * (1.0 - k) / a_coeff;
659 if r <= 0.0 {
660 return Ok(None);
661 }
662 let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
663 let (x0, x1) = (s0 - s_c, s1 - s_c);
664 if x0 * x1 <= 0.0 {
665 return Ok(None);
666 }
667 let side = x0.signum();
668 let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
669 let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
670 if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
671 return Ok(None);
672 }
673 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
674 let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
675 let mut control = vec![from];
676 let mut weights = vec![1.0];
677 for i in 0..pieces {
678 #[allow(clippy::cast_precision_loss)]
679 let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
680 let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
681 let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
682 let w = half.cosh();
683 control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
684 weights.push(w);
685 control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
686 weights.push(1.0);
687 }
688 (control, weights)
689 };
690 let pieces = (control.len() - 1) / 2;
691 let mut knots = vec![0.0; 3];
692 for i in 1..pieces {
693 #[allow(clippy::cast_precision_loss)]
694 knots.extend([i as f64; 2]);
695 }
696 #[allow(clippy::cast_precision_loss)]
697 knots.extend([pieces as f64; 3]);
698 let curve = NurbsCurve::new(2, knots, control, weights)?;
699 let (sin_a, cos_a) = cone.half_angle().sin_cos();
704 let off_cone = |q: Point3| {
705 let w = q - apex;
706 let h = w.dot(axis);
707 (w - axis * h)
708 .length()
709 .mul_add(sin_a, -(h.abs() * cos_a))
710 .abs()
711 };
712 for i in 0..pieces {
713 for f in [0.25, 0.5, 0.75] {
714 #[allow(clippy::cast_precision_loss)]
715 if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
716 return Ok(None);
717 }
718 }
719 }
720 Ok(Some(curve))
721}
722
723#[derive(Clone, Copy)]
725pub enum AnalyticSurface<'a> {
726 Cylinder(&'a CylindricalSurface),
728 Cone(&'a ConicalSurface),
730 Sphere(&'a SphericalSurface),
732 Torus(&'a ToroidalSurface),
734}
735
736fn dot_np(n: Vec3, p: Point3) -> f64 {
738 n.dot(Vec3::new(p.x(), p.y(), p.z()))
739}
740
741pub fn intersect_plane_analytic(
749 surface: AnalyticSurface<'_>,
750 normal: Vec3,
751 d: f64,
752) -> Result<Vec<IntersectionCurve>, MathError> {
753 match surface {
754 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
755 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
756 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
757 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
758 }
759}
760
761pub fn sample_plane_analytic(
772 surface: AnalyticSurface<'_>,
773 normal: Vec3,
774 d: f64,
775) -> Result<Vec<Vec<Point3>>, MathError> {
776 match surface {
777 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
778 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
779 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
780 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
781 }
782}
783
784#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
786fn sample_plane_cylinder(
787 cyl: &CylindricalSurface,
788 normal: Vec3,
789 d: f64,
790) -> Result<Vec<Vec<Point3>>, MathError> {
791 let n_samples = 64_usize;
792 let mut points = Vec::with_capacity(n_samples + 1);
793
794 for i in 0..=n_samples {
795 let u = TAU * (i as f64) / (n_samples as f64);
796 let base = cyl.evaluate(u, 0.0);
797 let n_dot_axis = normal.dot(cyl.axis());
798 let n_dot_base = dot_np(normal, base);
799
800 if n_dot_axis.abs() < 1e-12 {
801 if (n_dot_base - d).abs() < 1e-6 {
802 points.push(base);
803 }
804 } else {
805 let v = (d - n_dot_base) / n_dot_axis;
806 if v.abs() <= 100.0 {
807 points.push(cyl.evaluate(u, v));
808 }
809 }
810 }
811
812 if points.len() < 2 {
813 Ok(vec![])
814 } else {
815 Ok(vec![points])
816 }
817}
818
819#[allow(clippy::cast_precision_loss)]
821fn sample_plane_sphere(
822 sphere: &SphericalSurface,
823 normal: Vec3,
824 d: f64,
825) -> Result<Vec<Vec<Point3>>, MathError> {
826 let h = dot_np(normal, sphere.center()) - d;
827 let r = sphere.radius();
828
829 if h.abs() > r - 1e-10 {
830 return Ok(vec![]);
831 }
832
833 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
834 let circle_center = Point3::new(
835 h.mul_add(-normal.x(), sphere.center().x()),
836 h.mul_add(-normal.y(), sphere.center().y()),
837 h.mul_add(-normal.z(), sphere.center().z()),
838 );
839
840 let basis = Frame3::from_normal(circle_center, normal)?;
841 let u_dir = basis.x;
842 let v_dir = basis.y;
843
844 let n_samples = 64_usize;
845 let mut points = Vec::with_capacity(n_samples + 1);
846
847 for i in 0..=n_samples {
848 let theta = TAU * (i as f64) / (n_samples as f64);
849 let (sin_t, cos_t) = theta.sin_cos();
850 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
851 }
852
853 Ok(vec![points])
854}
855
856#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
868fn sample_plane_cone(
869 cone: &ConicalSurface,
870 normal: Vec3,
871 d: f64,
872 reach: f64,
873) -> Result<Vec<Vec<Point3>>, MathError> {
874 let apex = cone.apex();
875 let n_dot_apex = dot_np(normal, apex);
876 let e = d - n_dot_apex;
877
878 let n_samples = 512_usize;
882 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
883 let mut v_min = f64::INFINITY;
884 for i in 0..n_samples {
885 let u = TAU * (i as f64) / (n_samples as f64);
886 let g = cone.evaluate(u, 1.0) - apex;
887 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
888 if n_dot_g.abs() < 1e-12 {
889 vs.push(None);
890 continue;
891 }
892 let v = e / n_dot_g;
893 if v >= -1e-12 {
894 let v = v.max(0.0);
895 v_min = v_min.min(v);
896 vs.push(Some(v));
897 } else {
898 vs.push(None);
899 }
900 }
901
902 if !v_min.is_finite() {
903 return Ok(Vec::new());
904 }
905
906 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
915
916 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
919
920 let point_at = |u: f64, v: f64| -> Point3 {
921 let g = cone.evaluate(u, 1.0) - apex;
922 apex + g * v
923 };
924 #[allow(clippy::cast_precision_loss)]
925 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
926 let n_dot_g_at = |u: f64| -> f64 {
927 let g = cone.evaluate(u, 1.0) - apex;
928 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
929 };
930
931 if kept.iter().all(Option::is_some) {
932 let mut pts: Vec<Point3> = kept
934 .iter()
935 .enumerate()
936 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
937 .collect();
938 if let Some(&first) = pts.first() {
939 pts.push(first);
940 }
941 return Ok(vec![pts]);
942 }
943
944 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
953 let Some(v_end) = kept[i_end] else {
954 return Vec::new();
955 };
956 let u_end = u_of(i_end);
957 #[allow(clippy::cast_precision_loss)]
958 let pitch = TAU / (n_samples as f64);
959 let u_next = if forward {
960 u_end + pitch
961 } else {
962 u_end - pitch
963 };
964 let target = e / v_max;
965 let h_end = n_dot_g_at(u_end) - target;
966 let h_next = n_dot_g_at(u_next) - target;
967 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
968 return Vec::new();
969 }
970 let (mut lo, mut hi) = (u_end, u_next);
971 for _ in 0..60 {
972 let mid = f64::midpoint(lo, hi);
973 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
974 lo = mid;
975 } else {
976 hi = mid;
977 }
978 }
979 let u_star = f64::midpoint(lo, hi);
980 let tail_n = 8_usize;
981 (1..=tail_n)
982 .filter_map(|k| {
983 #[allow(clippy::cast_precision_loss)]
984 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
985 let ng = n_dot_g_at(u);
986 if ng.abs() < 1e-12 {
987 return None;
988 }
989 let v = e / ng;
990 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
991 })
992 .collect()
993 };
994
995 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
998 let mut chains: Vec<Vec<Point3>> = Vec::new();
999 let mut run: Vec<usize> = Vec::new();
1000 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
1001 if run.len() >= 2 {
1002 let first = run[0];
1003 let last = run[run.len() - 1];
1004 let mut pts: Vec<Point3> = tail(first, false, &kept);
1005 pts.reverse();
1006 pts.extend(
1007 run.iter()
1008 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
1009 );
1010 pts.extend(tail(last, true, &kept));
1011 chains.push(pts);
1012 }
1013 run.clear();
1014 };
1015 for k in 0..n_samples {
1016 let idx = (gap + k) % n_samples;
1017 if kept[idx].is_some() {
1018 run.push(idx);
1019 } else {
1020 flush(&mut run, &mut chains);
1021 }
1022 }
1023 flush(&mut run, &mut chains);
1024 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
1025}
1026
1027#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
1033 torus: &ToroidalSurface,
1034 normal: Vec3,
1035 d: f64,
1036) -> Result<Vec<Vec<Point3>>, MathError> {
1037 Ok(plane_torus_loops(torus, normal, d, 128)
1038 .into_iter()
1039 .map(|run| run.into_iter().map(|p| p.point).collect())
1040 .collect())
1041}
1042
1043#[allow(clippy::cast_precision_loss)]
1053pub fn intersect_plane_cylinder(
1054 cyl: &CylindricalSurface,
1055 normal: Vec3,
1056 d: f64,
1057) -> Result<Vec<IntersectionCurve>, MathError> {
1058 let n_samples = 64_usize;
1059 let mut points_3d = Vec::new();
1060 let mut ipoints = Vec::new();
1061
1062 for i in 0..=n_samples {
1063 let u = TAU * (i as f64) / (n_samples as f64);
1064 let base = cyl.evaluate(u, 0.0);
1067 let n_dot_axis = normal.dot(cyl.axis());
1068 let n_dot_base = dot_np(normal, base);
1069
1070 if n_dot_axis.abs() < 1e-12 {
1071 if (n_dot_base - d).abs() < 1e-6 {
1073 let pt = base;
1074 points_3d.push(pt);
1075 ipoints.push(IntersectionPoint {
1076 point: pt,
1077 param1: (u, 0.0),
1078 param2: (0.0, 0.0),
1079 });
1080 }
1081 } else {
1082 let v = (d - n_dot_base) / n_dot_axis;
1083 if v.abs() <= 100.0 {
1085 let pt = cyl.evaluate(u, v);
1086 points_3d.push(pt);
1087 ipoints.push(IntersectionPoint {
1088 point: pt,
1089 param1: (u, v),
1090 param2: (0.0, 0.0),
1091 });
1092 }
1093 }
1094 }
1095
1096 build_curves_from_points(&points_3d, ipoints)
1097}
1098
1099#[allow(clippy::cast_precision_loss)]
1108pub fn intersect_plane_sphere(
1109 sphere: &SphericalSurface,
1110 normal: Vec3,
1111 d: f64,
1112) -> Result<Vec<IntersectionCurve>, MathError> {
1113 let h = dot_np(normal, sphere.center()) - d;
1114 let r = sphere.radius();
1115
1116 if h.abs() > r - 1e-10 {
1118 return Ok(vec![]);
1119 }
1120
1121 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
1122 let circle_center = Point3::new(
1123 h.mul_add(-normal.x(), sphere.center().x()),
1124 h.mul_add(-normal.y(), sphere.center().y()),
1125 h.mul_add(-normal.z(), sphere.center().z()),
1126 );
1127
1128 let basis = Frame3::from_normal(circle_center, normal)?;
1130 let u_dir = basis.x;
1131 let v_dir = basis.y;
1132
1133 let n_samples = 64_usize;
1134 let mut points_3d = Vec::new();
1135 let mut ipoints = Vec::new();
1136
1137 for i in 0..=n_samples {
1138 let theta = TAU * (i as f64) / (n_samples as f64);
1139 let (sin_t, cos_t) = theta.sin_cos();
1140 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
1141 points_3d.push(pt);
1142 ipoints.push(IntersectionPoint {
1143 point: pt,
1144 param1: (theta, 0.0),
1145 param2: (0.0, 0.0),
1146 });
1147 }
1148
1149 build_curves_from_points(&points_3d, ipoints)
1150}
1151
1152#[allow(clippy::cast_precision_loss)]
1161pub fn intersect_plane_cone(
1162 cone: &ConicalSurface,
1163 normal: Vec3,
1164 d: f64,
1165) -> Result<Vec<IntersectionCurve>, MathError> {
1166 let n_samples = 64_usize;
1167 let mut points_3d = Vec::new();
1168 let mut ipoints = Vec::new();
1169
1170 for i in 0..n_samples {
1171 let u = TAU * (i as f64) / (n_samples as f64);
1172 let apex = cone.apex();
1175 let n_dot_apex = dot_np(normal, apex);
1176 let p1 = cone.evaluate(u, 1.0);
1178 let dir = p1 - apex;
1179 let n_dot_dir = normal.dot(dir);
1180
1181 if n_dot_dir.abs() < 1e-12 {
1182 continue;
1183 }
1184
1185 let v = (d - n_dot_apex) / n_dot_dir;
1186 if v.abs() > 1e-10 && v.abs() < 100.0 {
1188 let pt = cone.evaluate(u, v);
1189 points_3d.push(pt);
1190 ipoints.push(IntersectionPoint {
1191 point: pt,
1192 param1: (u, v),
1193 param2: (0.0, 0.0),
1194 });
1195 }
1196 }
1197
1198 build_curves_from_points(&points_3d, ipoints)
1199}
1200
1201#[allow(clippy::unnecessary_wraps)]
1213pub fn intersect_plane_torus(
1214 torus: &ToroidalSurface,
1215 normal: Vec3,
1216 d: f64,
1217) -> Result<Vec<IntersectionCurve>, MathError> {
1218 let mut curves = Vec::new();
1222 for ipts in plane_torus_loops(torus, normal, d, 128) {
1223 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1224 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1225 curves.push(IntersectionCurve {
1226 curve,
1227 points: ipts,
1228 });
1229 }
1230 }
1231
1232 Ok(curves)
1233}
1234
1235const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1238
1239#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1262fn plane_torus_loops(
1263 torus: &ToroidalSurface,
1264 normal: Vec3,
1265 d: f64,
1266 n_v: usize,
1267) -> Vec<Vec<IntersectionPoint>> {
1268 let big_r = torus.major_radius();
1269 let small_r = torus.minor_radius();
1270 let a = normal.dot(torus.x_axis());
1271 let b = normal.dot(torus.y_axis());
1272 let c = normal.dot(torus.z_axis());
1273 let s = a.hypot(b);
1274 let phi = b.atan2(a);
1275 let d_local = d - dot_np(normal, torus.center());
1276 let point = |u: f64, v: f64| IntersectionPoint {
1277 point: torus.evaluate(u, v),
1278 param1: (u, v.rem_euclid(TAU)),
1279 param2: (0.0, 0.0),
1280 };
1281 let closed = |mut run: Vec<IntersectionPoint>| {
1282 run.push(run[0]);
1283 run
1284 };
1285
1286 if s < 1e-12 {
1288 if c.abs() < 1e-12 {
1289 return Vec::new();
1290 }
1291 let sin_v = d_local / (small_r * c);
1292 if sin_v.abs() > 1.0 + 1e-9 {
1293 return Vec::new();
1294 }
1295 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1296 let v1 = std::f64::consts::PI - v0;
1297 let mut vs = vec![v0];
1298 let apart = (v1 - v0).rem_euclid(TAU);
1301 if apart.min(TAU - apart) > 1e-9 {
1302 vs.push(v1);
1303 }
1304 return vs
1305 .into_iter()
1306 .map(|v| {
1307 closed(
1308 (0..n_v)
1309 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1310 .collect(),
1311 )
1312 })
1313 .collect();
1314 }
1315
1316 let step = TAU / (n_v as f64);
1319 let v_off = step * 0.5;
1320 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1322 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1323 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1324 let peak = |lo: f64, hi: f64| {
1326 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1327 let (mut lo, mut hi) = (lo, hi);
1328 for _ in 0..80 {
1329 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1330 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1331 hi = m2;
1332 } else {
1333 lo = m1;
1334 }
1335 }
1336 let v = f64::midpoint(lo, hi);
1337 (v, rhs_at(v).abs())
1338 };
1339 let mut scan: Vec<f64> = Vec::with_capacity(n_v + 2);
1343 for i in 0..n_v {
1344 let v = (i as f64).mul_add(step, v_off);
1345 scan.push(v);
1346 if inside(v) && inside(v + step) {
1347 let (at, top) = peak(v, v + step);
1348 if top > 1.0 + 1e-12 {
1349 scan.push(at);
1350 }
1351 }
1352 }
1353 let n = scan.len();
1354 #[allow(clippy::cast_possible_wrap)]
1356 let scan_v = |i: isize| {
1357 let len = n as isize;
1358 scan[i.rem_euclid(len) as usize] + TAU * (i.div_euclid(len) as f64)
1359 };
1360 let touch_at = |lo: f64, hi: f64| {
1363 let (v, top) = peak(lo, hi);
1364 ((top - 1.0).abs() < 1e-12).then_some(v)
1365 };
1366 let node = |v: f64| {
1368 let u = if rhs_at(v) > 0.0 {
1369 phi
1370 } else {
1371 phi + std::f64::consts::PI
1372 };
1373 point(u, v)
1374 };
1375 let turn = |v_in: f64, v_out: f64| {
1377 let (mut lo, mut hi) = (v_in, v_out);
1378 for _ in 0..60 {
1379 let mid = f64::midpoint(lo, hi);
1380 if inside(mid) {
1381 lo = mid;
1382 } else {
1383 hi = mid;
1384 }
1385 }
1386 lo
1387 };
1388 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1389 #[allow(clippy::cast_possible_wrap)]
1390 if in_scan.iter().all(|&x| x) {
1391 let at_node = (0..n as isize).find_map(|i| touch_at(scan_v(i), scan_v(i + 1)));
1392 return [1.0, -1.0]
1393 .into_iter()
1394 .map(|sign| match at_node {
1395 Some(v_node) => {
1396 let mut run = vec![node(v_node)];
1397 run.extend((1..n_v).map(|i| branch((i as f64).mul_add(step, v_node), sign)));
1398 closed(run)
1399 }
1400 None => closed(scan.iter().map(|&v| branch(v, sign)).collect()),
1401 })
1402 .collect();
1403 }
1404 let Some(first) = (0..n).find(|&i| in_scan[i] && !in_scan[(i + n - 1) % n]) else {
1405 return Vec::new();
1406 };
1407 let mut loops = Vec::new();
1408 let mut k = 0;
1409 while k < n {
1410 let i = (first + k) % n;
1411 if !in_scan[i] {
1412 k += 1;
1413 continue;
1414 }
1415 let len = (0..n - k).take_while(|&j| in_scan[(i + j) % n]).count();
1417 #[allow(clippy::cast_possible_wrap)]
1418 let run_v = |j: isize| scan_v(i as isize + j);
1419 #[allow(clippy::cast_possible_wrap)]
1420 let last = len as isize - 1;
1421 let (t_lo, t_hi) = (
1422 turn(run_v(0), run_v(-1)),
1423 turn(run_v(last), run_v(last + 1)),
1424 );
1425 let at_node = (0..last).find_map(|j| touch_at(run_v(j), run_v(j + 1)));
1426 let (u_lo, u_hi) = (0..=last)
1434 .map(run_v)
1435 .chain([t_lo, t_hi])
1436 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1437 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1438 (lo.min(u), hi.max(u))
1439 });
1440 let m = (len as f64)
1441 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1442 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1443 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1444 .ceil();
1445 let at = |k: f64| {
1446 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1447 (t_hi - t_lo).mul_add(f, t_lo)
1448 };
1449 let steps = m as usize;
1450 if let Some(v_node) = at_node {
1451 for (from, to) in [(v_node, t_lo), (v_node, t_hi)] {
1454 let along = |k: usize| {
1455 let f = 0.5 * (1.0 - (std::f64::consts::PI * k as f64 / m).cos());
1456 (to - from).mul_add(f, from)
1457 };
1458 let mut pts = vec![node(v_node)];
1459 pts.extend((1..=steps).map(|k| branch(along(k), 1.0)));
1460 pts.extend((1..steps).rev().map(|k| branch(along(k), -1.0)));
1461 loops.push(closed(pts));
1462 }
1463 } else {
1464 let mut pts: Vec<IntersectionPoint> =
1465 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1466 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1467 loops.push(closed(pts));
1468 }
1469 k += len;
1470 }
1471 loops
1472}
1473
1474#[allow(clippy::cast_precision_loss)]
1483fn plane_torus_winding_loops(
1484 torus: &ToroidalSurface,
1485 normal: Vec3,
1486 d: f64,
1487 n_v: usize,
1488) -> Option<Vec<Vec<Point3>>> {
1489 let big_r = torus.major_radius();
1490 let small_r = torus.minor_radius();
1491 let a = normal.dot(torus.x_axis());
1492 let b = normal.dot(torus.y_axis());
1493 let c = normal.dot(torus.z_axis());
1494 let s = a.hypot(b);
1495 if s < 1e-12 * normal.length() || small_r >= big_r {
1496 return None;
1497 }
1498 let phi = b.atan2(a);
1499 let d_local = d - dot_np(normal, torus.center());
1500 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1501 let dense = 8 * n_v;
1502 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1503 return None;
1504 }
1505 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1506 for i in 0..n_v {
1507 let v = TAU * i as f64 / n_v as f64;
1508 let delta = rhs(v).acos();
1509 loops[0].push(torus.evaluate(phi + delta, v));
1510 loops[1].push(torus.evaluate(phi - delta, v));
1511 }
1512 Some(
1513 loops
1514 .into_iter()
1515 .map(|mut run| {
1516 run.push(run[0]);
1517 run
1518 })
1519 .collect(),
1520 )
1521}
1522
1523#[must_use]
1537pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1538 let c = torus.center();
1539 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1540 let big_r = torus.major_radius();
1541 let small_r = torus.minor_radius();
1542
1543 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1545 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1546 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1547 let (c0, c1) = (za.dot(o), za.dot(dir));
1548
1549 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1551 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1552 let g0 = a0.mul_add(
1553 a0,
1554 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1555 );
1556
1557 let four_rr = 4.0 * big_r * big_r;
1559 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1560 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1561 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1562
1563 let e4 = g2 * g2;
1565 let e3 = 2.0 * g2 * g1;
1566 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1567 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1568 let e0 = g0.mul_add(g0, -h0);
1569
1570 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1571 let tube = |t: f64, across: bool| -> f64 {
1577 let p = origin + dir * t;
1578 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1579 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1580 let centre = if across { -big_r } else { big_r };
1581 (a.hypot(b) - centre).hypot(cc) - small_r
1582 };
1583 for t in &mut roots {
1584 let across = big_r < small_r && tube(*t, true).abs() < tube(*t, false).abs();
1585 for _ in 0..8 {
1586 let eps = 1e-7;
1587 let f = tube(*t, across);
1588 let df = (tube(*t + eps, across) - tube(*t - eps, across)) / (2.0 * eps);
1589 if df.abs() <= 1e-12 {
1590 break;
1591 }
1592 let step = f / df;
1593 *t -= step;
1594 if step.abs() < 1e-14 * (1.0 + t.abs()) {
1595 break;
1596 }
1597 }
1598 }
1599 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1600 roots.dedup_by(|a, b| (*a - *b).abs() < 1e-9 * (1.0 + b.abs()));
1601 roots
1602}
1603
1604fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1607 if c4.abs() < 1e-14 {
1609 return real_roots_cubic(c3, c2, c1, c0);
1610 }
1611 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1613 let eval = |z: Complex| -> Complex {
1614 let mut acc = Complex::new(1.0, 0.0);
1616 acc = acc * z + Complex::new(a, 0.0);
1617 acc = acc * z + Complex::new(b, 0.0);
1618 acc = acc * z + Complex::new(c, 0.0);
1619 acc * z + Complex::new(d, 0.0)
1620 };
1621 let seed = Complex::new(0.4, 0.9);
1623 let mut r = [
1624 Complex::new(1.0, 0.0),
1625 seed,
1626 seed * seed,
1627 seed * seed * seed,
1628 ];
1629 for _ in 0..100 {
1630 let mut max_step = 0.0_f64;
1631 for i in 0..4 {
1632 let mut denom = Complex::new(1.0, 0.0);
1633 for j in 0..4 {
1634 if i != j {
1635 denom = denom * (r[i] - r[j]);
1636 }
1637 }
1638 if denom.norm() < 1e-300 {
1639 continue;
1640 }
1641 let step = eval(r[i]) / denom;
1642 r[i] = r[i] - step;
1643 max_step = max_step.max(step.norm());
1644 }
1645 if max_step < 1e-14 {
1646 break;
1647 }
1648 }
1649 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1656 let mut out: Vec<f64> = Vec::new();
1657 for z in r {
1658 if z.im.abs() >= 1e-7 {
1659 continue;
1660 }
1661 let x = z.re;
1662 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1665 if p_real(x).abs() > 1e-6 * scale {
1666 continue;
1667 }
1668 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1669 continue;
1670 }
1671 out.push(x);
1672 }
1673 out
1674}
1675
1676fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1678 if a.abs() < 1e-14 {
1679 return real_roots_quadratic(b, c, d);
1680 }
1681 let (b, c, d) = (b / a, c / a, d / a);
1683 let p = c - b * b / 3.0;
1684 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1685 let shift = -b / 3.0;
1686 let disc = q * q / 4.0 + p * p * p / 27.0;
1687 if disc > 1e-14 {
1688 let sq = disc.sqrt();
1689 let u = (-q / 2.0 + sq).cbrt();
1690 let v = (-q / 2.0 - sq).cbrt();
1691 vec![u + v + shift]
1692 } else if disc < -1e-14 {
1693 let m = 2.0 * (-p / 3.0).sqrt();
1695 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1696 (0..3)
1697 .map(|k| {
1698 m.mul_add(
1699 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1700 shift,
1701 )
1702 })
1703 .collect()
1704 } else {
1705 let u = (-q / 2.0).cbrt();
1707 vec![2.0 * u + shift, -u + shift]
1708 }
1709}
1710
1711fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1713 if a.abs() < 1e-14 {
1714 if b.abs() < 1e-14 {
1715 return Vec::new();
1716 }
1717 return vec![-c / b];
1718 }
1719 let disc = b * b - 4.0 * a * c;
1720 if disc < 0.0 {
1721 Vec::new()
1722 } else {
1723 let sq = disc.sqrt();
1724 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1725 }
1726}
1727
1728#[derive(Clone, Copy)]
1730struct Complex {
1731 re: f64,
1732 im: f64,
1733}
1734
1735impl Complex {
1736 const fn new(re: f64, im: f64) -> Self {
1737 Self { re, im }
1738 }
1739 fn norm(self) -> f64 {
1740 self.re.hypot(self.im)
1741 }
1742}
1743
1744impl std::ops::Add for Complex {
1745 type Output = Self;
1746 fn add(self, o: Self) -> Self {
1747 Self::new(self.re + o.re, self.im + o.im)
1748 }
1749}
1750
1751impl std::ops::Sub for Complex {
1752 type Output = Self;
1753 fn sub(self, o: Self) -> Self {
1754 Self::new(self.re - o.re, self.im - o.im)
1755 }
1756}
1757
1758impl std::ops::Mul for Complex {
1759 type Output = Self;
1760 fn mul(self, o: Self) -> Self {
1761 Self::new(
1762 self.re.mul_add(o.re, -(self.im * o.im)),
1763 self.re.mul_add(o.im, self.im * o.re),
1764 )
1765 }
1766}
1767
1768impl std::ops::Div for Complex {
1769 type Output = Self;
1770 fn div(self, o: Self) -> Self {
1771 let den = o.re.mul_add(o.re, o.im * o.im);
1772 Self::new(
1773 self.re.mul_add(o.re, self.im * o.im) / den,
1774 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1775 )
1776 }
1777}
1778
1779fn build_curves_from_points(
1783 points_3d: &[Point3],
1784 ipoints: Vec<IntersectionPoint>,
1785) -> Result<Vec<IntersectionCurve>, MathError> {
1786 if points_3d.len() < 2 {
1787 return Ok(vec![]);
1788 }
1789
1790 let degree = 3.min(points_3d.len() - 1);
1791 let curve = interpolate(points_3d, degree)?;
1792 Ok(vec![IntersectionCurve {
1793 curve,
1794 points: ipoints,
1795 }])
1796}
1797
1798#[allow(
1810 clippy::cast_precision_loss,
1811 clippy::too_many_lines,
1812 clippy::similar_names,
1813 clippy::unnecessary_wraps,
1814 clippy::type_complexity
1815)]
1816pub fn intersect_analytic_analytic(
1817 a: AnalyticSurface<'_>,
1818 b: AnalyticSurface<'_>,
1819 grid_res: usize,
1820) -> Result<Vec<IntersectionCurve>, MathError> {
1821 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1822}
1823
1824pub fn intersect_analytic_analytic_bounded(
1835 a: AnalyticSurface<'_>,
1836 b: AnalyticSurface<'_>,
1837 grid_res: usize,
1838 v_range_hint_a: Option<(f64, f64)>,
1839 v_range_hint_b: Option<(f64, f64)>,
1840) -> Result<Vec<IntersectionCurve>, MathError> {
1841 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1842}
1843
1844pub fn intersect_analytic_analytic_in_region(
1857 a: AnalyticSurface<'_>,
1858 b: AnalyticSurface<'_>,
1859 grid_res: usize,
1860 v_range_hint_a: Option<(f64, f64)>,
1861 v_range_hint_b: Option<(f64, f64)>,
1862 region: Aabb3,
1863) -> Result<Vec<IntersectionCurve>, MathError> {
1864 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1865}
1866
1867fn intersect_analytic_analytic_impl(
1868 a: AnalyticSurface<'_>,
1869 b: AnalyticSurface<'_>,
1870 grid_res: usize,
1871 v_range_hint_a: Option<(f64, f64)>,
1872 v_range_hint_b: Option<(f64, f64)>,
1873 region: Option<Aabb3>,
1874) -> Result<Vec<IntersectionCurve>, MathError> {
1875 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1878 return Ok(result);
1879 }
1880
1881 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1882 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1883 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1884 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1885
1886 let diag_a = {
1888 let p00 = surf_a(u_range_a.0, v_range_a.0);
1889 let p11 = surf_a(u_range_a.1, v_range_a.1);
1890 (p00 - p11).length()
1891 };
1892 let diag_b = {
1893 let p00 = surf_b(u_range_b.0, v_range_b.0);
1894 let p11 = surf_b(u_range_b.1, v_range_b.1);
1895 (p00 - p11).length()
1896 };
1897 let char_size = diag_a.min(diag_b).max(0.1);
1898
1899 #[allow(clippy::type_complexity)]
1903 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1904 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1908 let mut min_dist = f64::INFINITY;
1909
1910 #[allow(clippy::cast_precision_loss)]
1911 for ia in 0..grid_res {
1912 for ja in 0..grid_res {
1913 let ua =
1914 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1915 let va =
1916 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1917
1918 let pa = surf_a(ua, va);
1919
1920 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1922 let pb = surf_b(ub, vb);
1923 let dist = (pa - pb).length();
1924 min_dist = min_dist.min(dist);
1925
1926 if dist < seed_threshold {
1927 let mid = Point3::new(
1932 (pa.x() + pb.x()) * 0.5,
1933 (pa.y() + pb.y()) * 0.5,
1934 (pa.z() + pb.z()) * 0.5,
1935 );
1936 seeds.push((mid, (ua, va), (ub, vb)));
1937 }
1938 }
1939 }
1940
1941 let reject_dist = (char_size / grid_res as f64) * 3.0;
1950 if min_dist > reject_dist {
1951 return Ok(vec![]);
1952 }
1953
1954 if seeds.is_empty() {
1955 return Ok(vec![]);
1956 }
1957
1958 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1962 let dedup_radius = march_step * 10.0;
1963 let mut unique_seeds = Vec::new();
1964 for seed in &seeds {
1965 let dominated = unique_seeds
1966 .iter()
1967 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1968 if !dominated {
1969 unique_seeds.push(*seed);
1970 }
1971 }
1972
1973 let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1978 if let Some(r) = region {
1979 for seed in &mut unique_seeds {
1980 let mut p = seed.0;
1981 for _ in 0..8 {
1982 let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1983 let pa = surf_a(ua, va);
1984 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1985 let pb = surf_b(ub, vb);
1986 p = Point3::new(
1987 (pa.x() + pb.x()) * 0.5,
1988 (pa.y() + pb.y()) * 0.5,
1989 (pa.z() + pb.z()) * 0.5,
1990 );
1991 if (pa - pb).length() < 1e-9 {
1992 break;
1993 }
1994 }
1995 seed.0 = p;
1996 }
1997 unique_seeds.retain(|seed| r.contains_point(seed.0));
1998 }
1999
2000 let mut curves = Vec::new();
2002 let mut used_seeds = vec![false; unique_seeds.len()];
2003
2004 for si in 0..unique_seeds.len() {
2005 if used_seeds[si] {
2006 continue;
2007 }
2008 used_seeds[si] = true;
2009
2010 let march_result = march_analytic_intersection(
2011 &a,
2012 &b,
2013 surf_a.as_ref(),
2014 norm_a.as_ref(),
2015 surf_b.as_ref(),
2016 norm_b.as_ref(),
2017 unique_seeds[si].0,
2018 u_range_a,
2019 v_range_a,
2020 u_range_b,
2021 v_range_b,
2022 march_step,
2023 is_u_periodic(&a),
2024 is_u_periodic(&b),
2025 region,
2026 );
2027
2028 if march_result.len() >= 2 {
2029 for (sj, other) in unique_seeds.iter().enumerate() {
2030 if !used_seeds[sj]
2031 && march_result
2032 .iter()
2033 .any(|p| (*p - other.0).length() < dedup_radius)
2034 {
2035 used_seeds[sj] = true;
2036 }
2037 }
2038
2039 let ipts: Vec<IntersectionPoint> = march_result
2040 .iter()
2041 .map(|&pt| IntersectionPoint {
2042 point: pt,
2043 param1: (0.0, 0.0),
2044 param2: (0.0, 0.0),
2045 })
2046 .collect();
2047
2048 let degree = 3.min(march_result.len() - 1);
2049 if let Ok(curve) = interpolate(&march_result, degree) {
2050 curves.push(IntersectionCurve {
2051 curve,
2052 points: ipts,
2053 });
2054 }
2055 }
2056 }
2057
2058 Ok(curves)
2059}
2060
2061#[allow(clippy::too_many_lines)]
2075fn try_algebraic_intersection(
2076 a: &AnalyticSurface<'_>,
2077 b: &AnalyticSurface<'_>,
2078 v_range_a: Option<(f64, f64)>,
2079 v_range_b: Option<(f64, f64)>,
2080) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2081 match (a, b) {
2082 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
2083 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
2084 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
2085 ),
2086 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
2087 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
2088 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
2089 ),
2090 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
2091 algebraic_sphere_sphere(s1, s2).map(Some)
2092 }
2093 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
2094 let axis_dot = c1.axis().dot(c2.axis()).abs();
2095 if axis_dot > 1.0 - 1e-10 {
2096 let delta = c2.origin() - c1.origin();
2098 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2099 let along = delta_vec.dot(c1.axis());
2100 let perp = (delta_vec - c1.axis() * along).length();
2101 if perp < 1e-8 {
2102 if (c1.radius() - c2.radius()).abs() < 1e-8 {
2105 return Ok(None); }
2107 return Ok(Some(vec![])); }
2109 if let Some(rulings) = parallel_cylinders(c1, c2, v_range_a, v_range_b)? {
2110 return Ok(Some(rulings));
2111 }
2112 }
2113 algebraic_cylinder_cylinder(c1, c2)
2115 }
2116 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
2118 algebraic_sphere_cylinder(s, c, true)
2119 }
2120 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
2121 algebraic_sphere_cylinder(s, c, false)
2122 }
2123 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
2124 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
2125 Ok(ruling_cone_sphere(cone, sphere, true))
2126 }
2127 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
2128 Ok(ruling_cone_sphere(cone, sphere, false))
2129 }
2130 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
2131 Ok(parallel_axis_torus_cylinder(t, c, true)
2132 .or_else(|| ruling_torus_cylinder(t, c, true)))
2133 }
2134 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
2135 Ok(parallel_axis_torus_cylinder(t, c, false)
2136 .or_else(|| ruling_torus_cylinder(t, c, false)))
2137 }
2138 _ => Ok(None),
2139 }
2140}
2141
2142fn parallel_axis_torus_cylinder(
2149 torus: &ToroidalSurface,
2150 cyl: &CylindricalSurface,
2151 torus_first: bool,
2152) -> Option<Vec<IntersectionCurve>> {
2153 let axis = torus.z_axis();
2154 let along = cyl.axis().dot(axis);
2155 if along.abs() < 1.0 - 1e-10 {
2156 return None;
2157 }
2158 let offset = cyl.origin() - torus.center();
2159 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
2160 return None;
2161 }
2162 let (major, minor) = (torus.major_radius(), torus.minor_radius());
2163 let roots = |u: f64| {
2164 let q = cyl.evaluate(u, 0.0) - torus.center();
2165 let height = q.dot(axis);
2166 let rho = (q - axis * height).length();
2167 let reach = minor * minor - (rho - major) * (rho - major);
2168 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
2169 };
2170 let samples = ruling_samples(cyl, &roots);
2171 let loops = if samples.iter().all(Option::is_some) {
2172 closed_ruling_loops(cyl, &roots, &samples)
2173 } else {
2174 partial_ruling_loops(cyl, &roots, &samples)
2175 };
2176 if loops.is_empty() {
2177 return None;
2178 }
2179 Some(fit_ruling_loops(&loops, |p| {
2180 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2181 }))
2182}
2183
2184fn meridian_crossings(
2190 first: (f64, f64, f64),
2191 second: (f64, f64, f64),
2192 scale: f64,
2193) -> Option<Vec<(f64, f64)>> {
2194 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
2195 let (dx, dz) = (x2 - x1, z2 - z1);
2196 let dist = dx.hypot(dz);
2197 let slack = 1e-9 * scale;
2198 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
2199 return None;
2200 }
2201 if dist > r1 + r2 || dist < (r1 - r2).abs() {
2202 return Some(Vec::new());
2203 }
2204 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
2205 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
2206 let (ux, uz) = (dx / dist, dz / dist);
2207 let mut crossings = Vec::with_capacity(2);
2208 for side in [1.0, -1.0] {
2209 let rho = x1 + along * ux - side * across * uz;
2210 if rho <= slack {
2211 return None;
2212 }
2213 crossings.push((rho, z1 + along * uz + side * across * ux));
2214 }
2215 Some(crossings)
2216}
2217
2218fn circles_about_axis(
2220 base: Point3,
2221 axis: Vec3,
2222 crossings: &[(f64, f64)],
2223) -> Result<Vec<ExactIntersectionCurve>, MathError> {
2224 crossings
2225 .iter()
2226 .map(|&(rho, z)| {
2227 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
2228 })
2229 .collect()
2230}
2231
2232pub fn exact_torus_torus(
2243 first: &ToroidalSurface,
2244 second: &ToroidalSurface,
2245) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2246 let axis = first.z_axis();
2247 let scale = first.major_radius() + second.major_radius();
2248 let offset = second.center() - first.center();
2249 if first.minor_radius() >= first.major_radius()
2251 || second.minor_radius() >= second.major_radius()
2252 || axis.cross(second.z_axis()).length() > 1e-9
2253 || offset.cross(axis).length() > 1e-9 * scale
2254 {
2255 return Ok(None);
2256 }
2257 let Some(crossings) = meridian_crossings(
2258 (first.major_radius(), 0.0, first.minor_radius()),
2259 (
2260 second.major_radius(),
2261 offset.dot(axis),
2262 second.minor_radius(),
2263 ),
2264 scale,
2265 ) else {
2266 return Ok(None);
2267 };
2268 circles_about_axis(first.center(), axis, &crossings).map(Some)
2269}
2270
2271pub fn exact_cylinder_torus(
2285 cylinder: &CylindricalSurface,
2286 torus: &ToroidalSurface,
2287) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2288 let axis = torus.z_axis();
2289 let scale = torus.major_radius() + cylinder.radius();
2290 let offset = cylinder.origin() - torus.center();
2291 let linear = crate::tolerance::Tolerance::default().linear;
2292 if axis.cross(cylinder.axis()).length() > 1e-9
2293 || offset.cross(axis).length() > 1e-9 * scale
2294 || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2295 {
2296 return Ok(None);
2297 }
2298 let gap = cylinder.radius() - torus.major_radius();
2299 let small = torus.minor_radius();
2300 if (gap.abs() - small).abs() <= linear {
2304 return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2305 }
2306 if gap.abs() > small {
2307 return Ok(Some(Vec::new()));
2308 }
2309 let height = small.mul_add(small, -(gap * gap)).sqrt();
2310 circles_about_axis(
2311 torus.center(),
2312 axis,
2313 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2314 )
2315 .map(Some)
2316}
2317
2318pub fn exact_sphere_torus(
2331 sphere: &SphericalSurface,
2332 torus: &ToroidalSurface,
2333) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2334 let axis = torus.z_axis();
2335 let scale = torus.major_radius() + sphere.radius();
2336 let offset = sphere.center() - torus.center();
2337 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2339 return Ok(None);
2340 }
2341 let Some(crossings) = meridian_crossings(
2342 (0.0, offset.dot(axis), sphere.radius()),
2343 (torus.major_radius(), 0.0, torus.minor_radius()),
2344 scale,
2345 ) else {
2346 return Ok(None);
2347 };
2348 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2349}
2350
2351pub fn exact_cone_cone(
2376 c1: &ConicalSurface,
2377 c2: &ConicalSurface,
2378) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2379 let axis = c1.axis();
2380 let axis2 = c2.axis();
2381
2382 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2384 return Ok(None); }
2386 let apex1 = c1.apex();
2387 let apex2 = c2.apex();
2388 let delta = apex2 - apex1;
2389 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2390 let along = delta_v.dot(axis);
2391 if (delta_v - axis * along).length() > 1e-8 {
2392 return offset_parallel_cone_cone(c1, c2);
2393 }
2394
2395 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2396 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2397 return Ok(None); }
2399 let m1 = c1.half_angle().cos() / s1;
2400 let m2 = c2.half_angle().cos() / s2;
2401 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2402 let d2 = along; let denom = m1 - m2 * sigma;
2405 if denom.abs() < 1e-12 {
2406 if sigma > 0.0 && d2.abs() < 1e-9 {
2409 return Ok(None);
2410 }
2411 return Ok(Some(vec![]));
2412 }
2413
2414 let t_star = (-m2 * sigma * d2) / denom;
2415 let radius = m1 * t_star;
2416 if radius < 1e-12 {
2417 return Ok(Some(vec![])); }
2419
2420 let center = Point3::new(
2421 apex1.x() + axis.x() * t_star,
2422 apex1.y() + axis.y() * t_star,
2423 apex1.z() + axis.z() * t_star,
2424 );
2425 let circle = Circle3D::new(center, axis, radius)?;
2426 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2427}
2428
2429fn offset_parallel_cone_cone(
2440 c1: &ConicalSurface,
2441 c2: &ConicalSurface,
2442) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2443 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2444 return Ok(None); }
2446 let t1 = c1.half_angle().tan();
2447 let t2 = c2.half_angle().tan();
2448 if !t1.is_finite() || !t2.is_finite() {
2449 return Ok(None);
2450 }
2451 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2452 return Ok(None);
2453 }
2454
2455 let w = c1.axis();
2456 let apex1 = c1.apex();
2457 let apex2 = c2.apex();
2458 let delta = apex2 - apex1;
2459 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2460 let s = delta_v.dot(w);
2461 let tm = 0.5 * (t1 + t2);
2462 let k = 1.0 + tm * tm;
2463
2464 let n = (delta_v - w * (k * s)) * 2.0;
2468 let n_len = n.length();
2469 if n_len < 1e-12 {
2470 return Ok(None);
2471 }
2472 let n_hat = n * (1.0 / n_len);
2473 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2474
2475 let axis2 = c2.axis();
2481 let scale = 1.0 + delta_v.length();
2482 let mut out = Vec::new();
2483 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2484 let samples: Vec<Point3> = match &curve {
2485 ExactIntersectionCurve::Circle(c) => (0..4)
2486 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2487 .collect(),
2488 ExactIntersectionCurve::Ellipse(e) => (0..4)
2489 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2490 .collect(),
2491 ExactIntersectionCurve::Points(_) => return Ok(None),
2492 };
2493 let on_real_nappe = |p: &Point3| {
2494 let rel = *p - apex2;
2495 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2496 };
2497 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2498 match hits {
2499 0 => {}
2500 4 => out.push(curve),
2501 _ => return Ok(None),
2502 }
2503 }
2504 Ok(Some(out))
2505}
2506
2507pub fn exact_cone_cylinder(
2527 cone: &ConicalSurface,
2528 cyl: &CylindricalSurface,
2529) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2530 let axis = cone.axis();
2531 let cyl_axis = cyl.axis();
2532
2533 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2535 return Ok(None);
2536 }
2537 let apex = cone.apex();
2538 let delta = apex - cyl.origin();
2539 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2540 let along = delta_v.dot(cyl_axis);
2541 if (delta_v - cyl_axis * along).length() > 1e-8 {
2542 return Ok(None);
2543 }
2544
2545 let s = cone.half_angle().sin();
2546 if s.abs() < 1e-12 {
2547 return Ok(None); }
2549 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2551 return Ok(None); }
2553
2554 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2556 return Ok(Some(vec![])); }
2558 let center = Point3::new(
2559 apex.x() + axis.x() * t_star,
2560 apex.y() + axis.y() * t_star,
2561 apex.z() + axis.z() * t_star,
2562 );
2563 let circle = Circle3D::new(center, axis, cyl.radius())?;
2564 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2565}
2566
2567fn conic_loop(centre: Point3, u: Vec3, v: Vec3, a: f64, b: f64) -> Result<NurbsCurve, MathError> {
2572 let w = std::f64::consts::FRAC_1_SQRT_2;
2573 let corners: [(f64, f64); 9] = [
2574 (1.0, 0.0),
2575 (1.0, 1.0),
2576 (0.0, 1.0),
2577 (-1.0, 1.0),
2578 (-1.0, 0.0),
2579 (-1.0, -1.0),
2580 (0.0, -1.0),
2581 (1.0, -1.0),
2582 (1.0, 0.0),
2583 ];
2584 let points = corners
2585 .iter()
2586 .map(|&(x, y)| centre + u * (a * x) + v * (b * y))
2587 .collect();
2588 let weights = vec![1.0, w, 1.0, w, 1.0, w, 1.0, w, 1.0];
2589 let knots = vec![
2590 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
2591 ];
2592 NurbsCurve::new(2, knots, points, weights)
2593}
2594
2595fn algebraic_cone_cone(
2604 c1: &ConicalSurface,
2605 c2: &ConicalSurface,
2606) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2607 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2608 return Ok(None);
2609 };
2610 let mut curves = Vec::new();
2611 for exact in exacts {
2612 let n_samples = 33;
2613 let mut points = Vec::with_capacity(n_samples);
2614 #[allow(clippy::cast_precision_loss)]
2615 for i in 0..n_samples {
2616 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2617 let pt = match &exact {
2618 ExactIntersectionCurve::Circle(circle) => {
2619 crate::traits::ParametricCurve::evaluate(circle, theta)
2620 }
2621 ExactIntersectionCurve::Ellipse(ellipse) => {
2622 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2623 }
2624 ExactIntersectionCurve::Points(_) => break,
2625 };
2626 points.push(IntersectionPoint {
2627 point: pt,
2628 param1: (0.0, 0.0),
2629 param2: (0.0, 0.0),
2630 });
2631 }
2632 let curve = match &exact {
2633 ExactIntersectionCurve::Circle(c) => {
2634 conic_loop(c.center(), c.u_axis(), c.v_axis(), c.radius(), c.radius())?
2635 }
2636 ExactIntersectionCurve::Ellipse(e) => conic_loop(
2637 e.center(),
2638 e.u_axis(),
2639 e.v_axis(),
2640 e.semi_major(),
2641 e.semi_minor(),
2642 )?,
2643 ExactIntersectionCurve::Points(_) => continue,
2644 };
2645 curves.push(IntersectionCurve { curve, points });
2646 }
2647 Ok(Some(curves))
2648}
2649
2650pub fn exact_sphere_cylinder(
2670 sphere: &SphericalSurface,
2671 cyl: &CylindricalSurface,
2672) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2673 let sc = sphere.center();
2674 let r_sphere = sphere.radius();
2675 let co = cyl.origin();
2676 let axis = cyl.axis();
2677 let r_cyl = cyl.radius();
2678
2679 let delta = sc - co;
2681 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2682 let along = delta_vec.dot(axis);
2683 let perp_vec = delta_vec - axis * along;
2684 let d_perp = perp_vec.length();
2685
2686 if d_perp > 1e-7 {
2689 return Ok(None);
2690 }
2691
2692 if r_cyl > r_sphere + 1e-10 {
2695 return Ok(Some(vec![]));
2696 }
2697 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2698 if z_sq < 0.0 {
2699 return Ok(Some(vec![]));
2700 }
2701 let z = z_sq.sqrt();
2702
2703 let center_axis_pt = Point3::new(
2706 co.x() + axis.x() * along,
2707 co.y() + axis.y() * along,
2708 co.z() + axis.z() * along,
2709 );
2710
2711 let mut circles = Vec::new();
2712 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2713 for &z_offset in offsets {
2714 let center = Point3::new(
2715 center_axis_pt.x() + axis.x() * z_offset,
2716 center_axis_pt.y() + axis.y() * z_offset,
2717 center_axis_pt.z() + axis.z() * z_offset,
2718 );
2719 let circle = Circle3D::new(center, axis, r_cyl)?;
2720 circles.push(ExactIntersectionCurve::Circle(circle));
2721 }
2722 Ok(Some(circles))
2723}
2724
2725pub fn exact_cone_sphere(
2743 cone: &ConicalSurface,
2744 sphere: &SphericalSurface,
2745) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2746 let offset = cone.apex() - sphere.center();
2747 let along = offset.dot(cone.axis());
2748 if (offset - cone.axis() * along).length() > 1e-7 {
2749 return Ok(None);
2750 }
2751 let lin_tol = Tolerance::new().linear;
2752 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2753 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2754 let b = 2.0 * sin_a * along;
2755 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2756 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2759 if disc < -noise {
2760 return Ok(Some(vec![]));
2761 }
2762 let roots: &[f64] = if far - near < lin_tol {
2763 &[far]
2764 } else {
2765 &[near, far]
2766 };
2767 let mut circles = Vec::new();
2768 for &v in roots {
2769 if v * cos_a > lin_tol {
2770 let centre = cone.apex() + cone.axis() * (v * sin_a);
2771 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2772 circles.push(ExactIntersectionCurve::Circle(circle));
2773 }
2774 }
2775 Ok(Some(circles))
2776}
2777
2778fn algebraic_sphere_cylinder(
2787 sphere: &SphericalSurface,
2788 cyl: &CylindricalSurface,
2789 sphere_first: bool,
2790) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2791 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2792 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2793 };
2794
2795 let mut curves = Vec::new();
2796 for exact in exacts {
2797 let ExactIntersectionCurve::Circle(circle) = exact else {
2798 continue;
2799 };
2800 let n_samples = 33;
2801 let mut points = Vec::with_capacity(n_samples);
2802 #[allow(clippy::cast_precision_loss)]
2803 for i in 0..n_samples {
2804 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2805 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2806 let (param1, param2) = in_order(
2807 sphere.project_point(pt),
2808 cyl.project_point(pt),
2809 sphere_first,
2810 );
2811 points.push(IntersectionPoint {
2812 point: pt,
2813 param1,
2814 param2,
2815 });
2816 }
2817 let r = circle.radius();
2818 let curve = conic_loop(circle.center(), circle.u_axis(), circle.v_axis(), r, r)?;
2819 curves.push(IntersectionCurve { curve, points });
2820 }
2821
2822 Ok(Some(curves))
2823}
2824
2825fn off_axis_sphere_cylinder(
2834 sphere: &SphericalSurface,
2835 cyl: &CylindricalSurface,
2836 sphere_first: bool,
2837) -> Option<Vec<IntersectionCurve>> {
2838 let (centre, radius) = (sphere.center(), sphere.radius());
2839 let axis = cyl.axis();
2840 let offset = centre - cyl.origin();
2841 let axis_distance = (offset - axis * offset.dot(axis)).length();
2842 let lin_tol = Tolerance::new().linear;
2843 if axis_distance > radius + cyl.radius() + lin_tol
2844 || axis_distance + radius < cyl.radius() - lin_tol
2845 {
2846 return Some(Vec::new());
2847 }
2848 let roots = |u: f64| {
2849 let q = cyl.evaluate(u, 0.0) - centre;
2850 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2851 };
2852 let samples = ruling_samples(cyl, &roots);
2853 let loops = if samples.iter().all(Option::is_some) {
2854 closed_ruling_loops(cyl, &roots, &samples)
2855 } else {
2856 partial_ruling_loops(cyl, &roots, &samples)
2857 };
2858 if loops.is_empty() {
2859 return None;
2860 }
2861 Some(fit_ruling_loops(&loops, |p| {
2862 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2863 }))
2864}
2865
2866const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2869 if a_first { (a, b) } else { (b, a) }
2870}
2871
2872fn parallel_cylinders(
2882 c1: &CylindricalSurface,
2883 c2: &CylindricalSurface,
2884 v_range_1: Option<(f64, f64)>,
2885 v_range_2: Option<(f64, f64)>,
2886) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2887 let axis = c1.axis();
2888 let delta = c2.origin() - c1.origin();
2889 let along = delta.dot(axis);
2890 let across = delta - axis * along;
2891 let d = across.length();
2892 let (r1, r2) = (c1.radius(), c2.radius());
2893 let flip = c2.axis().dot(axis);
2895 let ranges = [
2896 v_range_1,
2897 v_range_2.map(|(a, b)| (along + a * flip, along + b * flip)),
2898 ];
2899 let (lo, hi) = ranges
2900 .into_iter()
2901 .flatten()
2902 .fold((f64::NEG_INFINITY, f64::INFINITY), |(lo, hi), (a, b)| {
2903 (lo.max(a.min(b)), hi.min(a.max(b)))
2904 });
2905 if !lo.is_finite() || !hi.is_finite() {
2906 return Ok(None);
2907 }
2908 let rounding = 8.0 * f64::EPSILON * (r1 + r2 + d);
2909 if d > r1 + r2 + rounding || d < (r1 - r2).abs() - rounding || hi <= lo {
2910 return Ok(Some(Vec::new()));
2911 }
2912 let toward = across * (1.0 / d);
2913 let side = axis.cross(toward);
2914 let x = r2.mul_add(-r2, d.mul_add(d, r1 * r1)) / (2.0 * d);
2915 let y = r1.mul_add(r1, -(x * x)).max(0.0).sqrt();
2916 let linear = Tolerance::new().linear;
2917 let feet = if 2.0 * y <= linear {
2918 vec![toward * x]
2919 } else {
2920 vec![toward * x + side * y, toward * x - side * y]
2921 };
2922 let on_second = |p: Point3| {
2923 let w = p - c2.origin();
2924 ((w - c2.axis() * w.dot(c2.axis())).length() - r2).abs() <= linear
2925 };
2926 let mut rulings = Vec::with_capacity(feet.len());
2927 for foot in feet {
2928 let base = c1.origin() + foot;
2929 let (from, to) = (base + axis * lo, base + axis * hi);
2930 if !on_second(from) || !on_second(to) {
2931 return Ok(None);
2932 }
2933 rulings.push(ruling_segment(from, to, |p| {
2934 (c1.project_point(p), c2.project_point(p))
2935 })?);
2936 }
2937 Ok(Some(rulings))
2938}
2939
2940fn ruling_segment(
2943 from: Point3,
2944 to: Point3,
2945 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
2946) -> Result<IntersectionCurve, MathError> {
2947 let curve = NurbsCurve::new(1, vec![0.0, 0.0, 1.0, 1.0], vec![from, to], vec![1.0, 1.0])?;
2948 let points = [from, from + (to - from) * 0.5, to]
2949 .into_iter()
2950 .map(|point| {
2951 let (param1, param2) = params(point);
2952 IntersectionPoint {
2953 point,
2954 param1,
2955 param2,
2956 }
2957 })
2958 .collect();
2959 Ok(IntersectionCurve { curve, points })
2960}
2961
2962#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2976fn algebraic_cylinder_cylinder(
2977 c1: &CylindricalSurface,
2978 c2: &CylindricalSurface,
2979) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2980 let alpha = c1.axis().dot(c2.axis());
2981 let a_coeff = 1.0 - alpha * alpha;
2982
2983 if a_coeff.abs() < 1e-12 {
2985 return Ok(None);
2986 }
2987
2988 let r1 = c1.radius();
2989 let r2 = c2.radius();
2990 let o1 = c1.origin();
2991 let o2 = c2.origin();
2992 let a1 = c1.axis();
2993 let a2 = c2.axis();
2994
2995 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2998 let cross = a1.cross(a2);
2999 let cross_len = cross.length();
3000 if cross_len > 1e-12 {
3001 let axis_dist = delta.dot(cross).abs() / cross_len;
3002 if axis_dist > r1 + r2 + Tolerance::new().linear {
3003 return Ok(Some(vec![])); }
3005 }
3006
3007 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
3013 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
3014 let alpha = sweep.axis().dot(a);
3015 let quad = 1.0 - alpha * alpha;
3016 let (axis, sweep) = (sweep.axis(), sweep.clone());
3017 move |u: f64| {
3018 let q = sweep.evaluate(u, 0.0) - o;
3019 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
3020 let b = 2.0 * (q_a1 - alpha * q_a2);
3021 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
3022 ruling_quadratic(quad, b, c)
3023 }
3024 };
3025 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
3026 let samples1 = ruling_samples(c1, &roots1);
3027 let loops = if samples1.iter().all(Option::is_some) {
3028 closed_ruling_loops(c1, &roots1, &samples1)
3029 } else {
3030 let samples2 = ruling_samples(c2, &roots2);
3031 if samples2.iter().all(Option::is_some) {
3032 closed_ruling_loops(c2, &roots2, &samples2)
3033 } else if samples1.iter().any(Option::is_some) {
3034 partial_ruling_loops(c1, &roots1, &samples1)
3035 } else {
3036 partial_ruling_loops(c2, &roots2, &samples2)
3037 }
3038 };
3039 if loops.is_empty() {
3040 return Ok(None);
3041 }
3042 Ok(Some(fit_ruling_loops(&loops, |p| {
3043 (c1.project_point(p), c2.project_point(p))
3044 })))
3045}
3046
3047fn ruling_cone_cylinder(
3054 cone: &ConicalSurface,
3055 cyl: &CylindricalSurface,
3056 cone_first: bool,
3057) -> Option<Vec<IntersectionCurve>> {
3058 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3059 if sin_t < 1e-12 || cos_t < 1e-12 {
3060 return None;
3061 }
3062 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
3063 let s = 1.0 / (sin_t * sin_t);
3064 let alpha = w.dot(d);
3065 let quad = 1.0 - s * alpha * alpha;
3066 if quad.abs() < 1e-9 {
3067 return None;
3068 }
3069 let roots = |u: f64| {
3070 let delta = cyl.evaluate(u, 0.0) - apex;
3071 let (dd, dw) = (delta.dot(d), delta.dot(w));
3072 let b = 2.0 * (dw - s * dd * alpha);
3073 let c = delta.dot(delta) - s * dd * dd;
3074 ruling_quadratic(quad, b, c)
3075 };
3076 let lin_tol = Tolerance::new().linear;
3077 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
3078 #[allow(clippy::cast_precision_loss)]
3079 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
3080 let (disc, vp, vm) = roots(u);
3081 disc >= -lin_tol
3082 && [vp, vm]
3083 .iter()
3084 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
3085 });
3086 if far_nappe {
3087 return None;
3088 }
3089 let samples = ruling_samples(cyl, &roots);
3090 let scan = WINDOW_SCAN * RULING_SAMPLES;
3095 #[allow(clippy::cast_precision_loss)]
3098 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
3099 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
3100 let mut k = start;
3101 while k < start + scan {
3102 if !meets(k) {
3103 k += 1;
3104 continue;
3105 }
3106 let first = k;
3107 while k < start + scan && meets(k) {
3108 k += 1;
3109 }
3110 let covered = (first..k)
3111 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
3112 .count();
3113 if covered < WINDOW_MIN_SAMPLES {
3114 return None;
3115 }
3116 }
3117 }
3118 let loops = if samples.iter().all(Option::is_some) {
3119 closed_ruling_loops(cyl, &roots, &samples)
3120 } else {
3121 partial_ruling_loops(cyl, &roots, &samples)
3122 };
3123 if loops.is_empty() {
3124 return None;
3125 }
3126 Some(fit_ruling_loops(&loops, |p| {
3127 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
3128 }))
3129}
3130
3131fn ruling_torus_cylinder(
3141 torus: &ToroidalSurface,
3142 cyl: &CylindricalSurface,
3143 torus_first: bool,
3144) -> Option<Vec<IntersectionCurve>> {
3145 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
3146 || torus.minor_radius() >= torus.major_radius()
3147 {
3148 return None;
3149 }
3150 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
3151 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
3152 let count = rows[0].len();
3153 let scan = WINDOW_SCAN * RULING_SAMPLES;
3154 #[allow(clippy::cast_precision_loss)]
3155 if count == 0
3156 || count % 2 == 1
3157 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
3158 {
3159 return None;
3160 }
3161 let loops: Vec<Vec<Point3>> = (0..count)
3162 .map(|j| {
3163 let mut pts: Vec<Point3> = rows
3164 .iter()
3165 .enumerate()
3166 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
3167 .collect();
3168 pts.push(pts[0]);
3169 pts
3170 })
3171 .collect();
3172 Some(fit_ruling_loops(&loops, |p| {
3173 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
3174 }))
3175}
3176
3177fn ruling_cone_sphere(
3186 cone: &ConicalSurface,
3187 sphere: &SphericalSurface,
3188 cone_first: bool,
3189) -> Option<Vec<IntersectionCurve>> {
3190 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
3191 let offset = apex - centre;
3192 let lin_tol = Tolerance::new().linear;
3193 let along = offset.dot(cone.axis());
3194 let across = (offset - cone.axis() * along).length();
3195 if across < lin_tol {
3196 return None;
3197 }
3198 let k = offset.dot(offset) - radius * radius;
3204 if radius - offset.length() > lin_tol {
3205 let exit = |u: f64| {
3206 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
3207 let root = h.mul_add(h, -k).sqrt();
3208 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
3209 };
3210 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
3215 .map(|i| (ruling_u(i), exit(ruling_u(i))))
3216 .collect();
3217 for _ in 0..10 {
3218 let mut refined = Vec::with_capacity(2 * samples.len());
3219 for pair in samples.windows(2) {
3220 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
3221 refined.push(pair[0]);
3222 let um = 0.5 * (u0 + u1);
3223 let pm = exit(um);
3224 let chord = (p1 - p0).length();
3225 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
3226 refined.push((um, pm));
3227 }
3228 }
3229 refined.extend(samples.last().copied());
3230 if refined.len() == samples.len() {
3231 break;
3232 }
3233 samples = refined;
3234 }
3235 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
3236 if let Some(last) = pts.last_mut() {
3237 *last = samples[0].1;
3238 }
3239 return Some(fit_ruling_loops(&[pts], |p| {
3240 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3241 }));
3242 }
3243 let crossing = |h: f64| {
3247 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
3248 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
3249 };
3250 let (sin_a, cos_a) = cone.half_angle().sin_cos();
3255 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
3256 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
3257 {
3258 return window_cone_sphere(cone, sphere, cone_first);
3259 }
3260 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
3261 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
3262 .collect::<Option<_>>()?;
3263 let loops: Vec<Vec<Point3>> = [0, 1]
3264 .iter()
3265 .map(|&j| {
3266 let mut pts: Vec<Point3> = rows
3267 .iter()
3268 .enumerate()
3269 .map(|(i, &(near, far))| {
3270 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
3271 })
3272 .collect();
3273 pts.push(pts[0]);
3274 pts
3275 })
3276 .collect();
3277 Some(fit_ruling_loops(&loops, |p| {
3278 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3279 }))
3280}
3281
3282fn window_cone_sphere(
3297 cone: &ConicalSurface,
3298 sphere: &SphericalSurface,
3299 cone_first: bool,
3300) -> Option<Vec<IntersectionCurve>> {
3301 let offset = cone.apex() - sphere.center();
3302 let lin_tol = Tolerance::new().linear;
3303 if offset.length() - sphere.radius() <= lin_tol {
3304 return None;
3305 }
3306 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
3307 let (sin_a, cos_a) = cone.half_angle().sin_cos();
3308 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
3309 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
3310 if a < lin_tol {
3311 return None;
3312 }
3313 let reach = (-k.sqrt() - c) / a;
3314 if reach <= -1.0 {
3315 return Some(Vec::new());
3316 }
3317 if reach >= 1.0 {
3318 return None;
3319 }
3320 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
3321 let half = std::f64::consts::PI - half;
3322 let n = RULING_SAMPLES;
3323 let mut pts: Vec<Point3> = (0..n)
3324 .map(|i| {
3325 #[allow(clippy::cast_precision_loss)]
3326 let theta = TAU * i as f64 / n as f64;
3327 let u = half.mul_add(-theta.cos(), mid);
3328 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
3329 let split = h.mul_add(h, -k).max(0.0).sqrt();
3330 cone.evaluate(u, -h - split.copysign(theta.sin()))
3331 })
3332 .collect();
3333 pts.push(pts[0]);
3334 Some(fit_ruling_loops(&[pts], |p| {
3335 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3336 }))
3337}
3338
3339const WINDOW_SCAN: usize = 16;
3342const WINDOW_MIN_SAMPLES: usize = 8;
3343
3344const RULING_SAMPLES: usize = 128;
3348
3349#[allow(clippy::cast_precision_loss)]
3350fn ruling_u(i: usize) -> f64 {
3351 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
3352}
3353
3354fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
3356 let disc = b * b - 4.0 * quad * c;
3357 let root = disc.max(0.0).sqrt();
3358 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
3359}
3360
3361fn ruling_samples(
3365 sweep: &CylindricalSurface,
3366 roots: &impl Fn(f64) -> (f64, f64, f64),
3367) -> Vec<Option<(Point3, Point3)>> {
3368 let lin_tol = Tolerance::new().linear;
3369 (0..RULING_SAMPLES)
3370 .map(|i| {
3371 let u = ruling_u(i);
3372 let (disc, vp, vm) = roots(u);
3373 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
3374 })
3375 .collect()
3376}
3377
3378fn closed_ruling_loops(
3385 sweep: &CylindricalSurface,
3386 roots: &impl Fn(f64) -> (f64, f64, f64),
3387 samples: &[Option<(Point3, Point3)>],
3388) -> Vec<Vec<Point3>> {
3389 let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3390 if let Some((neck, touching)) = narrowest_ruling(roots) {
3391 let count = 2 * RULING_SAMPLES;
3392 #[allow(clippy::cast_precision_loss)]
3393 (0..count)
3394 .map(|i| {
3395 let t = i as f64 / count as f64;
3396 let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3397 let (_, vp, vm) = roots(u);
3398 if i == 0 && touching {
3399 let at = sweep.evaluate(u, 0.5 * (vp + vm));
3400 (at, at)
3401 } else {
3402 (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3403 }
3404 })
3405 .unzip()
3406 } else {
3407 samples.iter().flatten().copied().unzip()
3408 };
3409 plus.push(plus[0]);
3410 minus.push(minus[0]);
3411 vec![plus, minus]
3412}
3413
3414fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3422 let scan = WINDOW_SCAN * RULING_SAMPLES;
3423 #[allow(clippy::cast_precision_loss)]
3424 let step = TAU / scan as f64;
3425 let gap = |u: f64| {
3426 let (_, vp, vm) = roots(u);
3427 (vp - vm).abs()
3428 };
3429 #[allow(clippy::cast_precision_loss)]
3430 let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3431 let widest = gaps.iter().copied().fold(0.0, f64::max);
3432 let tol = Tolerance::new().linear * (1.0 + widest);
3433 let mut necks = Vec::new();
3434 for k in 0..scan {
3435 let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3436 if here > before || here >= after || here >= 0.25 * widest {
3437 continue;
3438 }
3439 #[allow(clippy::cast_precision_loss)]
3440 let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3441 for _ in 0..100 {
3442 let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3443 if gap(a) < gap(b) {
3444 hi = b;
3445 } else {
3446 lo = a;
3447 }
3448 }
3449 let u = 0.5 * (lo + hi);
3450 necks.push((u, gap(u) <= tol));
3451 }
3452 match necks[..] {
3453 [neck] => Some(neck),
3454 _ => None,
3455 }
3456}
3457
3458fn partial_ruling_loops(
3463 sweep: &CylindricalSurface,
3464 roots: &impl Fn(f64) -> (f64, f64, f64),
3465 samples: &[Option<(Point3, Point3)>],
3466) -> Vec<Vec<Point3>> {
3467 let branch_point = |inside: usize, outside: usize| -> Point3 {
3468 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3469 if (hi - lo).abs() > std::f64::consts::PI {
3470 hi += if hi < lo { TAU } else { -TAU };
3471 }
3472 for _ in 0..60 {
3473 let mid = 0.5 * (lo + hi);
3474 if roots(mid).0 >= 0.0 {
3475 lo = mid;
3476 } else {
3477 hi = mid;
3478 }
3479 }
3480 let (_, vp, vm) = roots(lo);
3481 sweep.evaluate(lo, 0.5 * (vp + vm))
3482 };
3483 let Some(first_gap) = samples.iter().position(Option::is_none) else {
3484 return Vec::new();
3485 };
3486 let mut loops = Vec::new();
3487 let mut k = 0;
3488 while k < RULING_SAMPLES {
3489 let i = (first_gap + k) % RULING_SAMPLES;
3490 if samples[i].is_none() {
3491 k += 1;
3492 continue;
3493 }
3494 let start = i;
3495 let mut run = Vec::new();
3496 while k < RULING_SAMPLES {
3497 let j = (first_gap + k) % RULING_SAMPLES;
3498 let Some(pair) = samples[j] else { break };
3499 run.push(pair);
3500 k += 1;
3501 }
3502 let end = (start + run.len() - 1) % RULING_SAMPLES;
3503 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3504 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3505 let mut pts = vec![head];
3506 pts.extend(run.iter().map(|p| p.0));
3507 pts.push(tail);
3508 pts.extend(run.iter().rev().map(|p| p.1));
3509 pts.push(head);
3510 loops.push(pts);
3511 }
3512 loops
3513}
3514
3515fn fit_ruling_loops(
3518 loops: &[Vec<Point3>],
3519 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3520) -> Vec<IntersectionCurve> {
3521 let mut curves = Vec::new();
3522 for pts in loops {
3523 if pts.len() < 4 {
3524 continue;
3525 }
3526 let ipts: Vec<IntersectionPoint> = pts
3527 .iter()
3528 .map(|&p| {
3529 let (param1, param2) = params(p);
3530 IntersectionPoint {
3531 point: p,
3532 param1,
3533 param2,
3534 }
3535 })
3536 .collect();
3537 let degree = 3.min(pts.len() - 1);
3538 if let Ok(curve) = interpolate(pts, degree) {
3539 curves.push(IntersectionCurve {
3540 curve,
3541 points: ipts,
3542 });
3543 }
3544 }
3545 curves
3546}
3547
3548#[allow(clippy::unnecessary_wraps)]
3574fn algebraic_parallel_cone_cylinder(
3575 cone: &ConicalSurface,
3576 cyl: &CylindricalSurface,
3577 v_range_cone: Option<(f64, f64)>,
3578 v_range_cyl: Option<(f64, f64)>,
3579) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3580 let axis = cone.axis();
3581 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3582 return Ok(None); }
3584
3585 let apex = cone.apex();
3586 let delta = cyl.origin() - apex;
3587 let along = delta.dot(axis);
3588 let perp = delta - axis * along;
3589 let d = perp.length();
3590 if d < 1e-9 {
3591 return Ok(None); }
3593
3594 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3595 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3596
3597 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3598 if cos_t < 1e-12 || sin_t < 1e-12 {
3599 return Ok(None);
3600 }
3601 let r = cyl.radius();
3602
3603 let mut v_min = (d - r).abs() / cos_t;
3605 let mut v_max = (d + r) / cos_t;
3606 if v_max <= v_min {
3607 return Ok(Some(vec![]));
3608 }
3609
3610 let mut lo = v_min;
3616 let mut hi = v_max;
3617 if let Some((a, b)) = v_range_cone {
3622 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3623 lo = lo.max(a);
3624 hi = hi.min(b);
3625 }
3626 if let Some((a, b)) = v_range_cyl {
3627 let flip = cyl.axis().dot(axis);
3630 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3631 let (a, b) = (to_cone_v(a), to_cone_v(b));
3632 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3633 lo = lo.max(a);
3634 hi = hi.min(b);
3635 }
3636 let (turn_lo, turn_hi) = (v_min, v_max);
3637 v_min = lo.max(v_min);
3638 v_max = hi.min(v_max);
3639 if v_max - v_min <= 1e-12 {
3640 return Ok(Some(vec![]));
3641 }
3642 let slack = Tolerance::new().linear;
3650 #[allow(clippy::cast_precision_loss)]
3651 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3652 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3653 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3654 .map(|i| {
3655 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3656 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3657 let off = foot - apex;
3658 let across = off - axis * off.dot(axis);
3659 apex + across + axis * (across.length() * sin_t / cos_t)
3660 })
3661 .collect();
3662 pts.push(pts[0]);
3663 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3664 (cone.project_point(p), cyl.project_point(p))
3665 })));
3666 }
3667
3668 let n_samples = 128;
3669 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3670 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3671 #[allow(clippy::cast_precision_loss)]
3672 for i in 0..=n_samples {
3673 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3674 let rho = v * cos_t;
3675 if rho < 1e-12 {
3676 if (d - r).abs() < 1e-12 {
3684 let apex = cone.evaluate(phi0, v);
3685 plus.push(apex);
3686 minus.push(apex);
3687 }
3688 continue;
3689 }
3690 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3691 let alpha = cos_alpha.acos();
3692 plus.push(cone.evaluate(phi0 + alpha, v));
3693 minus.push(cone.evaluate(phi0 - alpha, v));
3694 }
3695
3696 let mut curves = Vec::new();
3697 for pts in [&plus, &minus] {
3698 if pts.len() < 4 {
3701 continue;
3702 }
3703 let ipts: Vec<IntersectionPoint> = pts
3704 .iter()
3705 .map(|&p| IntersectionPoint {
3706 point: p,
3707 param1: cone.project_point(p),
3708 param2: cyl.project_point(p),
3709 })
3710 .collect();
3711 let degree = 3.min(pts.len() - 1);
3712 match interpolate(pts, degree) {
3713 Ok(curve) => curves.push(IntersectionCurve {
3714 curve,
3715 points: ipts,
3716 }),
3717 Err(_) => return Ok(None),
3722 }
3723 }
3724
3725 Ok(Some(curves))
3726}
3727
3728fn algebraic_sphere_sphere(
3736 s1: &SphericalSurface,
3737 s2: &SphericalSurface,
3738) -> Result<Vec<IntersectionCurve>, MathError> {
3739 let c1 = s1.center();
3740 let c2 = s2.center();
3741 let r1 = s1.radius();
3742 let r2 = s2.radius();
3743
3744 let delta = c2 - c1;
3745 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3746 let d = d_sq.sqrt();
3747
3748 if d < 1e-12 {
3749 return Ok(vec![]);
3751 }
3752
3753 if d > r1 + r2 + 1e-10 {
3755 return Ok(vec![]); }
3757 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3758 return Ok(vec![]); }
3760
3761 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3763
3764 let r_circle_sq = r1 * r1 - d1 * d1;
3766 if r_circle_sq < 0.0 {
3767 if r_circle_sq > -1e-10 {
3769 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3771 let tangent_pt = Point3::new(
3772 c1.x() + axis.x() * d1,
3773 c1.y() + axis.y() * d1,
3774 c1.z() + axis.z() * d1,
3775 );
3776 let ipt = IntersectionPoint {
3777 point: tangent_pt,
3778 param1: (0.0, 0.0),
3779 param2: (0.0, 0.0),
3780 };
3781 return Ok(vec![IntersectionCurve {
3783 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3784 points: vec![ipt],
3785 }]);
3786 }
3787 return Ok(vec![]);
3788 }
3789
3790 let r_circle = r_circle_sq.sqrt();
3791 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3792 let center = Point3::new(
3793 c1.x() + axis.x() * d1,
3794 c1.y() + axis.y() * d1,
3795 c1.z() + axis.z() * d1,
3796 );
3797
3798 let basis = Frame3::from_normal(center, axis)?;
3800 let u_dir = basis.x;
3801 let v_dir = basis.y;
3802
3803 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3806 #[allow(clippy::cast_precision_loss)]
3807 for i in 0..n_samples {
3808 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3809 let (sin_t, cos_t) = theta.sin_cos();
3810 let pt = Point3::new(
3811 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3812 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3813 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3814 );
3815 points.push(IntersectionPoint {
3816 point: pt,
3817 param1: (0.0, 0.0),
3818 param2: (0.0, 0.0),
3819 });
3820 }
3821
3822 let curve = conic_loop(center, u_dir, v_dir, r_circle, r_circle)?;
3823
3824 Ok(vec![IntersectionCurve { curve, points }])
3825}
3826
3827#[allow(clippy::too_many_arguments)]
3833fn correct_to_intersection(
3834 a: &AnalyticSurface<'_>,
3835 b: &AnalyticSurface<'_>,
3836 surf_a: &dyn Fn(f64, f64) -> Point3,
3837 norm_a: &dyn Fn(f64, f64) -> Vec3,
3838 surf_b: &dyn Fn(f64, f64) -> Point3,
3839 norm_b: &dyn Fn(f64, f64) -> Vec3,
3840 point: Point3,
3841 u_range_a: (f64, f64),
3842 v_range_a: (f64, f64),
3843 u_range_b: (f64, f64),
3844 v_range_b: (f64, f64),
3845 max_iters: usize,
3846) -> Point3 {
3847 let mut p = point;
3848 for _ in 0..max_iters {
3849 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3850 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3851 let pa = surf_a(ua, va);
3852 let pb = surf_b(ub, vb);
3853 let na = norm_a(ua, va);
3854 let nb = norm_b(ub, vb);
3855 let pv = Vec3::new(p.x(), p.y(), p.z());
3856
3857 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3858 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3859
3860 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3861 break;
3862 }
3863
3864 let t = na.cross(nb);
3865 let t_len = t.length();
3866 if t_len < 1e-10 {
3867 return Point3::new(
3869 (pa.x() + pb.x()) * 0.5,
3870 (pa.y() + pb.y()) * 0.5,
3871 (pa.z() + pb.z()) * 0.5,
3872 );
3873 }
3874 let t_hat = t * (1.0 / t_len);
3875
3876 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3878 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3879 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3880 if det.abs() < 1e-15 {
3881 return Point3::new(
3882 (pa.x() + pb.x()) * 0.5,
3883 (pa.y() + pb.y()) * 0.5,
3884 (pa.z() + pb.z()) * 0.5,
3885 );
3886 }
3887 let inv = 1.0 / det;
3888 let dx = inv
3890 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3891 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3892 let dy = inv
3893 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3894 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3895 let dz = inv
3896 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3897 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3898 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3899
3900 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3903 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3904 let pc_a = surf_a(uc, vc);
3905 let pc_b = surf_b(ud, vd);
3906 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3907 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3908 .dot(norm_a(uc, vc))
3909 .abs();
3910 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3911 .dot(norm_b(ud, vd))
3912 .abs();
3913 if da_new > da.abs() && db_new > db.abs() {
3914 return p;
3915 }
3916
3917 p = candidate;
3918 }
3919 p
3920}
3921
3922#[allow(clippy::too_many_arguments)]
3928fn march_analytic_intersection(
3929 a: &AnalyticSurface<'_>,
3930 b: &AnalyticSurface<'_>,
3931 surf_a: &dyn Fn(f64, f64) -> Point3,
3932 norm_a: &dyn Fn(f64, f64) -> Vec3,
3933 surf_b: &dyn Fn(f64, f64) -> Point3,
3934 norm_b: &dyn Fn(f64, f64) -> Vec3,
3935 seed: Point3,
3936 u_range_a: (f64, f64),
3937 v_range_a: (f64, f64),
3938 u_range_b: (f64, f64),
3939 v_range_b: (f64, f64),
3940 initial_step: f64,
3941 u_periodic_a: bool,
3942 u_periodic_b: bool,
3943 region: Option<Aabb3>,
3944) -> Vec<Point3> {
3945 let max_steps = 500;
3946 let h_min = 1e-6;
3947 let h_max = initial_step * 4.0;
3948 let closure_dist = initial_step * 5.0;
3952 let max_angle = 10.0_f64.to_radians();
3954 let min_angle = 2.0_f64.to_radians();
3955
3956 let mut forward = Vec::new();
3958 let mut backward = Vec::new();
3960
3961 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3962 let mut current = seed;
3963 let mut h = initial_step;
3964 let mut prev_tangent: Option<Vec3> = None;
3965
3966 for _ in 0..max_steps {
3967 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3968 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3969
3970 let na = norm_a(ua, va);
3971 let nb = norm_b(ub, vb);
3972
3973 let tangent = na.cross(nb);
3974 let t_len = tangent.length();
3975 if t_len < 1e-10 {
3976 break;
3977 }
3978 let t_dir = tangent * (direction / t_len);
3979
3980 if let Some(prev_t) = prev_tangent {
3982 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3983 let angle = cos_angle.acos();
3984 if angle > max_angle && h > h_min {
3985 h = (h * 0.5).max(h_min);
3986 } else if angle < min_angle {
3987 h = (h * 2.0).min(h_max);
3988 }
3989 }
3990 prev_tangent = Some(t_dir);
3991
3992 let next = Point3::new(
3993 h.mul_add(t_dir.x(), current.x()),
3994 h.mul_add(t_dir.y(), current.y()),
3995 h.mul_add(t_dir.z(), current.z()),
3996 );
3997
3998 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3999 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
4000
4001 let pa = surf_a(ua2, va2);
4002 let pb = surf_b(ub2, vb2);
4003 let mid = Point3::new(
4004 (pa.x() + pb.x()) * 0.5,
4005 (pa.y() + pb.y()) * 0.5,
4006 (pa.z() + pb.z()) * 0.5,
4007 );
4008 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
4009 || va2 <= v_range_a.0
4010 || va2 >= v_range_a.1;
4011 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
4012 || vb2 <= v_range_b.0
4013 || vb2 >= v_range_b.1;
4014
4015 if out_a || out_b {
4016 break;
4017 }
4018 if region.is_some_and(|r| !r.contains_point(mid)) {
4019 points.push(mid);
4020 break;
4021 }
4022
4023 let dist_to_seed = (mid - seed).length();
4027 if points.len() > 10 && dist_to_seed < closure_dist {
4028 points.push(seed);
4029 break;
4030 }
4031
4032 points.push(mid);
4033 current = mid;
4034 }
4035 }
4036
4037 backward.reverse();
4039 let mut result = backward;
4040 result.push(seed);
4041 result.append(&mut forward);
4042
4043 for pt in &mut result {
4045 *pt = correct_to_intersection(
4046 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
4047 5,
4048 );
4049 }
4050
4051 result
4052}
4053
4054fn project_analytic(
4058 surface: &AnalyticSurface<'_>,
4059 point: Point3,
4060 u_range: (f64, f64),
4061 v_range: (f64, f64),
4062) -> (f64, f64) {
4063 match surface {
4064 AnalyticSurface::Cylinder(cyl) => {
4065 let (u, v) = cyl.project_point(point);
4066 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
4067 }
4068 AnalyticSurface::Sphere(sphere) => {
4069 let (u, v) = sphere.project_point(point);
4070 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
4071 }
4072 AnalyticSurface::Cone(cone) => {
4073 let (u, v) = cone.project_point(point);
4074 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
4075 }
4076 AnalyticSurface::Torus(torus) => {
4077 let (u, v) = torus.project_point(point);
4078 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
4079 }
4080 }
4081}
4082
4083fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
4087 matches!(
4088 surface,
4089 AnalyticSurface::Cylinder(_)
4090 | AnalyticSurface::Cone(_)
4091 | AnalyticSurface::Sphere(_)
4092 | AnalyticSurface::Torus(_)
4093 )
4094}
4095
4096#[allow(clippy::type_complexity)]
4098fn surface_closures<'a>(
4099 surface: &'a AnalyticSurface<'a>,
4100) -> (
4101 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
4102 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
4103 (f64, f64),
4104 (f64, f64),
4105) {
4106 match surface {
4107 AnalyticSurface::Cylinder(cyl) => (
4108 Box::new(|u, v| cyl.evaluate(u, v)),
4109 Box::new(|u, v| cyl.normal(u, v)),
4110 (0.0, TAU),
4111 (-1.0, 1.0),
4112 ),
4113 AnalyticSurface::Cone(cone) => (
4114 Box::new(|u, v| cone.evaluate(u, v)),
4115 Box::new(|u, v| cone.normal(u, v)),
4116 (0.0, TAU),
4117 (0.01, 2.0),
4118 ),
4119 AnalyticSurface::Sphere(sphere) => (
4120 Box::new(|u, v| sphere.evaluate(u, v)),
4121 Box::new(|u, v| sphere.normal(u, v)),
4122 (0.0, TAU),
4123 (-FRAC_PI_2, FRAC_PI_2),
4124 ),
4125 AnalyticSurface::Torus(torus) => (
4126 Box::new(|u, v| torus.evaluate(u, v)),
4127 Box::new(|u, v| torus.normal(u, v)),
4128 (0.0, TAU),
4129 (0.0, TAU),
4130 ),
4131 }
4132}
4133
4134#[cfg(test)]
4135#[allow(clippy::unwrap_used, clippy::expect_used)]
4136mod tests {
4137 use super::*;
4138 use crate::tolerance::Tolerance;
4139
4140 #[test]
4144 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
4145 let half_angle = 1.1_f64;
4146 let cone = ConicalSurface::new(
4147 Point3::new(0.0, 0.0, 0.0),
4148 Vec3::new(0.0, 0.0, 1.0),
4149 half_angle,
4150 )
4151 .unwrap();
4152 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
4153 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
4154 let chains =
4155 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
4156 .unwrap();
4157 let chain = chains
4158 .iter()
4159 .find_map(|c| match c {
4160 ExactIntersectionCurve::Points(chain) => Some(chain),
4161 _ => None,
4162 })
4163 .expect("a parabola or hyperbola section is sampled");
4164 let (from, to) = (chain[2], chain[chain.len() - 3]);
4165 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
4166 .unwrap()
4167 .expect("an exact arc");
4168 let (t0, t1) = arc.domain();
4169 assert!((arc.evaluate(t0) - from).length() < 1e-12);
4170 assert!((arc.evaluate(t1) - to).length() < 1e-12);
4171 for i in 0..=200 {
4172 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
4173 let w = q - Point3::new(0.0, 0.0, 0.0);
4174 let off_plane = (normal.dot(w) - d).abs();
4175 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
4176 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
4177 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
4178 }
4179 }
4180 }
4181
4182 #[test]
4186 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
4187 let half_angle = 1.1_f64;
4188 let cone = ConicalSurface::new(
4189 Point3::new(0.0, 0.0, 0.0),
4190 Vec3::new(0.0, 0.0, 1.0),
4191 half_angle,
4192 )
4193 .unwrap();
4194 for shortfall in [1e-10, 3e-10, 8e-10] {
4195 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
4196 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4197 let chains =
4198 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
4199 .unwrap();
4200 let Some(chain) = chains.iter().find_map(|c| match c {
4201 ExactIntersectionCurve::Points(chain) => Some(chain),
4202 _ => None,
4203 }) else {
4204 continue;
4205 };
4206 let (from, to) = (chain[2], chain[chain.len() - 3]);
4207 assert!(
4208 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
4209 .unwrap()
4210 .is_none(),
4211 "coincident ends"
4212 );
4213 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
4214 continue;
4215 };
4216 let (t0, t1) = arc.domain();
4217 for i in 0..=200 {
4218 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
4219 - Point3::new(0.0, 0.0, 0.0);
4220 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
4221 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
4222 }
4223 }
4224 }
4225
4226 #[test]
4227 fn plane_cylinder_perpendicular() {
4228 let cyl =
4229 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4230 .unwrap();
4231
4232 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
4234 assert!(!curves.is_empty(), "should find intersection curve");
4235 assert!(
4236 curves[0].points.len() > 10,
4237 "should have many sample points"
4238 );
4239
4240 let tol = Tolerance::loose();
4241 for pt in &curves[0].points {
4242 assert!(
4243 tol.approx_eq(pt.point.z(), 3.0),
4244 "z should be ~3.0, got {}",
4245 pt.point.z()
4246 );
4247 let r = pt.point.x().hypot(pt.point.y());
4248 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
4249 }
4250 }
4251
4252 #[test]
4253 fn plane_sphere_equator() {
4254 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4255
4256 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4257 assert!(!curves.is_empty());
4258
4259 let tol = Tolerance::loose();
4260 for pt in &curves[0].points {
4261 assert!(
4262 tol.approx_eq(pt.point.z(), 0.0),
4263 "z should be ~0, got {}",
4264 pt.point.z()
4265 );
4266 let r = pt.point.x().hypot(pt.point.y());
4267 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
4268 }
4269 }
4270
4271 #[test]
4272 fn plane_sphere_no_intersection() {
4273 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
4274
4275 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
4276 assert!(curves.is_empty());
4277 }
4278
4279 #[test]
4280 fn plane_cone_cross_section() {
4281 let cone = ConicalSurface::new(
4282 Point3::new(0.0, 0.0, 0.0),
4283 Vec3::new(0.0, 0.0, 1.0),
4284 std::f64::consts::FRAC_PI_4,
4285 )
4286 .unwrap();
4287
4288 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
4289 assert!(!curves.is_empty(), "should find intersection with cone");
4290 }
4291
4292 #[test]
4299 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
4300 let c1 = ConicalSurface::new(
4301 Point3::new(
4302 -16.999_999_999_999_975,
4303 -16.999_999_999_999_975,
4304 5.849_999_999_999_951,
4305 ),
4306 Vec3::new(0.0, 0.0, -1.0),
4307 0.785_398_163_397_433_5,
4308 )
4309 .unwrap();
4310 let c2 = ConicalSurface::new(
4311 Point3::new(
4312 -16.750_000_000_000_036,
4313 -16.750_000_000_000_018,
4314 0.749_999_999_999_881,
4315 ),
4316 Vec3::new(0.0, 0.0, 1.0),
4317 0.785_398_163_397_467_6,
4318 )
4319 .unwrap();
4320
4321 let curves = exact_cone_cone(&c1, &c2)
4322 .unwrap()
4323 .expect("offset parallel equal-angle cones must take the radical-plane path");
4324 assert_eq!(curves.len(), 1, "expected exactly one section conic");
4325 assert!(
4326 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
4327 "expected an ellipse section, got {:?}",
4328 curves[0]
4329 );
4330 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
4331 return;
4332 };
4333
4334 for i in 0..16 {
4338 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
4339 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
4340 let rel = p - cone.apex();
4341 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
4342 let axial = rel_v.dot(cone.axis());
4343 let radial = (rel_v - cone.axis() * axial).length();
4344 assert!(
4345 axial > 0.0,
4346 "{label}: sample on phantom nappe (axial {axial})"
4347 );
4348 let expect = cone.half_angle().tan() * axial;
4349 assert!(
4350 (radial - expect).abs() < 1e-9,
4351 "{label}: sample off surface by {}",
4352 (radial - expect).abs()
4353 );
4354 }
4355 }
4356 }
4357
4358 #[test]
4362 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
4363 let c1 = ConicalSurface::new(
4364 Point3::new(0.0, 0.0, 5.0),
4365 Vec3::new(0.0, 0.0, -1.0),
4366 std::f64::consts::FRAC_PI_4,
4367 )
4368 .unwrap();
4369 let c2 = ConicalSurface::new(
4370 Point3::new(0.25, 0.25, 20.0),
4371 Vec3::new(0.0, 0.0, 1.0),
4372 std::f64::consts::FRAC_PI_4,
4373 )
4374 .unwrap();
4375 let curves = exact_cone_cone(&c1, &c2)
4376 .unwrap()
4377 .expect("radical-plane path");
4378 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
4379 }
4380
4381 #[test]
4384 fn offset_parallel_cones_with_unequal_angles_defer() {
4385 let c1 = ConicalSurface::new(
4386 Point3::new(0.0, 0.0, 5.0),
4387 Vec3::new(0.0, 0.0, -1.0),
4388 std::f64::consts::FRAC_PI_4,
4389 )
4390 .unwrap();
4391 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4392 .unwrap();
4393 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4394 }
4395
4396 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4400 let cone = ConicalSurface::new(
4401 Point3::new(0.0, 0.0, 0.0),
4402 Vec3::new(0.0, 0.0, 1.0),
4403 std::f64::consts::FRAC_PI_4,
4404 )
4405 .unwrap();
4406 let (s, c) = 40.0_f64.to_radians().sin_cos();
4407 let tube =
4408 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4409 (cone, tube)
4410 }
4411
4412 #[test]
4413 fn marcher_keeps_to_its_region() {
4414 let (cone, tube) = cone_and_tilted_tube();
4415 let run = |region: Option<Aabb3>| {
4416 let (a, b) = (
4417 AnalyticSurface::Cone(&cone),
4418 AnalyticSurface::Cylinder(&tube),
4419 );
4420 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4421 match region {
4422 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4423 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4424 }
4425 .unwrap()
4426 };
4427 assert!(!run(None).is_empty());
4428 let near = Aabb3 {
4429 min: Point3::new(-0.5, -2.0, 0.8),
4430 max: Point3::new(0.7, -0.7, 2.0),
4431 };
4432 let curves = run(Some(near));
4433 assert!(!curves.is_empty(), "the loop through the region is kept");
4434 let reach = near.expanded(0.6);
4436 for curve in &curves {
4437 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4438 }
4439 let away = Aabb3 {
4440 min: Point3::new(5.0, 5.0, 5.0),
4441 max: Point3::new(6.0, 6.0, 6.0),
4442 };
4443 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4444 }
4445
4446 #[test]
4447 fn coaxial_cones_cross_at_single_circle() {
4448 let outer = ConicalSurface::new(
4453 Point3::new(0.0, 0.0, 50.0),
4454 Vec3::new(0.0, 0.0, -1.0),
4455 5.0_f64.atan(),
4456 )
4457 .unwrap();
4458 let inner = ConicalSurface::new(
4459 Point3::new(0.0, 0.0, 90.0),
4460 Vec3::new(0.0, 0.0, -1.0),
4461 10.0_f64.atan(),
4462 )
4463 .unwrap();
4464
4465 let curves = intersect_analytic_analytic_bounded(
4466 AnalyticSurface::Cone(&outer),
4467 AnalyticSurface::Cone(&inner),
4468 32,
4469 None,
4470 None,
4471 )
4472 .unwrap();
4473
4474 assert_eq!(
4475 curves.len(),
4476 1,
4477 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4478 curves.len()
4479 );
4480 for p in &curves[0].points {
4481 let r = p.point.x().hypot(p.point.y());
4482 assert!(
4483 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4484 "intersection point off the expected z=10,r=8 circle: {:?}",
4485 p.point
4486 );
4487 }
4488 }
4489
4490 #[test]
4491 fn plane_torus_cross_section() {
4492 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4493
4494 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4495 assert!(
4496 !curves.is_empty(),
4497 "should find intersection curves with torus"
4498 );
4499 }
4500
4501 #[test]
4504 fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4505 for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4506 let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4507 for z in [3.0 - minor, 3.0 + minor] {
4508 let exact = exact_plane_analytic(
4509 AnalyticSurface::Torus(&torus),
4510 Vec3::new(0.0, 0.0, 1.0),
4511 z,
4512 )
4513 .unwrap();
4514 assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4515 let circle = match &exact[0] {
4516 ExactIntersectionCurve::Circle(c) => Some(c),
4517 _ => None,
4518 };
4519 let c = circle.expect("a circle");
4520 assert!((c.radius() - major).abs() < 1e-12);
4521 assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4522
4523 let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4524 assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4525 }
4526 let single = |normal: Vec3, d: f64| {
4528 matches!(
4529 exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4530 .unwrap()
4531 .as_slice(),
4532 [ExactIntersectionCurve::Circle(_)]
4533 )
4534 };
4535 assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4536 }
4537 }
4538
4539 #[test]
4543 fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4544 let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4545 let level = Vec3::new(0.0, 0.0, 1.0);
4546 let curves =
4547 |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4548 assert!(matches!(
4549 curves(3.0 + 100.1).as_slice(),
4550 [ExactIntersectionCurve::Circle(_)]
4551 ));
4552 let inside = curves(3.0 + 100.1 - 4e-7);
4553 assert!(!matches!(
4554 inside.as_slice(),
4555 [ExactIntersectionCurve::Circle(_)]
4556 ));
4557 }
4558
4559 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4562 let rho = p.x().hypot(p.y());
4563 ((rho - major).hypot(p.z())) - minor
4564 }
4565
4566 #[test]
4572 fn oblique_cone_cylinder_traces_curves_on_both() {
4573 use crate::traits::ParametricCurve;
4574 let cone = ConicalSurface::new(
4578 Point3::new(0.0, 0.0, 3.0),
4579 Vec3::new(0.0, 0.0, -1.0),
4580 2.0_f64.atan(),
4581 )
4582 .unwrap();
4583 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4584 let cyl =
4585 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4586 .unwrap();
4587 for cone_first in [true, false] {
4588 let (a, b) = if cone_first {
4589 (
4590 AnalyticSurface::Cone(&cone),
4591 AnalyticSurface::Cylinder(&cyl),
4592 )
4593 } else {
4594 (
4595 AnalyticSurface::Cylinder(&cyl),
4596 AnalyticSurface::Cone(&cone),
4597 )
4598 };
4599 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4600 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4601 for c in &curves {
4602 let (t0, t1) = c.curve.domain();
4603 for k in 0..=64 {
4604 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4605 let p = ParametricCurve::evaluate(&c.curve, t);
4606 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4609 assert!(
4610 (rod - 0.6).abs() < 1e-4,
4611 "x0 {x0}: off the rod by {}",
4612 rod - 0.6
4613 );
4614 let cone_r = p.x().hypot(p.y());
4615 assert!(
4616 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4617 "x0 {x0}: off the cone at {p:?}"
4618 );
4619 }
4620 }
4621 }
4622 }
4623 }
4624
4625 #[test]
4626 fn a_rod_through_a_rings_tube_traces_four_loops() {
4627 use crate::traits::ParametricCurve;
4628 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4629 let rod =
4632 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4633 .unwrap();
4634 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4635 assert_eq!(curves.len(), 4);
4636 for c in &curves {
4637 let (t0, t1) = c.curve.domain();
4638 for k in 0..=64 {
4639 let p =
4640 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4641 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4642 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4643 assert!(
4644 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4645 "off by {on_rod}, {on_ring}"
4646 );
4647 }
4648 }
4649 let high =
4651 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4652 .unwrap();
4653 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4654 let grazing =
4656 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4657 .unwrap();
4658 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4659 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4661 let thin =
4662 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4663 .unwrap();
4664 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4665 }
4666
4667 #[test]
4668 fn a_pin_through_a_ball_traces_two_loops() {
4669 use crate::traits::ParametricCurve;
4670 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4671 let half = 0.08_f64.atan();
4674 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4675 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4676 for cone_first in [true, false] {
4677 let (a, b) = if cone_first {
4678 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4679 } else {
4680 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4681 };
4682 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4683 assert_eq!(curves.len(), 2, "entry and exit loops");
4684 for c in &curves {
4685 let (t0, t1) = c.curve.domain();
4686 for k in 0..=64 {
4687 let p = ParametricCurve::evaluate(
4688 &c.curve,
4689 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4690 );
4691 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4692 let axial = apex.z() - p.z();
4693 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4694 assert!(
4695 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4696 "off by {on_ball}, {on_pin}"
4697 );
4698 }
4699 }
4700 }
4701 let coaxial =
4706 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4707 .unwrap();
4708 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4709 let aside = ConicalSurface::new(
4710 Point3::new(2.8, 0.0, 10.0),
4711 Vec3::new(0.0, 0.0, -1.0),
4712 FRAC_PI_2 - half,
4713 )
4714 .unwrap();
4715 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4716 let holding = ConicalSurface::new(
4717 Point3::new(1.0, 0.5, 1.0),
4718 Vec3::new(0.0, 0.0, -1.0),
4719 FRAC_PI_2 - half,
4720 )
4721 .unwrap();
4722 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4723 let away = ConicalSurface::new(
4724 Point3::new(1.0, 0.5, 10.0),
4725 Vec3::new(0.0, 0.0, 1.0),
4726 FRAC_PI_2 - half,
4727 )
4728 .unwrap();
4729 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4730 let step = TAU / 2048.0;
4734 let grazed =
4735 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4736 .unwrap();
4737 let wide =
4738 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4739 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4740 }
4741
4742 #[test]
4743 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4744 use crate::traits::ParametricCurve;
4745 let cone = ConicalSurface::new(
4747 Point3::new(0.0, 0.0, 3.0),
4748 Vec3::new(0.0, 0.0, -1.0),
4749 2.0_f64.atan(),
4750 )
4751 .unwrap();
4752 for (centre, radius) in [
4753 (Point3::new(1.0, 0.8, 1.2), 1.1),
4754 (Point3::new(1.5, 0.0, 0.0), 0.8),
4755 ] {
4756 let ball = SphericalSurface::new(centre, radius).unwrap();
4757 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4758 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4759 let (t0, t1) = curves[0].curve.domain();
4760 for k in 0..=64 {
4761 let p = ParametricCurve::evaluate(
4762 &curves[0].curve,
4763 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4764 );
4765 let on_ball = (p - centre).length() - radius;
4766 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4767 assert!(
4768 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4769 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4770 );
4771 }
4772 }
4773 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4775 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4776 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4777 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4778 }
4779
4780 #[test]
4781 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4782 use crate::traits::ParametricCurve;
4783 let cone = ConicalSurface::new(
4784 Point3::new(0.0, 0.0, 3.0),
4785 Vec3::new(0.0, 0.0, -1.0),
4786 2.0_f64.atan(),
4787 )
4788 .unwrap();
4789 for (centre, radius) in [
4792 (Point3::new(0.5, 0.0, 2.5), 2.0),
4793 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4794 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4795 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4796 ] {
4797 let ball = SphericalSurface::new(centre, radius).unwrap();
4798 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4799 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4800 let (t0, t1) = curves[0].curve.domain();
4801 for k in 0..=4096 {
4802 let p = ParametricCurve::evaluate(
4803 &curves[0].curve,
4804 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4805 );
4806 let on_ball = (p - centre).length() - radius;
4807 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4808 assert!(
4809 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4810 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4811 );
4812 }
4813 }
4814 }
4815
4816 #[test]
4817 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4818 let t = 2.0_f64.atan();
4819 let cone =
4820 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4821 let through_apex =
4823 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4824 .unwrap();
4825 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4826 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4828 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4829 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4830 let pin =
4833 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4834 let tube =
4835 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4836 .unwrap();
4837 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4838 }
4839
4840 #[test]
4841 fn parallel_cone_cylinder_gives_two_exact_branches() {
4842 use crate::traits::ParametricCurve;
4843 let cone = ConicalSurface::new(
4844 Point3::new(-5.45, -36.55, -4.85),
4845 Vec3::new(0.0, 0.0, 1.0),
4846 std::f64::consts::FRAC_PI_4,
4847 )
4848 .unwrap();
4849 let cyl = CylindricalSurface::new(
4850 Point3::new(-8.0, -34.0, -5.0),
4851 Vec3::new(0.0, 0.0, 1.0),
4852 4.45,
4853 )
4854 .unwrap();
4855 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4857 let curves = intersect_analytic_analytic_bounded(
4858 AnalyticSurface::Cone(&cone),
4859 AnalyticSurface::Cylinder(&cyl),
4860 32,
4861 Some(v_hint),
4862 Some((0.0, 2.5)),
4863 )
4864 .unwrap();
4865
4866 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4867 for c in &curves {
4868 let (t0, t1) = c.curve.domain();
4869 for k in 0..=32 {
4870 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4871 let p = ParametricCurve::evaluate(&c.curve, t);
4872 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4874 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4875 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4877 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4878 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4880 }
4881 }
4882 }
4883
4884 #[test]
4885 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4886 use crate::traits::ParametricCurve;
4887 let cone = ConicalSurface::new(
4889 Point3::new(0.0, 0.0, 3.0),
4890 Vec3::new(0.0, 0.0, -1.0),
4891 2.0_f64.atan(),
4892 )
4893 .unwrap();
4894 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4896 let rod =
4897 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4898 .unwrap();
4899 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4900 .unwrap()
4901 .unwrap();
4902 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4903 let (t0, t1) = curves[0].curve.domain();
4904 let (first, last) = (
4905 ParametricCurve::evaluate(&curves[0].curve, t0),
4906 ParametricCurve::evaluate(&curves[0].curve, t1),
4907 );
4908 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4909 for k in 0..=64 {
4910 let p = ParametricCurve::evaluate(
4911 &curves[0].curve,
4912 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4913 );
4914 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4915 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4916 assert!(
4917 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4918 "({x}, {y}): off by {on_rod}, {on_cone}"
4919 );
4920 }
4921 }
4922 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4925 let rod =
4926 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4927 .unwrap();
4928 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4929 .unwrap()
4930 .unwrap();
4931 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4932 }
4933 }
4934
4935 #[test]
4938 fn coaxial_cone_cylinder_defers_to_other_paths() {
4939 let cone = ConicalSurface::new(
4940 Point3::new(0.0, 0.0, 0.0),
4941 Vec3::new(0.0, 0.0, 1.0),
4942 std::f64::consts::FRAC_PI_4,
4943 )
4944 .unwrap();
4945 let cyl =
4946 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4947 .unwrap();
4948 assert!(
4949 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4950 .unwrap()
4951 .is_none()
4952 );
4953 }
4954
4955 #[test]
4956 fn oblique_cone_cylinder_defers_to_other_paths() {
4957 let cone = ConicalSurface::new(
4958 Point3::new(0.0, 0.0, 0.0),
4959 Vec3::new(0.0, 0.0, 1.0),
4960 std::f64::consts::FRAC_PI_4,
4961 )
4962 .unwrap();
4963 let cyl =
4964 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4965 .unwrap();
4966 assert!(
4967 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4968 .unwrap()
4969 .is_none()
4970 );
4971 }
4972
4973 #[test]
4974 fn plane_torus_lobe_closes_and_stays_on_surface() {
4975 use crate::traits::ParametricCurve;
4976 let (major, minor) = (10.0, 3.0);
4977 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4978
4979 for (n, d) in [
4983 (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), ] {
4987 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4988 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4989 for c in &curves {
4990 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4991 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4992 assert!(
4993 (p0 - p1).length() < 1e-7,
4994 "lobe not closed: gap={} (n={n:?} d={d})",
4995 (p0 - p1).length()
4996 );
4997 for k in 0..=64 {
4999 let t = f64::from(k) / 64.0;
5000 let p = ParametricCurve::evaluate(&c.curve, t);
5001 assert!(
5002 torus_implicit(p, major, minor).abs() < 1e-2,
5003 "off-surface point {p:?} implicit={}",
5004 torus_implicit(p, major, minor)
5005 );
5006 }
5007 }
5008 }
5009 }
5010
5011 #[test]
5016 fn plane_torus_inner_tangent_figure_eight_is_two_loops_through_its_node() {
5017 let (major, minor) = (10.0, 3.0);
5018 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5019 let curves =
5020 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
5021 assert_eq!(curves.len(), 2, "one loop per lobe");
5022 let node = Point3::new(major - minor, 0.0, 0.0);
5023 let mut sides = Vec::new();
5024 for c in &curves {
5025 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
5026 assert!((first - node).length() < 1e-6, "starts at {first:?}");
5027 assert_eq!(first, last, "closes on the node");
5028 for p in c.points.iter().map(|p| p.point) {
5029 assert!((p.x() - node.x()).abs() < 1e-9, "off the plane at {p:?}");
5030 assert!(
5031 torus_implicit(p, major, minor).abs() < 1e-6,
5032 "off the torus at {p:?}"
5033 );
5034 }
5035 let y: f64 = c.points.iter().map(|p| p.point.y()).sum();
5036 sides.push(y > 0.0);
5037 assert!(
5038 c.points.iter().all(|p| p.point.y() * y >= -1e-9),
5039 "a lobe crosses y = 0"
5040 );
5041 }
5042 assert_ne!(sides[0], sides[1], "the lobes lie either side of y = 0");
5043 }
5044
5045 #[test]
5051 fn a_wall_just_off_the_inner_tangent_cuts_no_node() {
5052 let (major, minor) = (3.0, 1.0);
5053 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5054 for (x, loops) in [(2.0001, 1), (1.9999, 2)] {
5055 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), x).unwrap();
5056 assert_eq!(curves.len(), loops, "x = {x}");
5057 for c in &curves {
5058 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
5059 assert_eq!(first, last, "x = {x}: the loop closes");
5060 for p in c.points.iter().map(|p| p.point) {
5061 assert!((p.x() - x).abs() < 1e-9, "x = {x}: off the wall at {p:?}");
5062 assert!(
5063 torus_implicit(p, major, minor).abs() < 1e-6,
5064 "x = {x}: off the torus at {p:?}"
5065 );
5066 }
5067 }
5068 }
5069 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), 2.0001).unwrap();
5071 let low = curves[0]
5072 .points
5073 .iter()
5074 .map(|p| p.point)
5075 .filter(|p| p.y().abs() < 1e-6)
5076 .map(Point3::z)
5077 .collect::<Vec<_>>();
5078 let gap = (2.0001_f64 - 2.0)
5079 .mul_add(2.0, -(2.0001_f64 - 2.0).powi(2))
5080 .sqrt();
5081 assert!(
5082 low.iter().any(|z| (z - gap).abs() < 1e-6)
5083 && low.iter().any(|z| (z + gap).abs() < 1e-6),
5084 "turns at {low:?}, not at z = ±{gap}"
5085 );
5086 }
5087
5088 #[test]
5092 fn a_tilted_plane_tangent_inside_the_tube_cuts_two_loops_through_its_node() {
5093 let (major, minor) = (3.0, 1.0);
5094 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5095 for v0 in [1.7_f64, 2.0, 2.5, 4.0] {
5096 let normal = Vec3::new(v0.cos(), 0.0, v0.sin());
5097 let node = torus.evaluate(0.0, v0);
5098 let d = normal.dot(Vec3::new(node.x(), node.y(), node.z()));
5099 let curves = intersect_plane_torus(&torus, normal, d).unwrap();
5100 assert_eq!(curves.len(), 2, "v0 {v0}: one loop per lobe");
5101 for c in &curves {
5102 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
5103 assert!(
5104 (first - node).length() < 1e-6,
5105 "v0 {v0}: starts at {first:?}, not {node:?}"
5106 );
5107 assert_eq!(first, last, "v0 {v0}: closes on the node");
5108 for p in c.points.iter().map(|p| p.point) {
5109 let off = normal.dot(Vec3::new(p.x(), p.y(), p.z())) - d;
5110 assert!(off.abs() < 1e-9, "v0 {v0}: off the plane at {p:?}");
5111 assert!(
5112 torus_implicit(p, major, minor).abs() < 1e-6,
5113 "v0 {v0}: off the torus at {p:?}"
5114 );
5115 }
5116 }
5117 }
5118 }
5119
5120 #[test]
5125 fn plane_torus_wall_sections_close_into_their_loops() {
5126 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
5127 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5128 for k in 1..200 {
5129 let (d, want) = match k.cmp(&100) {
5130 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
5132 std::cmp::Ordering::Greater => (
5134 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
5135 1,
5136 ),
5137 std::cmp::Ordering::Equal => continue,
5138 };
5139 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
5140 let closed = loops
5141 .iter()
5142 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
5143 .count();
5144 assert_eq!(
5145 (loops.len(), closed),
5146 (want, want),
5147 "R {major} r {minor}, wall at {d}"
5148 );
5149 }
5150 }
5151 }
5152
5153 #[test]
5157 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
5158 let (major, minor) = (4.0, 1.5);
5159 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5160 for tilt in [0.03_f64, 0.08, 0.2] {
5161 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
5162 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
5163 assert_eq!(curves.len(), 2, "tilt {tilt}");
5164 for c in &curves {
5165 let (t0, t1) = c.curve.domain();
5166 let off = (0..=400)
5167 .map(|k| {
5168 let p = c
5169 .curve
5170 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
5171 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
5172 })
5173 .fold(0.0_f64, |m, e| m.max(e.abs()));
5174 assert!(
5175 off < 1e-6,
5176 "tilt {tilt}: fitted section {off} off the torus"
5177 );
5178 }
5179 }
5180 }
5181
5182 #[test]
5183 fn line_torus_box_edge_crossing_is_exact() {
5184 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
5187 let ts = intersect_line_torus(
5188 &torus,
5189 Point3::new(6.0, -4.0, -5.0),
5190 Vec3::new(0.0, 0.0, 1.0),
5191 );
5192 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
5194 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
5195 let rho = 6.0_f64.hypot(4.0);
5196 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
5197 assert!(
5198 (zs[0] - (-z_exp)).abs() < 1e-9,
5199 "z0={} exp={}",
5200 zs[0],
5201 -z_exp
5202 );
5203 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
5204 for &t in &ts {
5206 let p = Point3::new(6.0, -4.0, -5.0 + t);
5207 let rho = p.x().hypot(p.y());
5208 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
5209 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
5210 }
5211 }
5212
5213 #[test]
5218 fn line_spindle_torus_roots_lie_on_their_tubes() {
5219 let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
5220 let origin = Point3::new(42.36, 18.31, 3.4);
5221 let dir = Vec3::new(
5222 0.573_576_436_351_046,
5223 0.740_535_693_464_567_5,
5224 0.350_889_803_483_932_2,
5225 );
5226 let ts = intersect_line_torus(&torus, origin, dir);
5227 let tube = |t: f64, across: f64| {
5228 let q = origin + dir * t;
5229 ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
5230 };
5231 for &t in &ts {
5232 assert!(
5233 tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
5234 "root {t} off both tubes in {ts:?}"
5235 );
5236 }
5237 for want in [0.124, 0.398] {
5238 assert!(
5239 ts.iter().any(|&t| (t - want).abs() < 1e-3),
5240 "no root near {want} in {ts:?}"
5241 );
5242 }
5243 }
5244
5245 #[test]
5246 fn line_torus_miss_and_tangent() {
5247 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
5248 let miss = intersect_line_torus(
5250 &torus,
5251 Point3::new(20.0, 0.0, 0.0),
5252 Vec3::new(0.0, 0.0, 1.0),
5253 );
5254 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
5255 let axis =
5257 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
5258 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
5259 }
5260
5261 #[test]
5262 fn dispatch_via_analytic_surface() {
5263 let cyl =
5264 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5265 .unwrap();
5266 let curves = intersect_plane_analytic(
5267 AnalyticSurface::Cylinder(&cyl),
5268 Vec3::new(0.0, 0.0, 1.0),
5269 0.0,
5270 )
5271 .unwrap();
5272 assert!(!curves.is_empty());
5273 }
5274
5275 #[test]
5276 fn perpendicular_cylinders_intersect() {
5277 let cyl_z =
5278 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5279 .unwrap();
5280 let cyl_x =
5281 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
5282 .unwrap();
5283
5284 let curves = intersect_analytic_analytic(
5285 AnalyticSurface::Cylinder(&cyl_z),
5286 AnalyticSurface::Cylinder(&cyl_x),
5287 16,
5288 )
5289 .unwrap();
5290
5291 assert!(
5292 !curves.is_empty(),
5293 "perpendicular cylinders should intersect"
5294 );
5295
5296 for c in &curves {
5297 assert!(
5298 c.points.len() >= 2,
5299 "intersection curve should have >= 2 points, got {}",
5300 c.points.len()
5301 );
5302 }
5303 }
5304
5305 #[test]
5308 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
5309 let cyl_z =
5310 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5311 .unwrap();
5312 let cyl_x =
5313 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
5314 .unwrap();
5315 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5316 .unwrap()
5317 .unwrap();
5318 assert_eq!(curves.len(), 1);
5319 let curve = &curves[0].curve;
5320 let (t0, t1) = curve.domain();
5321 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
5322 let off = |p: Point3| {
5323 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
5324 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
5325 on_z.max(on_x)
5326 };
5327 let worst = (0..=400)
5328 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
5329 .fold(0.0, f64::max);
5330 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
5331 }
5332
5333 #[test]
5337 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
5338 let cyl_z =
5339 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5340 .unwrap();
5341 let cyl_x =
5342 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
5343 .unwrap();
5344 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5345 .unwrap()
5346 .expect("the thin cylinder's sweep finds the loop");
5347 assert_eq!(curves.len(), 1);
5348 }
5349
5350 #[test]
5351 fn sphere_cylinder_intersect() {
5352 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5353 let cyl =
5354 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5355 .unwrap();
5356
5357 let curves = intersect_analytic_analytic(
5358 AnalyticSurface::Sphere(&sphere),
5359 AnalyticSurface::Cylinder(&cyl),
5360 16,
5361 )
5362 .unwrap();
5363
5364 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
5368 }
5369
5370 #[test]
5371 fn exact_sphere_cylinder_coaxial_two_circles() {
5372 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5375 let cyl =
5376 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5377 .unwrap();
5378 let circles = exact_sphere_cylinder(&sphere, &cyl)
5379 .unwrap()
5380 .expect("coaxial case returns Some");
5381 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
5382 let mut zs: Vec<f64> = circles
5383 .iter()
5384 .filter_map(|c| match c {
5385 ExactIntersectionCurve::Circle(circle) => {
5386 assert!(
5387 (circle.radius() - 3.0).abs() < 1e-9,
5388 "rim radius == cyl radius"
5389 );
5390 Some(circle.center().z())
5391 }
5392 _ => None,
5393 })
5394 .collect();
5395 assert_eq!(zs.len(), 2, "both sections must be exact circles");
5396 zs.sort_by(f64::total_cmp);
5397 let z = 27.0_f64.sqrt();
5398 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
5399 }
5400
5401 #[test]
5402 fn exact_sphere_cylinder_non_coaxial_defers() {
5403 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5405 let cyl =
5406 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5407 .unwrap();
5408 assert!(
5409 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
5410 "non-coaxial sphere/cylinder defers to the marcher"
5411 );
5412 }
5413
5414 #[test]
5415 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
5416 let cone = ConicalSurface::new(
5418 Point3::new(0.0, 0.0, 3.0),
5419 Vec3::new(0.0, 0.0, -1.0),
5420 2.0_f64.atan(),
5421 )
5422 .unwrap();
5423 for (height, radius, count) in [
5424 (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), ] {
5430 let centre = Point3::new(0.0, 0.0, height);
5431 let ball = SphericalSurface::new(centre, radius).unwrap();
5432 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
5433 let circles = circles_of(&curves);
5434 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
5435 for circle in circles {
5436 for k in 0..16 {
5437 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5438 let on_ball = (p - centre).length() - radius;
5439 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
5440 assert!(
5441 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
5442 "ball at {height}: off by {on_ball}, {on_cone}"
5443 );
5444 }
5445 }
5446 }
5447 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
5448 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
5449 let wide =
5452 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
5453 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
5454 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
5455 let circles = circles_of(&curves);
5456 assert_eq!(circles.len(), 1, "the touch");
5457 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
5458 assert!(
5459 (circles[0].radius() - touch).abs() < 1e-3,
5460 "{}",
5461 circles[0].radius()
5462 );
5463 }
5464
5465 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5467 curves
5468 .iter()
5469 .filter_map(|c| match c {
5470 ExactIntersectionCurve::Circle(circle) => Some(circle),
5471 _ => None,
5472 })
5473 .collect()
5474 }
5475
5476 fn worst_off(
5479 circles: &[&Circle3D],
5480 torus: &ToroidalSurface,
5481 other: impl Fn(Point3) -> f64,
5482 ) -> f64 {
5483 let mut worst = 0.0_f64;
5484 for circle in circles {
5485 for k in 0..16 {
5486 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5487 let q = p - torus.center();
5488 let along = q.dot(torus.z_axis());
5489 let rho = (q - torus.z_axis() * along).length();
5490 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5491 worst = worst.max(off).max(other(p).abs());
5492 }
5493 }
5494 worst
5495 }
5496
5497 #[test]
5498 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5499 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5500 for height in [0.0, 1.0] {
5501 let centre = Point3::new(0.0, 0.0, height);
5502 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5503 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5504 let circles = circles_of(&curves);
5505 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5506 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5507 assert!(worst < 1e-9, "height {height}: {worst}");
5508 }
5509 }
5510
5511 #[test]
5512 fn exact_sphere_torus_misses_touches_and_defers() {
5513 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5514 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5515 assert!(
5516 exact_sphere_torus(&ball(0.0, 1.0), &torus)
5517 .unwrap()
5518 .unwrap()
5519 .is_empty(),
5520 "a small ball in the hole misses"
5521 );
5522 assert!(
5523 exact_sphere_torus(&ball(0.0, 2.5), &torus)
5524 .unwrap()
5525 .is_none(),
5526 "a ball touching the inner equator defers"
5527 );
5528 assert!(
5529 exact_sphere_torus(&ball(1.0, 3.0), &torus)
5530 .unwrap()
5531 .is_none(),
5532 "a ball off the axis defers"
5533 );
5534 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5535 Point3::new(0.0, 0.0, 0.0),
5536 1.0,
5537 2.0,
5538 Vec3::new(0.0, 0.0, 1.0),
5539 Vec3::new(1.0, 0.0, 0.0),
5540 )
5541 .unwrap();
5542 assert!(
5543 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5544 .unwrap()
5545 .is_none()
5546 );
5547 }
5548
5549 #[test]
5550 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5551 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5552 let z = Vec3::new(0.0, 0.0, 1.0);
5553 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5554 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5555 let circles = circles_of(&curves);
5556 assert_eq!((curves.len(), circles.len()), (2, 2));
5557 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5558 assert!(worst < 1e-9, "{worst}");
5559 assert!(
5560 exact_cylinder_torus(&rod(2.0), &torus)
5561 .unwrap()
5562 .unwrap()
5563 .is_empty(),
5564 "a rod clear in the hole misses"
5565 );
5566 for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5567 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5568 let circles = circles_of(&curves);
5569 assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5570 assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5571 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5572 assert!(worst < 1e-9, "{label}: {worst}");
5573 }
5574 assert!(
5575 exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5576 .unwrap()
5577 .unwrap()
5578 .is_empty(),
5579 "a wall clear of the tube by more than the tolerance misses it"
5580 );
5581 assert_eq!(
5582 exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5583 .unwrap()
5584 .unwrap()
5585 .len(),
5586 2,
5587 "a wall into the tube by more than the tolerance crosses it twice"
5588 );
5589 for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5593 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5594 assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5595 }
5596 let tilted =
5597 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5598 .unwrap();
5599 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5600 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5601 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5602 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5603 Point3::new(0.0, 0.0, 0.0),
5604 1.0,
5605 2.0,
5606 z,
5607 Vec3::new(1.0, 0.0, 0.0),
5608 )
5609 .unwrap();
5610 assert!(
5611 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5612 "a spindle torus's inner lemon also meets the rod"
5613 );
5614 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5617 Point3::new(0.0, 0.0, 4.7),
5618 0.1,
5619 2.45,
5620 z,
5621 Vec3::new(1.0, 0.0, 0.0),
5622 )
5623 .unwrap();
5624 let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5625 let circles = circles_of(&curves);
5626 assert_eq!(circles.len(), 1);
5627 assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5628 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5629 assert!(worst < 1e-9, "{worst}");
5630 let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5631 let circles = circles_of(&curves);
5632 assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5633 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5634 assert!(worst < 1e-9, "{worst}");
5635 }
5636
5637 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5640 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5641 let cyl =
5642 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5643 .unwrap();
5644 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5645 .unwrap()
5646 .unwrap();
5647 let mut worst: f64 = 0.0;
5648 for c in &curves {
5649 for ip in &c.points {
5650 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5651 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5652 worst = worst
5653 .max((on_sphere - ip.point).length())
5654 .max((on_cylinder - ip.point).length());
5655 }
5656 let (t0, t1) = c.curve.domain();
5657 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5658 for k in 0..=400 {
5659 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5660 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5661 let on_cylinder = ((p.x() - cylinder_origin.x())
5662 .hypot(p.y() - cylinder_origin.y())
5663 - cylinder_radius)
5664 .abs();
5665 worst = worst.max(on_sphere).max(on_cylinder);
5666 }
5667 }
5668 (curves.len(), worst)
5669 }
5670
5671 #[test]
5674 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5675 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5676 assert_eq!(count, 2);
5677 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5678 }
5679
5680 #[test]
5682 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5683 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5684 assert_eq!(count, 1);
5685 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5686 }
5687
5688 #[test]
5689 fn disjoint_cylinders_no_intersection() {
5690 let cyl_a =
5691 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5692 .unwrap();
5693 let cyl_b =
5694 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5695 .unwrap();
5696
5697 let curves = intersect_analytic_analytic(
5698 AnalyticSurface::Cylinder(&cyl_a),
5699 AnalyticSurface::Cylinder(&cyl_b),
5700 16,
5701 )
5702 .unwrap();
5703
5704 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5705 }
5706
5707 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5711 use crate::traits::ParametricCurve;
5712 match curve {
5713 ExactIntersectionCurve::Circle(c) => (0..=64)
5714 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5715 .collect(),
5716 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5717 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5718 .collect(),
5719 ExactIntersectionCurve::Points(pts) => pts.clone(),
5720 }
5721 }
5722
5723 fn assert_on_plane_and_cone(
5726 curves: &[ExactIntersectionCurve],
5727 cone: &ConicalSurface,
5728 n: Vec3,
5729 d: f64,
5730 z_bound: (f64, f64),
5731 ) {
5732 assert!(!curves.is_empty(), "expected at least one section curve");
5733 let mut total = 0;
5734 for curve in curves {
5735 for p in collect_points(curve) {
5736 total += 1;
5737 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5738 assert!(
5739 plane_err < 1e-9,
5740 "point off plane by {plane_err:.2e}: {p:?}"
5741 );
5742 let (u, v) = cone.project_point(p);
5743 let q = cone.evaluate(u, v);
5744 let cone_err =
5745 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5746 .sqrt();
5747 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5748 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5749 assert!(
5750 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5751 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5752 p.z()
5753 );
5754 }
5755 }
5756 assert!(total >= 8, "too few section points ({total})");
5757 }
5758
5759 #[test]
5760 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5761 let cone = ConicalSurface::new(
5765 Point3::new(0.0, 0.0, 0.0),
5766 Vec3::new(0.0, 0.0, 1.0),
5767 std::f64::consts::FRAC_PI_4,
5768 )
5769 .unwrap();
5770 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5771 let d = n.z() * 5.0;
5773 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5774 assert!(
5775 curves
5776 .iter()
5777 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5778 "oblique steep plane × cone must yield an exact Ellipse"
5779 );
5780 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5782 }
5783
5784 #[test]
5785 fn oblique_plane_cone_wrong_nappe_is_empty() {
5786 let cone = ConicalSurface::new(
5790 Point3::new(0.0, 0.0, 0.0),
5791 Vec3::new(0.0, 0.0, 1.0),
5792 std::f64::consts::FRAC_PI_4,
5793 )
5794 .unwrap();
5795 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5796 let d = n.z() * -5.0;
5797 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5798 assert!(
5799 curves.is_empty(),
5800 "plane on the phantom-nappe side must yield no real curve, got {}",
5801 curves.len()
5802 );
5803 }
5804
5805 #[test]
5806 fn oblique_plane_cone_parabola_on_both_single_branch() {
5807 let cone = ConicalSurface::new(
5810 Point3::new(0.0, 0.0, 0.0),
5811 Vec3::new(0.0, 0.0, 1.0),
5812 std::f64::consts::FRAC_PI_4,
5813 )
5814 .unwrap();
5815 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5816 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5818 assert_eq!(
5819 curves.len(),
5820 1,
5821 "a parabola is a single branch, got {}",
5822 curves.len()
5823 );
5824 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5826 }
5827
5828 #[test]
5829 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5830 let cone = ConicalSurface::new(
5838 Point3::new(-59.0, -59.0, 15.85),
5839 Vec3::new(0.0, 0.0, -1.0),
5840 std::f64::consts::FRAC_PI_4,
5841 )
5842 .unwrap();
5843 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5844 let d = -58.360_56;
5845 let cos_theta = n.dot(cone.axis()).abs();
5846 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5847 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5848 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5851 for c in &curves {
5853 assert!(
5854 matches!(c, ExactIntersectionCurve::Points(_)),
5855 "hyperbola must be sampled Points, not a closed conic"
5856 );
5857 }
5858 }
5859
5860 #[test]
5864 fn sphere_sphere_meets_in_a_circle_on_both() {
5865 let ball = |x: f64, y: f64, z: f64, r: f64| {
5866 SphericalSurface::new(Point3::new(x, y, z), r).unwrap()
5867 };
5868 let (a, b) = (ball(0.0, 0.0, 0.0, 2.0), ball(1.6, 0.3, 0.2, 1.5));
5869 let curves = exact_sphere_sphere(&a, &b).unwrap().unwrap();
5870 assert_eq!(curves.len(), 1);
5871 let circle = match &curves[0] {
5872 ExactIntersectionCurve::Circle(circle) => circle,
5873 other => unreachable!("expected a circle, got {other:?}"),
5874 };
5875 for k in 0..16 {
5876 let p = circle.evaluate(std::f64::consts::TAU * f64::from(k) / 16.0);
5877 assert!(((p - a.center()).length() - 2.0).abs() < 1e-12);
5878 assert!(((p - b.center()).length() - 1.5).abs() < 1e-12);
5879 }
5880 assert!(
5881 exact_sphere_sphere(&a, &ball(4.0, 0.0, 0.0, 1.5))
5882 .unwrap()
5883 .unwrap()
5884 .is_empty()
5885 );
5886 assert!(
5887 exact_sphere_sphere(&a, &ball(0.2, 0.0, 0.0, 1.0))
5888 .unwrap()
5889 .unwrap()
5890 .is_empty()
5891 );
5892 assert!(
5893 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 1.0))
5894 .unwrap()
5895 .unwrap()
5896 .is_empty()
5897 );
5898 assert!(
5899 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 2.0))
5900 .unwrap()
5901 .is_none()
5902 );
5903
5904 let radius = |p: &SphericalSurface, q: &SphericalSurface| -> f64 {
5907 match exact_sphere_sphere(p, q).unwrap().unwrap().as_slice() {
5908 [ExactIntersectionCurve::Circle(c)] => c.radius(),
5909 other => unreachable!("expected one circle, got {other:?}"),
5910 }
5911 };
5912 let tiny = ball(2.0, 0.0, 0.0, 1e-5);
5913 assert!((radius(&a, &tiny) - 1e-5).abs() < 1e-9);
5914 assert!((radius(&tiny, &a) - 1e-5).abs() < 1e-9);
5915 let near = radius(&ball(0.0, 0.0, 0.0, 2.0), &ball(3.5, 0.0, 0.0, 1.500_001));
5916 let far = radius(
5917 &ball(1e10, 0.0, 0.0, 2.0),
5918 &ball(1e10 + 3.5, 0.0, 0.0, 1.500_001),
5919 );
5920 assert!(near > 1e-3);
5921 assert!((far - near).abs() < 1e-9 * near.max(1.0));
5922 }
5923
5924 #[allow(clippy::panic)]
5927 fn lines_of(sections: &[PlaneSection]) -> Vec<(Line3D, bool)> {
5928 sections
5929 .iter()
5930 .map(|s| match s {
5931 PlaneSection::Line { line, ray } => (line.clone(), *ray),
5932 other => panic!("expected a line, got {other:?}"),
5933 })
5934 .collect()
5935 }
5936
5937 #[test]
5942 fn a_plane_along_a_cylinder_cuts_its_rulings() {
5943 let cyl =
5944 CylindricalSurface::new(Point3::new(0.0, 0.0, -2.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5945 .unwrap();
5946 let y = Vec3::new(0.0, 1.0, 0.0);
5947 let surface = AnalyticSurface::Cylinder(&cyl);
5948 let on_both = |p: Point3, d: f64| {
5949 assert!((p.y() - d).abs() < 1e-12, "{p:?} off the plane y = {d}");
5950 assert!(
5951 (p.x().hypot(p.y()) - 1.0).abs() < 1e-12,
5952 "{p:?} off the cylinder"
5953 );
5954 };
5955 for (d, count) in [(0.0, 2), (1e-6_f64.cos(), 2), (1.0, 1), (1.5, 0)] {
5956 assert!(exact_plane_analytic(surface, y, d).unwrap().is_empty());
5957 let lines = lines_of(&exact_plane_section(surface, y, d).unwrap());
5958 assert_eq!(lines.len(), count, "plane y = {d}");
5959 let mut xs = Vec::new();
5960 for (line, ray) in lines {
5961 assert!(!ray);
5962 assert!(line.direction().cross(cyl.axis()).length() < 1e-12);
5963 for t in [-3.0, 0.0, 3.0] {
5964 on_both(line.evaluate(t), d);
5965 }
5966 xs.push(line.evaluate(0.0).x());
5967 }
5968 if count == 2 {
5969 let apart = (xs[0] - xs[1]).abs();
5970 let want = 2.0 * (1.0 - d * d).sqrt();
5971 assert!(
5972 (apart - want).abs() < 1e-9 * want.max(1e-6),
5973 "{apart} apart"
5974 );
5975 }
5976 }
5977 }
5978
5979 #[test]
5984 #[allow(clippy::panic)]
5985 fn a_plane_through_a_cone_apex_cuts_its_rulings() {
5986 use std::f64::consts::FRAC_PI_4;
5987
5988 let cone = ConicalSurface::new(
5989 Point3::new(1.0, 2.0, 3.0),
5990 Vec3::new(0.0, 0.0, 1.0),
5991 FRAC_PI_4,
5992 )
5993 .unwrap();
5994 let surface = AnalyticSurface::Cone(&cone);
5995 let apex = cone.apex();
5996 let through = |n: Vec3| {
5997 let n = n.normalize().unwrap();
5998 let d = n.dot(apex - Point3::new(0.0, 0.0, 0.0));
5999 assert!(exact_plane_analytic(surface, n, d).unwrap().is_empty());
6000 (n, d, exact_plane_section(surface, n, d).unwrap())
6001 };
6002 for (n, count) in [
6003 (Vec3::new(1.0, 0.0, 0.0), 2),
6004 (Vec3::new(1.0, 0.0, -1.0), 1),
6005 ] {
6006 let (n, d, sections) = through(n);
6007 let rays = lines_of(§ions);
6008 assert_eq!(rays.len(), count, "plane normal {n:?}");
6009 for (line, ray) in rays {
6010 assert!(ray);
6011 assert!((line.evaluate(0.0) - apex).length() < 1e-12);
6012 let p = line.evaluate(2.0);
6013 assert!((n.dot(p - Point3::new(0.0, 0.0, 0.0)) - d).abs() < 1e-12);
6014 let rel = p - apex;
6015 assert!(rel.z() > 0.0, "{p:?} on the phantom nappe");
6016 assert!(
6017 (rel.x().hypot(rel.y()) - rel.z()).abs() < 1e-12,
6018 "{p:?} off the cone"
6019 );
6020 }
6021 }
6022 for n in [Vec3::new(0.1, 0.0, 1.0), Vec3::new(0.0, 0.0, 1.0)] {
6023 let (_, _, sections) = through(n);
6024 assert!(
6025 matches!(sections[..], [PlaneSection::Point(p)] if (p - apex).length() < 1e-12),
6026 "{sections:?}"
6027 );
6028 }
6029
6030 let upright = ConicalSurface::new(
6031 Point3::new(0.0, 0.0, 0.0),
6032 Vec3::new(0.0, 0.0, 1.0),
6033 FRAC_PI_4,
6034 )
6035 .unwrap();
6036 let n = Vec3::new((0.5 - 1e-8_f64).sqrt(), 0.0, (0.5 + 1e-8_f64).sqrt());
6037 let sections = exact_plane_section(AnalyticSurface::Cone(&upright), n, 5e-8).unwrap();
6038 let [PlaneSection::Ellipse(ellipse)] = §ions[..] else {
6039 panic!("{sections:?}");
6040 };
6041 assert!(
6042 ellipse.semi_major() > 1.0,
6043 "semi-major {}",
6044 ellipse.semi_major()
6045 );
6046 }
6047
6048 #[test]
6051 #[allow(clippy::panic)]
6052 fn a_plane_touching_a_sphere_meets_it_at_a_point() {
6053 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 1.0), 1.0).unwrap();
6054 let surface = AnalyticSurface::Sphere(&sphere);
6055 let z = Vec3::new(0.0, 0.0, 1.0);
6056 assert!(exact_plane_analytic(surface, z, 0.0).unwrap().is_empty());
6057 let sections = exact_plane_section(surface, z, 0.0).unwrap();
6058 assert!(
6059 matches!(sections[..], [PlaneSection::Point(p)] if p.z().abs() < 1e-15 && p.x() == 0.0),
6060 "{sections:?}"
6061 );
6062 assert!(exact_plane_section(surface, z, -1e-9).unwrap().is_empty());
6063 for height in [5e-15, 1e-12, 1e-8] {
6064 let sections = exact_plane_section(surface, z, height).unwrap();
6065 let [PlaneSection::Circle(circle)] = §ions[..] else {
6066 panic!("{sections:?}");
6067 };
6068 let want = (height * (2.0 - height)).sqrt();
6069 assert!(
6070 (circle.radius() - want).abs() < 1e-3 * want,
6071 "circle at z = {height}: radius {}, want {want}",
6072 circle.radius()
6073 );
6074 assert!((circle.center().z() - height).abs() < 1e-15);
6075 }
6076 }
6077
6078 fn farthest(curves: &[IntersectionCurve], off: impl Fn(Point3) -> f64) -> f64 {
6080 let mut far = 0.0_f64;
6081 for ic in curves {
6082 let (t0, t1) = ic.curve.domain();
6083 for i in 0..=2000 {
6084 let t = t0 + (t1 - t0) * f64::from(i) / 2000.0;
6085 far = far.max(off(ic.curve.evaluate(t)));
6086 }
6087 }
6088 far
6089 }
6090
6091 #[test]
6094 fn a_ball_in_its_bore_meets_it_on_an_exact_circle() {
6095 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
6096 let bore =
6097 CylindricalSurface::new(Point3::new(0.0, 0.0, -2.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
6098 .unwrap();
6099 let curves = intersect_analytic_analytic_bounded(
6100 AnalyticSurface::Sphere(&ball),
6101 AnalyticSurface::Cylinder(&bore),
6102 64,
6103 None,
6104 Some((0.0, 4.0)),
6105 )
6106 .unwrap();
6107 assert_eq!(curves.len(), 1);
6108 let far = farthest(&curves, |p| {
6109 let on_ball = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 1.0).abs();
6110 on_ball
6111 .max((p.x().hypot(p.y()) - 1.0).abs())
6112 .max(p.z().abs())
6113 });
6114 assert!(far < 1e-14, "circle strays {far:.2e}");
6115 }
6116
6117 #[test]
6120 fn overlapping_balls_meet_on_an_exact_circle() {
6121 let a = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
6122 let b = SphericalSurface::new(Point3::new(1.0, 0.0, 0.0), 1.0).unwrap();
6123 let curves = intersect_analytic_analytic_bounded(
6124 AnalyticSurface::Sphere(&a),
6125 AnalyticSurface::Sphere(&b),
6126 64,
6127 None,
6128 None,
6129 )
6130 .unwrap();
6131 assert_eq!(curves.len(), 1);
6132 let far = farthest(&curves, |p| {
6133 let on_a = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 1.0).abs();
6134 on_a.max(((p - Point3::new(1.0, 0.0, 0.0)).length() - 1.0).abs())
6135 });
6136 assert!(far < 1e-14, "circle strays {far:.2e}");
6137 }
6138
6139 #[test]
6142 fn a_conic_loop_is_its_ellipse() {
6143 let centre = Point3::new(1.0, -2.0, 0.5);
6144 let frame = Frame3::from_normal(centre, Vec3::new(0.3, -0.4, 1.0)).unwrap();
6145 let curve = conic_loop(centre, frame.x, frame.y, 3.0, 1.5).unwrap();
6146 let (t0, t1) = curve.domain();
6147 assert!((curve.evaluate(t0) - (centre + frame.x * 3.0)).length() < 1e-15);
6148 for i in 0..=2000 {
6149 let p = curve.evaluate(t0 + (t1 - t0) * f64::from(i) / 2000.0) - centre;
6150 let (x, y) = (p.dot(frame.x) / 3.0, p.dot(frame.y) / 1.5);
6151 assert!(
6152 (x.hypot(y) - 1.0).abs() < 1e-14,
6153 "off its ellipse by {}",
6154 x.hypot(y) - 1.0
6155 );
6156 assert!(p.dot(frame.z).abs() < 1e-14);
6157 }
6158 }
6159
6160 fn parallel_rulings(
6163 (c1, r1, v1): (Point3, f64, (f64, f64)),
6164 (c2, r2, axis2, v2): (Point3, f64, Vec3, (f64, f64)),
6165 ) -> Vec<(Point3, Point3)> {
6166 rulings_within((c1, r1, v1), (c2, r2, axis2, v2), 1e-12)
6167 }
6168
6169 fn rulings_within(
6172 (c1, r1, v1): (Point3, f64, (f64, f64)),
6173 (c2, r2, axis2, v2): (Point3, f64, Vec3, (f64, f64)),
6174 tolerance: f64,
6175 ) -> Vec<(Point3, Point3)> {
6176 let a = CylindricalSurface::new(c1, Vec3::new(0.0, 0.0, 1.0), r1).unwrap();
6177 let b = CylindricalSurface::new(c2, axis2, r2).unwrap();
6178 let curves = intersect_analytic_analytic_bounded(
6179 AnalyticSurface::Cylinder(&a),
6180 AnalyticSurface::Cylinder(&b),
6181 32,
6182 Some(v1),
6183 Some(v2),
6184 )
6185 .unwrap();
6186 curves
6187 .iter()
6188 .map(|ic| {
6189 let (t0, t1) = ic.curve.domain();
6190 let (p, q) = (ic.curve.evaluate(t0), ic.curve.evaluate(t1));
6191 for i in 0..=100 {
6192 let x = ic.curve.evaluate(t0 + (t1 - t0) * f64::from(i) / 100.0);
6193 let on_a = ((x - c1) - Vec3::new(0.0, 0.0, (x - c1).z())).length() - r1;
6194 let w = x - c2;
6195 let on_b = (w - axis2 * w.dot(axis2)).length() - r2;
6196 assert!(
6197 on_a.abs() < tolerance && on_b.abs() < tolerance,
6198 "{x:?} off a cylinder by {on_a:.1e}, {on_b:.1e}"
6199 );
6200 }
6201 if p.z() <= q.z() { (p, q) } else { (q, p) }
6202 })
6203 .collect()
6204 }
6205
6206 #[test]
6210 fn overlapping_parallel_rods_meet_along_two_rulings() {
6211 let mut rulings = parallel_rulings(
6212 (Point3::new(0.0, 0.0, 0.0), 1.0, (0.0, 4.0)),
6213 (
6214 Point3::new(1.2, 0.0, 3.0),
6215 1.0,
6216 Vec3::new(0.0, 0.0, -1.0),
6217 (0.0, 2.0),
6218 ),
6219 );
6220 rulings.sort_by(|a, b| a.0.y().total_cmp(&b.0.y()));
6221 assert_eq!(rulings.len(), 2);
6222 for ((lo, hi), y) in rulings.into_iter().zip([-0.8, 0.8]) {
6223 assert!((lo - Point3::new(0.6, y, 1.0)).length() < 1e-12, "{lo:?}");
6224 assert!((hi - Point3::new(0.6, y, 3.0)).length() < 1e-12, "{hi:?}");
6225 }
6226 }
6227
6228 #[test]
6231 fn touching_parallel_rods_meet_along_one_ruling() {
6232 let up = Vec3::new(0.0, 0.0, 1.0);
6233 let beside = parallel_rulings(
6234 (Point3::new(0.0, 0.0, 0.0), 1.0, (-2.0, 2.0)),
6235 (Point3::new(2.0, 0.0, -2.0), 1.0, up, (0.0, 4.0)),
6236 );
6237 assert_eq!(beside.len(), 1);
6238 assert!((beside[0].0 - Point3::new(1.0, 0.0, -2.0)).length() < 1e-12);
6239 assert!((beside[0].1 - Point3::new(1.0, 0.0, 2.0)).length() < 1e-12);
6240 let inside = parallel_rulings(
6241 (Point3::new(0.0, 0.0, 0.0), 2.0, (0.0, 1.0)),
6242 (Point3::new(-1.0, 0.0, 0.0), 1.0, up, (0.0, 1.0)),
6243 );
6244 assert_eq!(inside.len(), 1);
6245 assert!((inside[0].0 - Point3::new(-2.0, 0.0, 0.0)).length() < 1e-12);
6246 let apart = parallel_rulings(
6247 (Point3::new(0.0, 0.0, 0.0), 1.0, (0.0, 1.0)),
6248 (Point3::new(2.5, 0.0, 0.0), 1.0, up, (0.0, 1.0)),
6249 );
6250 assert!(apart.is_empty());
6251 }
6252
6253 #[test]
6257 fn rods_leaning_apart_keep_rulings_only_over_a_short_span() {
6258 let up = CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
6259 .unwrap();
6260 let lean = Vec3::new(1e-5_f64.sin(), 0.0, 1e-5_f64.cos());
6261 let leaning = CylindricalSurface::new(Point3::new(1.2, 0.0, 0.0), lean, 1.0).unwrap();
6262 let span = |h: f64| parallel_cylinders(&up, &leaning, Some((0.0, h)), Some((0.0, h)));
6263 assert!(span(100.0).unwrap().is_none());
6264 assert_eq!(span(0.001).unwrap().map(|r| r.len()), Some(2));
6265 }
6266
6267 #[test]
6271 fn parallel_rods_keep_short_and_close_rulings() {
6272 let up = Vec3::new(0.0, 0.0, 1.0);
6273 let huge = rulings_within(
6274 (Point3::new(0.0, 0.0, 0.0), 1e6, (0.0, 0.001)),
6275 (Point3::new(1.2e6, 0.0, 0.0), 1e6, up, (0.0, 0.001)),
6276 1e-9,
6277 );
6278 assert_eq!(huge.len(), 2);
6279 assert!(
6280 huge.iter()
6281 .all(|(lo, hi)| ((*hi - *lo).length() - 0.001).abs() < 1e-12)
6282 );
6283 let mut close = rulings_within(
6284 (Point3::new(0.0, 0.0, 0.0), 100.0, (0.0, 1.0)),
6285 (Point3::new(199.999_999_7, 0.0, 0.0), 100.0, up, (0.0, 1.0)),
6286 1e-9,
6287 );
6288 assert_eq!(close.len(), 2);
6289 close.sort_by(|a, b| a.0.y().total_cmp(&b.0.y()));
6290 let apart = close[1].0.y() - close[0].0.y();
6291 assert!((apart - 0.010_954).abs() < 1e-5, "{apart}");
6292 }
6293}