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 }
2110 algebraic_cylinder_cylinder(c1, c2)
2112 }
2113 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
2115 algebraic_sphere_cylinder(s, c, true)
2116 }
2117 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
2118 algebraic_sphere_cylinder(s, c, false)
2119 }
2120 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
2121 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
2122 Ok(ruling_cone_sphere(cone, sphere, true))
2123 }
2124 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
2125 Ok(ruling_cone_sphere(cone, sphere, false))
2126 }
2127 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
2128 Ok(parallel_axis_torus_cylinder(t, c, true)
2129 .or_else(|| ruling_torus_cylinder(t, c, true)))
2130 }
2131 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
2132 Ok(parallel_axis_torus_cylinder(t, c, false)
2133 .or_else(|| ruling_torus_cylinder(t, c, false)))
2134 }
2135 _ => Ok(None),
2136 }
2137}
2138
2139fn parallel_axis_torus_cylinder(
2146 torus: &ToroidalSurface,
2147 cyl: &CylindricalSurface,
2148 torus_first: bool,
2149) -> Option<Vec<IntersectionCurve>> {
2150 let axis = torus.z_axis();
2151 let along = cyl.axis().dot(axis);
2152 if along.abs() < 1.0 - 1e-10 {
2153 return None;
2154 }
2155 let offset = cyl.origin() - torus.center();
2156 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
2157 return None;
2158 }
2159 let (major, minor) = (torus.major_radius(), torus.minor_radius());
2160 let roots = |u: f64| {
2161 let q = cyl.evaluate(u, 0.0) - torus.center();
2162 let height = q.dot(axis);
2163 let rho = (q - axis * height).length();
2164 let reach = minor * minor - (rho - major) * (rho - major);
2165 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
2166 };
2167 let samples = ruling_samples(cyl, &roots);
2168 let loops = if samples.iter().all(Option::is_some) {
2169 closed_ruling_loops(cyl, &roots, &samples)
2170 } else {
2171 partial_ruling_loops(cyl, &roots, &samples)
2172 };
2173 if loops.is_empty() {
2174 return None;
2175 }
2176 Some(fit_ruling_loops(&loops, |p| {
2177 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2178 }))
2179}
2180
2181fn meridian_crossings(
2187 first: (f64, f64, f64),
2188 second: (f64, f64, f64),
2189 scale: f64,
2190) -> Option<Vec<(f64, f64)>> {
2191 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
2192 let (dx, dz) = (x2 - x1, z2 - z1);
2193 let dist = dx.hypot(dz);
2194 let slack = 1e-9 * scale;
2195 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
2196 return None;
2197 }
2198 if dist > r1 + r2 || dist < (r1 - r2).abs() {
2199 return Some(Vec::new());
2200 }
2201 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
2202 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
2203 let (ux, uz) = (dx / dist, dz / dist);
2204 let mut crossings = Vec::with_capacity(2);
2205 for side in [1.0, -1.0] {
2206 let rho = x1 + along * ux - side * across * uz;
2207 if rho <= slack {
2208 return None;
2209 }
2210 crossings.push((rho, z1 + along * uz + side * across * ux));
2211 }
2212 Some(crossings)
2213}
2214
2215fn circles_about_axis(
2217 base: Point3,
2218 axis: Vec3,
2219 crossings: &[(f64, f64)],
2220) -> Result<Vec<ExactIntersectionCurve>, MathError> {
2221 crossings
2222 .iter()
2223 .map(|&(rho, z)| {
2224 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
2225 })
2226 .collect()
2227}
2228
2229pub fn exact_torus_torus(
2240 first: &ToroidalSurface,
2241 second: &ToroidalSurface,
2242) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2243 let axis = first.z_axis();
2244 let scale = first.major_radius() + second.major_radius();
2245 let offset = second.center() - first.center();
2246 if first.minor_radius() >= first.major_radius()
2248 || second.minor_radius() >= second.major_radius()
2249 || axis.cross(second.z_axis()).length() > 1e-9
2250 || offset.cross(axis).length() > 1e-9 * scale
2251 {
2252 return Ok(None);
2253 }
2254 let Some(crossings) = meridian_crossings(
2255 (first.major_radius(), 0.0, first.minor_radius()),
2256 (
2257 second.major_radius(),
2258 offset.dot(axis),
2259 second.minor_radius(),
2260 ),
2261 scale,
2262 ) else {
2263 return Ok(None);
2264 };
2265 circles_about_axis(first.center(), axis, &crossings).map(Some)
2266}
2267
2268pub fn exact_cylinder_torus(
2282 cylinder: &CylindricalSurface,
2283 torus: &ToroidalSurface,
2284) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2285 let axis = torus.z_axis();
2286 let scale = torus.major_radius() + cylinder.radius();
2287 let offset = cylinder.origin() - torus.center();
2288 let linear = crate::tolerance::Tolerance::default().linear;
2289 if axis.cross(cylinder.axis()).length() > 1e-9
2290 || offset.cross(axis).length() > 1e-9 * scale
2291 || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2292 {
2293 return Ok(None);
2294 }
2295 let gap = cylinder.radius() - torus.major_radius();
2296 let small = torus.minor_radius();
2297 if (gap.abs() - small).abs() <= linear {
2301 return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2302 }
2303 if gap.abs() > small {
2304 return Ok(Some(Vec::new()));
2305 }
2306 let height = small.mul_add(small, -(gap * gap)).sqrt();
2307 circles_about_axis(
2308 torus.center(),
2309 axis,
2310 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2311 )
2312 .map(Some)
2313}
2314
2315pub fn exact_sphere_torus(
2328 sphere: &SphericalSurface,
2329 torus: &ToroidalSurface,
2330) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2331 let axis = torus.z_axis();
2332 let scale = torus.major_radius() + sphere.radius();
2333 let offset = sphere.center() - torus.center();
2334 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2336 return Ok(None);
2337 }
2338 let Some(crossings) = meridian_crossings(
2339 (0.0, offset.dot(axis), sphere.radius()),
2340 (torus.major_radius(), 0.0, torus.minor_radius()),
2341 scale,
2342 ) else {
2343 return Ok(None);
2344 };
2345 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2346}
2347
2348pub fn exact_cone_cone(
2373 c1: &ConicalSurface,
2374 c2: &ConicalSurface,
2375) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2376 let axis = c1.axis();
2377 let axis2 = c2.axis();
2378
2379 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2381 return Ok(None); }
2383 let apex1 = c1.apex();
2384 let apex2 = c2.apex();
2385 let delta = apex2 - apex1;
2386 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2387 let along = delta_v.dot(axis);
2388 if (delta_v - axis * along).length() > 1e-8 {
2389 return offset_parallel_cone_cone(c1, c2);
2390 }
2391
2392 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2393 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2394 return Ok(None); }
2396 let m1 = c1.half_angle().cos() / s1;
2397 let m2 = c2.half_angle().cos() / s2;
2398 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2399 let d2 = along; let denom = m1 - m2 * sigma;
2402 if denom.abs() < 1e-12 {
2403 if sigma > 0.0 && d2.abs() < 1e-9 {
2406 return Ok(None);
2407 }
2408 return Ok(Some(vec![]));
2409 }
2410
2411 let t_star = (-m2 * sigma * d2) / denom;
2412 let radius = m1 * t_star;
2413 if radius < 1e-12 {
2414 return Ok(Some(vec![])); }
2416
2417 let center = Point3::new(
2418 apex1.x() + axis.x() * t_star,
2419 apex1.y() + axis.y() * t_star,
2420 apex1.z() + axis.z() * t_star,
2421 );
2422 let circle = Circle3D::new(center, axis, radius)?;
2423 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2424}
2425
2426fn offset_parallel_cone_cone(
2437 c1: &ConicalSurface,
2438 c2: &ConicalSurface,
2439) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2440 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2441 return Ok(None); }
2443 let t1 = c1.half_angle().tan();
2444 let t2 = c2.half_angle().tan();
2445 if !t1.is_finite() || !t2.is_finite() {
2446 return Ok(None);
2447 }
2448 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2449 return Ok(None);
2450 }
2451
2452 let w = c1.axis();
2453 let apex1 = c1.apex();
2454 let apex2 = c2.apex();
2455 let delta = apex2 - apex1;
2456 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2457 let s = delta_v.dot(w);
2458 let tm = 0.5 * (t1 + t2);
2459 let k = 1.0 + tm * tm;
2460
2461 let n = (delta_v - w * (k * s)) * 2.0;
2465 let n_len = n.length();
2466 if n_len < 1e-12 {
2467 return Ok(None);
2468 }
2469 let n_hat = n * (1.0 / n_len);
2470 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2471
2472 let axis2 = c2.axis();
2478 let scale = 1.0 + delta_v.length();
2479 let mut out = Vec::new();
2480 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2481 let samples: Vec<Point3> = match &curve {
2482 ExactIntersectionCurve::Circle(c) => (0..4)
2483 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2484 .collect(),
2485 ExactIntersectionCurve::Ellipse(e) => (0..4)
2486 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2487 .collect(),
2488 ExactIntersectionCurve::Points(_) => return Ok(None),
2489 };
2490 let on_real_nappe = |p: &Point3| {
2491 let rel = *p - apex2;
2492 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2493 };
2494 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2495 match hits {
2496 0 => {}
2497 4 => out.push(curve),
2498 _ => return Ok(None),
2499 }
2500 }
2501 Ok(Some(out))
2502}
2503
2504pub fn exact_cone_cylinder(
2524 cone: &ConicalSurface,
2525 cyl: &CylindricalSurface,
2526) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2527 let axis = cone.axis();
2528 let cyl_axis = cyl.axis();
2529
2530 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2532 return Ok(None);
2533 }
2534 let apex = cone.apex();
2535 let delta = apex - cyl.origin();
2536 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2537 let along = delta_v.dot(cyl_axis);
2538 if (delta_v - cyl_axis * along).length() > 1e-8 {
2539 return Ok(None);
2540 }
2541
2542 let s = cone.half_angle().sin();
2543 if s.abs() < 1e-12 {
2544 return Ok(None); }
2546 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2548 return Ok(None); }
2550
2551 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2553 return Ok(Some(vec![])); }
2555 let center = Point3::new(
2556 apex.x() + axis.x() * t_star,
2557 apex.y() + axis.y() * t_star,
2558 apex.z() + axis.z() * t_star,
2559 );
2560 let circle = Circle3D::new(center, axis, cyl.radius())?;
2561 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2562}
2563
2564fn conic_loop(centre: Point3, u: Vec3, v: Vec3, a: f64, b: f64) -> Result<NurbsCurve, MathError> {
2569 let w = std::f64::consts::FRAC_1_SQRT_2;
2570 let corners: [(f64, f64); 9] = [
2571 (1.0, 0.0),
2572 (1.0, 1.0),
2573 (0.0, 1.0),
2574 (-1.0, 1.0),
2575 (-1.0, 0.0),
2576 (-1.0, -1.0),
2577 (0.0, -1.0),
2578 (1.0, -1.0),
2579 (1.0, 0.0),
2580 ];
2581 let points = corners
2582 .iter()
2583 .map(|&(x, y)| centre + u * (a * x) + v * (b * y))
2584 .collect();
2585 let weights = vec![1.0, w, 1.0, w, 1.0, w, 1.0, w, 1.0];
2586 let knots = vec![
2587 0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
2588 ];
2589 NurbsCurve::new(2, knots, points, weights)
2590}
2591
2592fn algebraic_cone_cone(
2601 c1: &ConicalSurface,
2602 c2: &ConicalSurface,
2603) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2604 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2605 return Ok(None);
2606 };
2607 let mut curves = Vec::new();
2608 for exact in exacts {
2609 let n_samples = 33;
2610 let mut points = Vec::with_capacity(n_samples);
2611 #[allow(clippy::cast_precision_loss)]
2612 for i in 0..n_samples {
2613 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2614 let pt = match &exact {
2615 ExactIntersectionCurve::Circle(circle) => {
2616 crate::traits::ParametricCurve::evaluate(circle, theta)
2617 }
2618 ExactIntersectionCurve::Ellipse(ellipse) => {
2619 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2620 }
2621 ExactIntersectionCurve::Points(_) => break,
2622 };
2623 points.push(IntersectionPoint {
2624 point: pt,
2625 param1: (0.0, 0.0),
2626 param2: (0.0, 0.0),
2627 });
2628 }
2629 let curve = match &exact {
2630 ExactIntersectionCurve::Circle(c) => {
2631 conic_loop(c.center(), c.u_axis(), c.v_axis(), c.radius(), c.radius())?
2632 }
2633 ExactIntersectionCurve::Ellipse(e) => conic_loop(
2634 e.center(),
2635 e.u_axis(),
2636 e.v_axis(),
2637 e.semi_major(),
2638 e.semi_minor(),
2639 )?,
2640 ExactIntersectionCurve::Points(_) => continue,
2641 };
2642 curves.push(IntersectionCurve { curve, points });
2643 }
2644 Ok(Some(curves))
2645}
2646
2647pub fn exact_sphere_cylinder(
2667 sphere: &SphericalSurface,
2668 cyl: &CylindricalSurface,
2669) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2670 let sc = sphere.center();
2671 let r_sphere = sphere.radius();
2672 let co = cyl.origin();
2673 let axis = cyl.axis();
2674 let r_cyl = cyl.radius();
2675
2676 let delta = sc - co;
2678 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2679 let along = delta_vec.dot(axis);
2680 let perp_vec = delta_vec - axis * along;
2681 let d_perp = perp_vec.length();
2682
2683 if d_perp > 1e-7 {
2686 return Ok(None);
2687 }
2688
2689 if r_cyl > r_sphere + 1e-10 {
2692 return Ok(Some(vec![]));
2693 }
2694 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2695 if z_sq < 0.0 {
2696 return Ok(Some(vec![]));
2697 }
2698 let z = z_sq.sqrt();
2699
2700 let center_axis_pt = Point3::new(
2703 co.x() + axis.x() * along,
2704 co.y() + axis.y() * along,
2705 co.z() + axis.z() * along,
2706 );
2707
2708 let mut circles = Vec::new();
2709 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2710 for &z_offset in offsets {
2711 let center = Point3::new(
2712 center_axis_pt.x() + axis.x() * z_offset,
2713 center_axis_pt.y() + axis.y() * z_offset,
2714 center_axis_pt.z() + axis.z() * z_offset,
2715 );
2716 let circle = Circle3D::new(center, axis, r_cyl)?;
2717 circles.push(ExactIntersectionCurve::Circle(circle));
2718 }
2719 Ok(Some(circles))
2720}
2721
2722pub fn exact_cone_sphere(
2740 cone: &ConicalSurface,
2741 sphere: &SphericalSurface,
2742) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2743 let offset = cone.apex() - sphere.center();
2744 let along = offset.dot(cone.axis());
2745 if (offset - cone.axis() * along).length() > 1e-7 {
2746 return Ok(None);
2747 }
2748 let lin_tol = Tolerance::new().linear;
2749 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2750 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2751 let b = 2.0 * sin_a * along;
2752 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2753 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2756 if disc < -noise {
2757 return Ok(Some(vec![]));
2758 }
2759 let roots: &[f64] = if far - near < lin_tol {
2760 &[far]
2761 } else {
2762 &[near, far]
2763 };
2764 let mut circles = Vec::new();
2765 for &v in roots {
2766 if v * cos_a > lin_tol {
2767 let centre = cone.apex() + cone.axis() * (v * sin_a);
2768 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2769 circles.push(ExactIntersectionCurve::Circle(circle));
2770 }
2771 }
2772 Ok(Some(circles))
2773}
2774
2775fn algebraic_sphere_cylinder(
2784 sphere: &SphericalSurface,
2785 cyl: &CylindricalSurface,
2786 sphere_first: bool,
2787) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2788 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2789 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2790 };
2791
2792 let mut curves = Vec::new();
2793 for exact in exacts {
2794 let ExactIntersectionCurve::Circle(circle) = exact else {
2795 continue;
2796 };
2797 let n_samples = 33;
2798 let mut points = Vec::with_capacity(n_samples);
2799 #[allow(clippy::cast_precision_loss)]
2800 for i in 0..n_samples {
2801 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2802 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2803 let (param1, param2) = in_order(
2804 sphere.project_point(pt),
2805 cyl.project_point(pt),
2806 sphere_first,
2807 );
2808 points.push(IntersectionPoint {
2809 point: pt,
2810 param1,
2811 param2,
2812 });
2813 }
2814 let r = circle.radius();
2815 let curve = conic_loop(circle.center(), circle.u_axis(), circle.v_axis(), r, r)?;
2816 curves.push(IntersectionCurve { curve, points });
2817 }
2818
2819 Ok(Some(curves))
2820}
2821
2822fn off_axis_sphere_cylinder(
2831 sphere: &SphericalSurface,
2832 cyl: &CylindricalSurface,
2833 sphere_first: bool,
2834) -> Option<Vec<IntersectionCurve>> {
2835 let (centre, radius) = (sphere.center(), sphere.radius());
2836 let axis = cyl.axis();
2837 let offset = centre - cyl.origin();
2838 let axis_distance = (offset - axis * offset.dot(axis)).length();
2839 let lin_tol = Tolerance::new().linear;
2840 if axis_distance > radius + cyl.radius() + lin_tol
2841 || axis_distance + radius < cyl.radius() - lin_tol
2842 {
2843 return Some(Vec::new());
2844 }
2845 let roots = |u: f64| {
2846 let q = cyl.evaluate(u, 0.0) - centre;
2847 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2848 };
2849 let samples = ruling_samples(cyl, &roots);
2850 let loops = if samples.iter().all(Option::is_some) {
2851 closed_ruling_loops(cyl, &roots, &samples)
2852 } else {
2853 partial_ruling_loops(cyl, &roots, &samples)
2854 };
2855 if loops.is_empty() {
2856 return None;
2857 }
2858 Some(fit_ruling_loops(&loops, |p| {
2859 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2860 }))
2861}
2862
2863const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2866 if a_first { (a, b) } else { (b, a) }
2867}
2868
2869#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2883fn algebraic_cylinder_cylinder(
2884 c1: &CylindricalSurface,
2885 c2: &CylindricalSurface,
2886) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2887 let alpha = c1.axis().dot(c2.axis());
2888 let a_coeff = 1.0 - alpha * alpha;
2889
2890 if a_coeff.abs() < 1e-12 {
2892 return Ok(None);
2893 }
2894
2895 let r1 = c1.radius();
2896 let r2 = c2.radius();
2897 let o1 = c1.origin();
2898 let o2 = c2.origin();
2899 let a1 = c1.axis();
2900 let a2 = c2.axis();
2901
2902 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2905 let cross = a1.cross(a2);
2906 let cross_len = cross.length();
2907 if cross_len > 1e-12 {
2908 let axis_dist = delta.dot(cross).abs() / cross_len;
2909 if axis_dist > r1 + r2 + Tolerance::new().linear {
2910 return Ok(Some(vec![])); }
2912 }
2913
2914 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2920 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2921 let alpha = sweep.axis().dot(a);
2922 let quad = 1.0 - alpha * alpha;
2923 let (axis, sweep) = (sweep.axis(), sweep.clone());
2924 move |u: f64| {
2925 let q = sweep.evaluate(u, 0.0) - o;
2926 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2927 let b = 2.0 * (q_a1 - alpha * q_a2);
2928 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2929 ruling_quadratic(quad, b, c)
2930 }
2931 };
2932 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2933 let samples1 = ruling_samples(c1, &roots1);
2934 let loops = if samples1.iter().all(Option::is_some) {
2935 closed_ruling_loops(c1, &roots1, &samples1)
2936 } else {
2937 let samples2 = ruling_samples(c2, &roots2);
2938 if samples2.iter().all(Option::is_some) {
2939 closed_ruling_loops(c2, &roots2, &samples2)
2940 } else if samples1.iter().any(Option::is_some) {
2941 partial_ruling_loops(c1, &roots1, &samples1)
2942 } else {
2943 partial_ruling_loops(c2, &roots2, &samples2)
2944 }
2945 };
2946 if loops.is_empty() {
2947 return Ok(None);
2948 }
2949 Ok(Some(fit_ruling_loops(&loops, |p| {
2950 (c1.project_point(p), c2.project_point(p))
2951 })))
2952}
2953
2954fn ruling_cone_cylinder(
2961 cone: &ConicalSurface,
2962 cyl: &CylindricalSurface,
2963 cone_first: bool,
2964) -> Option<Vec<IntersectionCurve>> {
2965 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2966 if sin_t < 1e-12 || cos_t < 1e-12 {
2967 return None;
2968 }
2969 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2970 let s = 1.0 / (sin_t * sin_t);
2971 let alpha = w.dot(d);
2972 let quad = 1.0 - s * alpha * alpha;
2973 if quad.abs() < 1e-9 {
2974 return None;
2975 }
2976 let roots = |u: f64| {
2977 let delta = cyl.evaluate(u, 0.0) - apex;
2978 let (dd, dw) = (delta.dot(d), delta.dot(w));
2979 let b = 2.0 * (dw - s * dd * alpha);
2980 let c = delta.dot(delta) - s * dd * dd;
2981 ruling_quadratic(quad, b, c)
2982 };
2983 let lin_tol = Tolerance::new().linear;
2984 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2985 #[allow(clippy::cast_precision_loss)]
2986 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2987 let (disc, vp, vm) = roots(u);
2988 disc >= -lin_tol
2989 && [vp, vm]
2990 .iter()
2991 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2992 });
2993 if far_nappe {
2994 return None;
2995 }
2996 let samples = ruling_samples(cyl, &roots);
2997 let scan = WINDOW_SCAN * RULING_SAMPLES;
3002 #[allow(clippy::cast_precision_loss)]
3005 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
3006 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
3007 let mut k = start;
3008 while k < start + scan {
3009 if !meets(k) {
3010 k += 1;
3011 continue;
3012 }
3013 let first = k;
3014 while k < start + scan && meets(k) {
3015 k += 1;
3016 }
3017 let covered = (first..k)
3018 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
3019 .count();
3020 if covered < WINDOW_MIN_SAMPLES {
3021 return None;
3022 }
3023 }
3024 }
3025 let loops = if samples.iter().all(Option::is_some) {
3026 closed_ruling_loops(cyl, &roots, &samples)
3027 } else {
3028 partial_ruling_loops(cyl, &roots, &samples)
3029 };
3030 if loops.is_empty() {
3031 return None;
3032 }
3033 Some(fit_ruling_loops(&loops, |p| {
3034 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
3035 }))
3036}
3037
3038fn ruling_torus_cylinder(
3048 torus: &ToroidalSurface,
3049 cyl: &CylindricalSurface,
3050 torus_first: bool,
3051) -> Option<Vec<IntersectionCurve>> {
3052 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
3053 || torus.minor_radius() >= torus.major_radius()
3054 {
3055 return None;
3056 }
3057 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
3058 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
3059 let count = rows[0].len();
3060 let scan = WINDOW_SCAN * RULING_SAMPLES;
3061 #[allow(clippy::cast_precision_loss)]
3062 if count == 0
3063 || count % 2 == 1
3064 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
3065 {
3066 return None;
3067 }
3068 let loops: Vec<Vec<Point3>> = (0..count)
3069 .map(|j| {
3070 let mut pts: Vec<Point3> = rows
3071 .iter()
3072 .enumerate()
3073 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
3074 .collect();
3075 pts.push(pts[0]);
3076 pts
3077 })
3078 .collect();
3079 Some(fit_ruling_loops(&loops, |p| {
3080 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
3081 }))
3082}
3083
3084fn ruling_cone_sphere(
3093 cone: &ConicalSurface,
3094 sphere: &SphericalSurface,
3095 cone_first: bool,
3096) -> Option<Vec<IntersectionCurve>> {
3097 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
3098 let offset = apex - centre;
3099 let lin_tol = Tolerance::new().linear;
3100 let along = offset.dot(cone.axis());
3101 let across = (offset - cone.axis() * along).length();
3102 if across < lin_tol {
3103 return None;
3104 }
3105 let k = offset.dot(offset) - radius * radius;
3111 if radius - offset.length() > lin_tol {
3112 let exit = |u: f64| {
3113 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
3114 let root = h.mul_add(h, -k).sqrt();
3115 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
3116 };
3117 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
3122 .map(|i| (ruling_u(i), exit(ruling_u(i))))
3123 .collect();
3124 for _ in 0..10 {
3125 let mut refined = Vec::with_capacity(2 * samples.len());
3126 for pair in samples.windows(2) {
3127 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
3128 refined.push(pair[0]);
3129 let um = 0.5 * (u0 + u1);
3130 let pm = exit(um);
3131 let chord = (p1 - p0).length();
3132 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
3133 refined.push((um, pm));
3134 }
3135 }
3136 refined.extend(samples.last().copied());
3137 if refined.len() == samples.len() {
3138 break;
3139 }
3140 samples = refined;
3141 }
3142 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
3143 if let Some(last) = pts.last_mut() {
3144 *last = samples[0].1;
3145 }
3146 return Some(fit_ruling_loops(&[pts], |p| {
3147 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3148 }));
3149 }
3150 let crossing = |h: f64| {
3154 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
3155 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
3156 };
3157 let (sin_a, cos_a) = cone.half_angle().sin_cos();
3162 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
3163 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
3164 {
3165 return window_cone_sphere(cone, sphere, cone_first);
3166 }
3167 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
3168 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
3169 .collect::<Option<_>>()?;
3170 let loops: Vec<Vec<Point3>> = [0, 1]
3171 .iter()
3172 .map(|&j| {
3173 let mut pts: Vec<Point3> = rows
3174 .iter()
3175 .enumerate()
3176 .map(|(i, &(near, far))| {
3177 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
3178 })
3179 .collect();
3180 pts.push(pts[0]);
3181 pts
3182 })
3183 .collect();
3184 Some(fit_ruling_loops(&loops, |p| {
3185 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3186 }))
3187}
3188
3189fn window_cone_sphere(
3204 cone: &ConicalSurface,
3205 sphere: &SphericalSurface,
3206 cone_first: bool,
3207) -> Option<Vec<IntersectionCurve>> {
3208 let offset = cone.apex() - sphere.center();
3209 let lin_tol = Tolerance::new().linear;
3210 if offset.length() - sphere.radius() <= lin_tol {
3211 return None;
3212 }
3213 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
3214 let (sin_a, cos_a) = cone.half_angle().sin_cos();
3215 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
3216 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
3217 if a < lin_tol {
3218 return None;
3219 }
3220 let reach = (-k.sqrt() - c) / a;
3221 if reach <= -1.0 {
3222 return Some(Vec::new());
3223 }
3224 if reach >= 1.0 {
3225 return None;
3226 }
3227 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
3228 let half = std::f64::consts::PI - half;
3229 let n = RULING_SAMPLES;
3230 let mut pts: Vec<Point3> = (0..n)
3231 .map(|i| {
3232 #[allow(clippy::cast_precision_loss)]
3233 let theta = TAU * i as f64 / n as f64;
3234 let u = half.mul_add(-theta.cos(), mid);
3235 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
3236 let split = h.mul_add(h, -k).max(0.0).sqrt();
3237 cone.evaluate(u, -h - split.copysign(theta.sin()))
3238 })
3239 .collect();
3240 pts.push(pts[0]);
3241 Some(fit_ruling_loops(&[pts], |p| {
3242 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
3243 }))
3244}
3245
3246const WINDOW_SCAN: usize = 16;
3249const WINDOW_MIN_SAMPLES: usize = 8;
3250
3251const RULING_SAMPLES: usize = 128;
3255
3256#[allow(clippy::cast_precision_loss)]
3257fn ruling_u(i: usize) -> f64 {
3258 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
3259}
3260
3261fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
3263 let disc = b * b - 4.0 * quad * c;
3264 let root = disc.max(0.0).sqrt();
3265 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
3266}
3267
3268fn ruling_samples(
3272 sweep: &CylindricalSurface,
3273 roots: &impl Fn(f64) -> (f64, f64, f64),
3274) -> Vec<Option<(Point3, Point3)>> {
3275 let lin_tol = Tolerance::new().linear;
3276 (0..RULING_SAMPLES)
3277 .map(|i| {
3278 let u = ruling_u(i);
3279 let (disc, vp, vm) = roots(u);
3280 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
3281 })
3282 .collect()
3283}
3284
3285fn closed_ruling_loops(
3292 sweep: &CylindricalSurface,
3293 roots: &impl Fn(f64) -> (f64, f64, f64),
3294 samples: &[Option<(Point3, Point3)>],
3295) -> Vec<Vec<Point3>> {
3296 let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3297 if let Some((neck, touching)) = narrowest_ruling(roots) {
3298 let count = 2 * RULING_SAMPLES;
3299 #[allow(clippy::cast_precision_loss)]
3300 (0..count)
3301 .map(|i| {
3302 let t = i as f64 / count as f64;
3303 let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3304 let (_, vp, vm) = roots(u);
3305 if i == 0 && touching {
3306 let at = sweep.evaluate(u, 0.5 * (vp + vm));
3307 (at, at)
3308 } else {
3309 (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3310 }
3311 })
3312 .unzip()
3313 } else {
3314 samples.iter().flatten().copied().unzip()
3315 };
3316 plus.push(plus[0]);
3317 minus.push(minus[0]);
3318 vec![plus, minus]
3319}
3320
3321fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3329 let scan = WINDOW_SCAN * RULING_SAMPLES;
3330 #[allow(clippy::cast_precision_loss)]
3331 let step = TAU / scan as f64;
3332 let gap = |u: f64| {
3333 let (_, vp, vm) = roots(u);
3334 (vp - vm).abs()
3335 };
3336 #[allow(clippy::cast_precision_loss)]
3337 let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3338 let widest = gaps.iter().copied().fold(0.0, f64::max);
3339 let tol = Tolerance::new().linear * (1.0 + widest);
3340 let mut necks = Vec::new();
3341 for k in 0..scan {
3342 let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3343 if here > before || here >= after || here >= 0.25 * widest {
3344 continue;
3345 }
3346 #[allow(clippy::cast_precision_loss)]
3347 let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3348 for _ in 0..100 {
3349 let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3350 if gap(a) < gap(b) {
3351 hi = b;
3352 } else {
3353 lo = a;
3354 }
3355 }
3356 let u = 0.5 * (lo + hi);
3357 necks.push((u, gap(u) <= tol));
3358 }
3359 match necks[..] {
3360 [neck] => Some(neck),
3361 _ => None,
3362 }
3363}
3364
3365fn partial_ruling_loops(
3370 sweep: &CylindricalSurface,
3371 roots: &impl Fn(f64) -> (f64, f64, f64),
3372 samples: &[Option<(Point3, Point3)>],
3373) -> Vec<Vec<Point3>> {
3374 let branch_point = |inside: usize, outside: usize| -> Point3 {
3375 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3376 if (hi - lo).abs() > std::f64::consts::PI {
3377 hi += if hi < lo { TAU } else { -TAU };
3378 }
3379 for _ in 0..60 {
3380 let mid = 0.5 * (lo + hi);
3381 if roots(mid).0 >= 0.0 {
3382 lo = mid;
3383 } else {
3384 hi = mid;
3385 }
3386 }
3387 let (_, vp, vm) = roots(lo);
3388 sweep.evaluate(lo, 0.5 * (vp + vm))
3389 };
3390 let Some(first_gap) = samples.iter().position(Option::is_none) else {
3391 return Vec::new();
3392 };
3393 let mut loops = Vec::new();
3394 let mut k = 0;
3395 while k < RULING_SAMPLES {
3396 let i = (first_gap + k) % RULING_SAMPLES;
3397 if samples[i].is_none() {
3398 k += 1;
3399 continue;
3400 }
3401 let start = i;
3402 let mut run = Vec::new();
3403 while k < RULING_SAMPLES {
3404 let j = (first_gap + k) % RULING_SAMPLES;
3405 let Some(pair) = samples[j] else { break };
3406 run.push(pair);
3407 k += 1;
3408 }
3409 let end = (start + run.len() - 1) % RULING_SAMPLES;
3410 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3411 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3412 let mut pts = vec![head];
3413 pts.extend(run.iter().map(|p| p.0));
3414 pts.push(tail);
3415 pts.extend(run.iter().rev().map(|p| p.1));
3416 pts.push(head);
3417 loops.push(pts);
3418 }
3419 loops
3420}
3421
3422fn fit_ruling_loops(
3425 loops: &[Vec<Point3>],
3426 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3427) -> Vec<IntersectionCurve> {
3428 let mut curves = Vec::new();
3429 for pts in loops {
3430 if pts.len() < 4 {
3431 continue;
3432 }
3433 let ipts: Vec<IntersectionPoint> = pts
3434 .iter()
3435 .map(|&p| {
3436 let (param1, param2) = params(p);
3437 IntersectionPoint {
3438 point: p,
3439 param1,
3440 param2,
3441 }
3442 })
3443 .collect();
3444 let degree = 3.min(pts.len() - 1);
3445 if let Ok(curve) = interpolate(pts, degree) {
3446 curves.push(IntersectionCurve {
3447 curve,
3448 points: ipts,
3449 });
3450 }
3451 }
3452 curves
3453}
3454
3455#[allow(clippy::unnecessary_wraps)]
3481fn algebraic_parallel_cone_cylinder(
3482 cone: &ConicalSurface,
3483 cyl: &CylindricalSurface,
3484 v_range_cone: Option<(f64, f64)>,
3485 v_range_cyl: Option<(f64, f64)>,
3486) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3487 let axis = cone.axis();
3488 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3489 return Ok(None); }
3491
3492 let apex = cone.apex();
3493 let delta = cyl.origin() - apex;
3494 let along = delta.dot(axis);
3495 let perp = delta - axis * along;
3496 let d = perp.length();
3497 if d < 1e-9 {
3498 return Ok(None); }
3500
3501 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3502 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3503
3504 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3505 if cos_t < 1e-12 || sin_t < 1e-12 {
3506 return Ok(None);
3507 }
3508 let r = cyl.radius();
3509
3510 let mut v_min = (d - r).abs() / cos_t;
3512 let mut v_max = (d + r) / cos_t;
3513 if v_max <= v_min {
3514 return Ok(Some(vec![]));
3515 }
3516
3517 let mut lo = v_min;
3523 let mut hi = v_max;
3524 if let Some((a, b)) = v_range_cone {
3529 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3530 lo = lo.max(a);
3531 hi = hi.min(b);
3532 }
3533 if let Some((a, b)) = v_range_cyl {
3534 let flip = cyl.axis().dot(axis);
3537 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3538 let (a, b) = (to_cone_v(a), to_cone_v(b));
3539 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3540 lo = lo.max(a);
3541 hi = hi.min(b);
3542 }
3543 let (turn_lo, turn_hi) = (v_min, v_max);
3544 v_min = lo.max(v_min);
3545 v_max = hi.min(v_max);
3546 if v_max - v_min <= 1e-12 {
3547 return Ok(Some(vec![]));
3548 }
3549 let slack = Tolerance::new().linear;
3557 #[allow(clippy::cast_precision_loss)]
3558 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3559 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3560 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3561 .map(|i| {
3562 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3563 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3564 let off = foot - apex;
3565 let across = off - axis * off.dot(axis);
3566 apex + across + axis * (across.length() * sin_t / cos_t)
3567 })
3568 .collect();
3569 pts.push(pts[0]);
3570 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3571 (cone.project_point(p), cyl.project_point(p))
3572 })));
3573 }
3574
3575 let n_samples = 128;
3576 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3577 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3578 #[allow(clippy::cast_precision_loss)]
3579 for i in 0..=n_samples {
3580 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3581 let rho = v * cos_t;
3582 if rho < 1e-12 {
3583 if (d - r).abs() < 1e-12 {
3591 let apex = cone.evaluate(phi0, v);
3592 plus.push(apex);
3593 minus.push(apex);
3594 }
3595 continue;
3596 }
3597 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3598 let alpha = cos_alpha.acos();
3599 plus.push(cone.evaluate(phi0 + alpha, v));
3600 minus.push(cone.evaluate(phi0 - alpha, v));
3601 }
3602
3603 let mut curves = Vec::new();
3604 for pts in [&plus, &minus] {
3605 if pts.len() < 4 {
3608 continue;
3609 }
3610 let ipts: Vec<IntersectionPoint> = pts
3611 .iter()
3612 .map(|&p| IntersectionPoint {
3613 point: p,
3614 param1: cone.project_point(p),
3615 param2: cyl.project_point(p),
3616 })
3617 .collect();
3618 let degree = 3.min(pts.len() - 1);
3619 match interpolate(pts, degree) {
3620 Ok(curve) => curves.push(IntersectionCurve {
3621 curve,
3622 points: ipts,
3623 }),
3624 Err(_) => return Ok(None),
3629 }
3630 }
3631
3632 Ok(Some(curves))
3633}
3634
3635fn algebraic_sphere_sphere(
3643 s1: &SphericalSurface,
3644 s2: &SphericalSurface,
3645) -> Result<Vec<IntersectionCurve>, MathError> {
3646 let c1 = s1.center();
3647 let c2 = s2.center();
3648 let r1 = s1.radius();
3649 let r2 = s2.radius();
3650
3651 let delta = c2 - c1;
3652 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3653 let d = d_sq.sqrt();
3654
3655 if d < 1e-12 {
3656 return Ok(vec![]);
3658 }
3659
3660 if d > r1 + r2 + 1e-10 {
3662 return Ok(vec![]); }
3664 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3665 return Ok(vec![]); }
3667
3668 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3670
3671 let r_circle_sq = r1 * r1 - d1 * d1;
3673 if r_circle_sq < 0.0 {
3674 if r_circle_sq > -1e-10 {
3676 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3678 let tangent_pt = Point3::new(
3679 c1.x() + axis.x() * d1,
3680 c1.y() + axis.y() * d1,
3681 c1.z() + axis.z() * d1,
3682 );
3683 let ipt = IntersectionPoint {
3684 point: tangent_pt,
3685 param1: (0.0, 0.0),
3686 param2: (0.0, 0.0),
3687 };
3688 return Ok(vec![IntersectionCurve {
3690 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3691 points: vec![ipt],
3692 }]);
3693 }
3694 return Ok(vec![]);
3695 }
3696
3697 let r_circle = r_circle_sq.sqrt();
3698 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3699 let center = Point3::new(
3700 c1.x() + axis.x() * d1,
3701 c1.y() + axis.y() * d1,
3702 c1.z() + axis.z() * d1,
3703 );
3704
3705 let basis = Frame3::from_normal(center, axis)?;
3707 let u_dir = basis.x;
3708 let v_dir = basis.y;
3709
3710 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3713 #[allow(clippy::cast_precision_loss)]
3714 for i in 0..n_samples {
3715 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3716 let (sin_t, cos_t) = theta.sin_cos();
3717 let pt = Point3::new(
3718 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3719 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3720 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3721 );
3722 points.push(IntersectionPoint {
3723 point: pt,
3724 param1: (0.0, 0.0),
3725 param2: (0.0, 0.0),
3726 });
3727 }
3728
3729 let curve = conic_loop(center, u_dir, v_dir, r_circle, r_circle)?;
3730
3731 Ok(vec![IntersectionCurve { curve, points }])
3732}
3733
3734#[allow(clippy::too_many_arguments)]
3740fn correct_to_intersection(
3741 a: &AnalyticSurface<'_>,
3742 b: &AnalyticSurface<'_>,
3743 surf_a: &dyn Fn(f64, f64) -> Point3,
3744 norm_a: &dyn Fn(f64, f64) -> Vec3,
3745 surf_b: &dyn Fn(f64, f64) -> Point3,
3746 norm_b: &dyn Fn(f64, f64) -> Vec3,
3747 point: Point3,
3748 u_range_a: (f64, f64),
3749 v_range_a: (f64, f64),
3750 u_range_b: (f64, f64),
3751 v_range_b: (f64, f64),
3752 max_iters: usize,
3753) -> Point3 {
3754 let mut p = point;
3755 for _ in 0..max_iters {
3756 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3757 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3758 let pa = surf_a(ua, va);
3759 let pb = surf_b(ub, vb);
3760 let na = norm_a(ua, va);
3761 let nb = norm_b(ub, vb);
3762 let pv = Vec3::new(p.x(), p.y(), p.z());
3763
3764 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3765 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3766
3767 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3768 break;
3769 }
3770
3771 let t = na.cross(nb);
3772 let t_len = t.length();
3773 if t_len < 1e-10 {
3774 return Point3::new(
3776 (pa.x() + pb.x()) * 0.5,
3777 (pa.y() + pb.y()) * 0.5,
3778 (pa.z() + pb.z()) * 0.5,
3779 );
3780 }
3781 let t_hat = t * (1.0 / t_len);
3782
3783 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3785 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3786 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3787 if det.abs() < 1e-15 {
3788 return Point3::new(
3789 (pa.x() + pb.x()) * 0.5,
3790 (pa.y() + pb.y()) * 0.5,
3791 (pa.z() + pb.z()) * 0.5,
3792 );
3793 }
3794 let inv = 1.0 / det;
3795 let dx = inv
3797 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3798 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3799 let dy = inv
3800 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3801 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3802 let dz = inv
3803 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3804 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3805 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3806
3807 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3810 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3811 let pc_a = surf_a(uc, vc);
3812 let pc_b = surf_b(ud, vd);
3813 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3814 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3815 .dot(norm_a(uc, vc))
3816 .abs();
3817 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3818 .dot(norm_b(ud, vd))
3819 .abs();
3820 if da_new > da.abs() && db_new > db.abs() {
3821 return p;
3822 }
3823
3824 p = candidate;
3825 }
3826 p
3827}
3828
3829#[allow(clippy::too_many_arguments)]
3835fn march_analytic_intersection(
3836 a: &AnalyticSurface<'_>,
3837 b: &AnalyticSurface<'_>,
3838 surf_a: &dyn Fn(f64, f64) -> Point3,
3839 norm_a: &dyn Fn(f64, f64) -> Vec3,
3840 surf_b: &dyn Fn(f64, f64) -> Point3,
3841 norm_b: &dyn Fn(f64, f64) -> Vec3,
3842 seed: Point3,
3843 u_range_a: (f64, f64),
3844 v_range_a: (f64, f64),
3845 u_range_b: (f64, f64),
3846 v_range_b: (f64, f64),
3847 initial_step: f64,
3848 u_periodic_a: bool,
3849 u_periodic_b: bool,
3850 region: Option<Aabb3>,
3851) -> Vec<Point3> {
3852 let max_steps = 500;
3853 let h_min = 1e-6;
3854 let h_max = initial_step * 4.0;
3855 let closure_dist = initial_step * 5.0;
3859 let max_angle = 10.0_f64.to_radians();
3861 let min_angle = 2.0_f64.to_radians();
3862
3863 let mut forward = Vec::new();
3865 let mut backward = Vec::new();
3867
3868 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3869 let mut current = seed;
3870 let mut h = initial_step;
3871 let mut prev_tangent: Option<Vec3> = None;
3872
3873 for _ in 0..max_steps {
3874 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3875 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3876
3877 let na = norm_a(ua, va);
3878 let nb = norm_b(ub, vb);
3879
3880 let tangent = na.cross(nb);
3881 let t_len = tangent.length();
3882 if t_len < 1e-10 {
3883 break;
3884 }
3885 let t_dir = tangent * (direction / t_len);
3886
3887 if let Some(prev_t) = prev_tangent {
3889 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3890 let angle = cos_angle.acos();
3891 if angle > max_angle && h > h_min {
3892 h = (h * 0.5).max(h_min);
3893 } else if angle < min_angle {
3894 h = (h * 2.0).min(h_max);
3895 }
3896 }
3897 prev_tangent = Some(t_dir);
3898
3899 let next = Point3::new(
3900 h.mul_add(t_dir.x(), current.x()),
3901 h.mul_add(t_dir.y(), current.y()),
3902 h.mul_add(t_dir.z(), current.z()),
3903 );
3904
3905 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3906 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3907
3908 let pa = surf_a(ua2, va2);
3909 let pb = surf_b(ub2, vb2);
3910 let mid = Point3::new(
3911 (pa.x() + pb.x()) * 0.5,
3912 (pa.y() + pb.y()) * 0.5,
3913 (pa.z() + pb.z()) * 0.5,
3914 );
3915 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3916 || va2 <= v_range_a.0
3917 || va2 >= v_range_a.1;
3918 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3919 || vb2 <= v_range_b.0
3920 || vb2 >= v_range_b.1;
3921
3922 if out_a || out_b {
3923 break;
3924 }
3925 if region.is_some_and(|r| !r.contains_point(mid)) {
3926 points.push(mid);
3927 break;
3928 }
3929
3930 let dist_to_seed = (mid - seed).length();
3934 if points.len() > 10 && dist_to_seed < closure_dist {
3935 points.push(seed);
3936 break;
3937 }
3938
3939 points.push(mid);
3940 current = mid;
3941 }
3942 }
3943
3944 backward.reverse();
3946 let mut result = backward;
3947 result.push(seed);
3948 result.append(&mut forward);
3949
3950 for pt in &mut result {
3952 *pt = correct_to_intersection(
3953 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3954 5,
3955 );
3956 }
3957
3958 result
3959}
3960
3961fn project_analytic(
3965 surface: &AnalyticSurface<'_>,
3966 point: Point3,
3967 u_range: (f64, f64),
3968 v_range: (f64, f64),
3969) -> (f64, f64) {
3970 match surface {
3971 AnalyticSurface::Cylinder(cyl) => {
3972 let (u, v) = cyl.project_point(point);
3973 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3974 }
3975 AnalyticSurface::Sphere(sphere) => {
3976 let (u, v) = sphere.project_point(point);
3977 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3978 }
3979 AnalyticSurface::Cone(cone) => {
3980 let (u, v) = cone.project_point(point);
3981 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3982 }
3983 AnalyticSurface::Torus(torus) => {
3984 let (u, v) = torus.project_point(point);
3985 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3986 }
3987 }
3988}
3989
3990fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3994 matches!(
3995 surface,
3996 AnalyticSurface::Cylinder(_)
3997 | AnalyticSurface::Cone(_)
3998 | AnalyticSurface::Sphere(_)
3999 | AnalyticSurface::Torus(_)
4000 )
4001}
4002
4003#[allow(clippy::type_complexity)]
4005fn surface_closures<'a>(
4006 surface: &'a AnalyticSurface<'a>,
4007) -> (
4008 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
4009 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
4010 (f64, f64),
4011 (f64, f64),
4012) {
4013 match surface {
4014 AnalyticSurface::Cylinder(cyl) => (
4015 Box::new(|u, v| cyl.evaluate(u, v)),
4016 Box::new(|u, v| cyl.normal(u, v)),
4017 (0.0, TAU),
4018 (-1.0, 1.0),
4019 ),
4020 AnalyticSurface::Cone(cone) => (
4021 Box::new(|u, v| cone.evaluate(u, v)),
4022 Box::new(|u, v| cone.normal(u, v)),
4023 (0.0, TAU),
4024 (0.01, 2.0),
4025 ),
4026 AnalyticSurface::Sphere(sphere) => (
4027 Box::new(|u, v| sphere.evaluate(u, v)),
4028 Box::new(|u, v| sphere.normal(u, v)),
4029 (0.0, TAU),
4030 (-FRAC_PI_2, FRAC_PI_2),
4031 ),
4032 AnalyticSurface::Torus(torus) => (
4033 Box::new(|u, v| torus.evaluate(u, v)),
4034 Box::new(|u, v| torus.normal(u, v)),
4035 (0.0, TAU),
4036 (0.0, TAU),
4037 ),
4038 }
4039}
4040
4041#[cfg(test)]
4042#[allow(clippy::unwrap_used, clippy::expect_used)]
4043mod tests {
4044 use super::*;
4045 use crate::tolerance::Tolerance;
4046
4047 #[test]
4051 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
4052 let half_angle = 1.1_f64;
4053 let cone = ConicalSurface::new(
4054 Point3::new(0.0, 0.0, 0.0),
4055 Vec3::new(0.0, 0.0, 1.0),
4056 half_angle,
4057 )
4058 .unwrap();
4059 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
4060 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
4061 let chains =
4062 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
4063 .unwrap();
4064 let chain = chains
4065 .iter()
4066 .find_map(|c| match c {
4067 ExactIntersectionCurve::Points(chain) => Some(chain),
4068 _ => None,
4069 })
4070 .expect("a parabola or hyperbola section is sampled");
4071 let (from, to) = (chain[2], chain[chain.len() - 3]);
4072 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
4073 .unwrap()
4074 .expect("an exact arc");
4075 let (t0, t1) = arc.domain();
4076 assert!((arc.evaluate(t0) - from).length() < 1e-12);
4077 assert!((arc.evaluate(t1) - to).length() < 1e-12);
4078 for i in 0..=200 {
4079 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
4080 let w = q - Point3::new(0.0, 0.0, 0.0);
4081 let off_plane = (normal.dot(w) - d).abs();
4082 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
4083 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
4084 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
4085 }
4086 }
4087 }
4088
4089 #[test]
4093 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
4094 let half_angle = 1.1_f64;
4095 let cone = ConicalSurface::new(
4096 Point3::new(0.0, 0.0, 0.0),
4097 Vec3::new(0.0, 0.0, 1.0),
4098 half_angle,
4099 )
4100 .unwrap();
4101 for shortfall in [1e-10, 3e-10, 8e-10] {
4102 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
4103 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4104 let chains =
4105 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
4106 .unwrap();
4107 let Some(chain) = chains.iter().find_map(|c| match c {
4108 ExactIntersectionCurve::Points(chain) => Some(chain),
4109 _ => None,
4110 }) else {
4111 continue;
4112 };
4113 let (from, to) = (chain[2], chain[chain.len() - 3]);
4114 assert!(
4115 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
4116 .unwrap()
4117 .is_none(),
4118 "coincident ends"
4119 );
4120 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
4121 continue;
4122 };
4123 let (t0, t1) = arc.domain();
4124 for i in 0..=200 {
4125 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
4126 - Point3::new(0.0, 0.0, 0.0);
4127 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
4128 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
4129 }
4130 }
4131 }
4132
4133 #[test]
4134 fn plane_cylinder_perpendicular() {
4135 let cyl =
4136 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4137 .unwrap();
4138
4139 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
4141 assert!(!curves.is_empty(), "should find intersection curve");
4142 assert!(
4143 curves[0].points.len() > 10,
4144 "should have many sample points"
4145 );
4146
4147 let tol = Tolerance::loose();
4148 for pt in &curves[0].points {
4149 assert!(
4150 tol.approx_eq(pt.point.z(), 3.0),
4151 "z should be ~3.0, got {}",
4152 pt.point.z()
4153 );
4154 let r = pt.point.x().hypot(pt.point.y());
4155 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
4156 }
4157 }
4158
4159 #[test]
4160 fn plane_sphere_equator() {
4161 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4162
4163 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4164 assert!(!curves.is_empty());
4165
4166 let tol = Tolerance::loose();
4167 for pt in &curves[0].points {
4168 assert!(
4169 tol.approx_eq(pt.point.z(), 0.0),
4170 "z should be ~0, got {}",
4171 pt.point.z()
4172 );
4173 let r = pt.point.x().hypot(pt.point.y());
4174 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
4175 }
4176 }
4177
4178 #[test]
4179 fn plane_sphere_no_intersection() {
4180 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
4181
4182 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
4183 assert!(curves.is_empty());
4184 }
4185
4186 #[test]
4187 fn plane_cone_cross_section() {
4188 let cone = ConicalSurface::new(
4189 Point3::new(0.0, 0.0, 0.0),
4190 Vec3::new(0.0, 0.0, 1.0),
4191 std::f64::consts::FRAC_PI_4,
4192 )
4193 .unwrap();
4194
4195 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
4196 assert!(!curves.is_empty(), "should find intersection with cone");
4197 }
4198
4199 #[test]
4206 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
4207 let c1 = ConicalSurface::new(
4208 Point3::new(
4209 -16.999_999_999_999_975,
4210 -16.999_999_999_999_975,
4211 5.849_999_999_999_951,
4212 ),
4213 Vec3::new(0.0, 0.0, -1.0),
4214 0.785_398_163_397_433_5,
4215 )
4216 .unwrap();
4217 let c2 = ConicalSurface::new(
4218 Point3::new(
4219 -16.750_000_000_000_036,
4220 -16.750_000_000_000_018,
4221 0.749_999_999_999_881,
4222 ),
4223 Vec3::new(0.0, 0.0, 1.0),
4224 0.785_398_163_397_467_6,
4225 )
4226 .unwrap();
4227
4228 let curves = exact_cone_cone(&c1, &c2)
4229 .unwrap()
4230 .expect("offset parallel equal-angle cones must take the radical-plane path");
4231 assert_eq!(curves.len(), 1, "expected exactly one section conic");
4232 assert!(
4233 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
4234 "expected an ellipse section, got {:?}",
4235 curves[0]
4236 );
4237 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
4238 return;
4239 };
4240
4241 for i in 0..16 {
4245 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
4246 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
4247 let rel = p - cone.apex();
4248 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
4249 let axial = rel_v.dot(cone.axis());
4250 let radial = (rel_v - cone.axis() * axial).length();
4251 assert!(
4252 axial > 0.0,
4253 "{label}: sample on phantom nappe (axial {axial})"
4254 );
4255 let expect = cone.half_angle().tan() * axial;
4256 assert!(
4257 (radial - expect).abs() < 1e-9,
4258 "{label}: sample off surface by {}",
4259 (radial - expect).abs()
4260 );
4261 }
4262 }
4263 }
4264
4265 #[test]
4269 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
4270 let c1 = ConicalSurface::new(
4271 Point3::new(0.0, 0.0, 5.0),
4272 Vec3::new(0.0, 0.0, -1.0),
4273 std::f64::consts::FRAC_PI_4,
4274 )
4275 .unwrap();
4276 let c2 = ConicalSurface::new(
4277 Point3::new(0.25, 0.25, 20.0),
4278 Vec3::new(0.0, 0.0, 1.0),
4279 std::f64::consts::FRAC_PI_4,
4280 )
4281 .unwrap();
4282 let curves = exact_cone_cone(&c1, &c2)
4283 .unwrap()
4284 .expect("radical-plane path");
4285 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
4286 }
4287
4288 #[test]
4291 fn offset_parallel_cones_with_unequal_angles_defer() {
4292 let c1 = ConicalSurface::new(
4293 Point3::new(0.0, 0.0, 5.0),
4294 Vec3::new(0.0, 0.0, -1.0),
4295 std::f64::consts::FRAC_PI_4,
4296 )
4297 .unwrap();
4298 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4299 .unwrap();
4300 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4301 }
4302
4303 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4307 let cone = ConicalSurface::new(
4308 Point3::new(0.0, 0.0, 0.0),
4309 Vec3::new(0.0, 0.0, 1.0),
4310 std::f64::consts::FRAC_PI_4,
4311 )
4312 .unwrap();
4313 let (s, c) = 40.0_f64.to_radians().sin_cos();
4314 let tube =
4315 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4316 (cone, tube)
4317 }
4318
4319 #[test]
4320 fn marcher_keeps_to_its_region() {
4321 let (cone, tube) = cone_and_tilted_tube();
4322 let run = |region: Option<Aabb3>| {
4323 let (a, b) = (
4324 AnalyticSurface::Cone(&cone),
4325 AnalyticSurface::Cylinder(&tube),
4326 );
4327 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4328 match region {
4329 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4330 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4331 }
4332 .unwrap()
4333 };
4334 assert!(!run(None).is_empty());
4335 let near = Aabb3 {
4336 min: Point3::new(-0.5, -2.0, 0.8),
4337 max: Point3::new(0.7, -0.7, 2.0),
4338 };
4339 let curves = run(Some(near));
4340 assert!(!curves.is_empty(), "the loop through the region is kept");
4341 let reach = near.expanded(0.6);
4343 for curve in &curves {
4344 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4345 }
4346 let away = Aabb3 {
4347 min: Point3::new(5.0, 5.0, 5.0),
4348 max: Point3::new(6.0, 6.0, 6.0),
4349 };
4350 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4351 }
4352
4353 #[test]
4354 fn coaxial_cones_cross_at_single_circle() {
4355 let outer = ConicalSurface::new(
4360 Point3::new(0.0, 0.0, 50.0),
4361 Vec3::new(0.0, 0.0, -1.0),
4362 5.0_f64.atan(),
4363 )
4364 .unwrap();
4365 let inner = ConicalSurface::new(
4366 Point3::new(0.0, 0.0, 90.0),
4367 Vec3::new(0.0, 0.0, -1.0),
4368 10.0_f64.atan(),
4369 )
4370 .unwrap();
4371
4372 let curves = intersect_analytic_analytic_bounded(
4373 AnalyticSurface::Cone(&outer),
4374 AnalyticSurface::Cone(&inner),
4375 32,
4376 None,
4377 None,
4378 )
4379 .unwrap();
4380
4381 assert_eq!(
4382 curves.len(),
4383 1,
4384 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4385 curves.len()
4386 );
4387 for p in &curves[0].points {
4388 let r = p.point.x().hypot(p.point.y());
4389 assert!(
4390 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4391 "intersection point off the expected z=10,r=8 circle: {:?}",
4392 p.point
4393 );
4394 }
4395 }
4396
4397 #[test]
4398 fn plane_torus_cross_section() {
4399 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4400
4401 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4402 assert!(
4403 !curves.is_empty(),
4404 "should find intersection curves with torus"
4405 );
4406 }
4407
4408 #[test]
4411 fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4412 for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4413 let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4414 for z in [3.0 - minor, 3.0 + minor] {
4415 let exact = exact_plane_analytic(
4416 AnalyticSurface::Torus(&torus),
4417 Vec3::new(0.0, 0.0, 1.0),
4418 z,
4419 )
4420 .unwrap();
4421 assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4422 let circle = match &exact[0] {
4423 ExactIntersectionCurve::Circle(c) => Some(c),
4424 _ => None,
4425 };
4426 let c = circle.expect("a circle");
4427 assert!((c.radius() - major).abs() < 1e-12);
4428 assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4429
4430 let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4431 assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4432 }
4433 let single = |normal: Vec3, d: f64| {
4435 matches!(
4436 exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4437 .unwrap()
4438 .as_slice(),
4439 [ExactIntersectionCurve::Circle(_)]
4440 )
4441 };
4442 assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4443 }
4444 }
4445
4446 #[test]
4450 fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4451 let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4452 let level = Vec3::new(0.0, 0.0, 1.0);
4453 let curves =
4454 |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4455 assert!(matches!(
4456 curves(3.0 + 100.1).as_slice(),
4457 [ExactIntersectionCurve::Circle(_)]
4458 ));
4459 let inside = curves(3.0 + 100.1 - 4e-7);
4460 assert!(!matches!(
4461 inside.as_slice(),
4462 [ExactIntersectionCurve::Circle(_)]
4463 ));
4464 }
4465
4466 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4469 let rho = p.x().hypot(p.y());
4470 ((rho - major).hypot(p.z())) - minor
4471 }
4472
4473 #[test]
4479 fn oblique_cone_cylinder_traces_curves_on_both() {
4480 use crate::traits::ParametricCurve;
4481 let cone = ConicalSurface::new(
4485 Point3::new(0.0, 0.0, 3.0),
4486 Vec3::new(0.0, 0.0, -1.0),
4487 2.0_f64.atan(),
4488 )
4489 .unwrap();
4490 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4491 let cyl =
4492 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4493 .unwrap();
4494 for cone_first in [true, false] {
4495 let (a, b) = if cone_first {
4496 (
4497 AnalyticSurface::Cone(&cone),
4498 AnalyticSurface::Cylinder(&cyl),
4499 )
4500 } else {
4501 (
4502 AnalyticSurface::Cylinder(&cyl),
4503 AnalyticSurface::Cone(&cone),
4504 )
4505 };
4506 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4507 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4508 for c in &curves {
4509 let (t0, t1) = c.curve.domain();
4510 for k in 0..=64 {
4511 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4512 let p = ParametricCurve::evaluate(&c.curve, t);
4513 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4516 assert!(
4517 (rod - 0.6).abs() < 1e-4,
4518 "x0 {x0}: off the rod by {}",
4519 rod - 0.6
4520 );
4521 let cone_r = p.x().hypot(p.y());
4522 assert!(
4523 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4524 "x0 {x0}: off the cone at {p:?}"
4525 );
4526 }
4527 }
4528 }
4529 }
4530 }
4531
4532 #[test]
4533 fn a_rod_through_a_rings_tube_traces_four_loops() {
4534 use crate::traits::ParametricCurve;
4535 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4536 let rod =
4539 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4540 .unwrap();
4541 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4542 assert_eq!(curves.len(), 4);
4543 for c in &curves {
4544 let (t0, t1) = c.curve.domain();
4545 for k in 0..=64 {
4546 let p =
4547 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4548 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4549 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4550 assert!(
4551 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4552 "off by {on_rod}, {on_ring}"
4553 );
4554 }
4555 }
4556 let high =
4558 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4559 .unwrap();
4560 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4561 let grazing =
4563 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4564 .unwrap();
4565 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4566 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4568 let thin =
4569 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4570 .unwrap();
4571 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4572 }
4573
4574 #[test]
4575 fn a_pin_through_a_ball_traces_two_loops() {
4576 use crate::traits::ParametricCurve;
4577 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4578 let half = 0.08_f64.atan();
4581 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4582 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4583 for cone_first in [true, false] {
4584 let (a, b) = if cone_first {
4585 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4586 } else {
4587 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4588 };
4589 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4590 assert_eq!(curves.len(), 2, "entry and exit loops");
4591 for c in &curves {
4592 let (t0, t1) = c.curve.domain();
4593 for k in 0..=64 {
4594 let p = ParametricCurve::evaluate(
4595 &c.curve,
4596 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4597 );
4598 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4599 let axial = apex.z() - p.z();
4600 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4601 assert!(
4602 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4603 "off by {on_ball}, {on_pin}"
4604 );
4605 }
4606 }
4607 }
4608 let coaxial =
4613 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4614 .unwrap();
4615 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4616 let aside = ConicalSurface::new(
4617 Point3::new(2.8, 0.0, 10.0),
4618 Vec3::new(0.0, 0.0, -1.0),
4619 FRAC_PI_2 - half,
4620 )
4621 .unwrap();
4622 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4623 let holding = ConicalSurface::new(
4624 Point3::new(1.0, 0.5, 1.0),
4625 Vec3::new(0.0, 0.0, -1.0),
4626 FRAC_PI_2 - half,
4627 )
4628 .unwrap();
4629 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4630 let away = ConicalSurface::new(
4631 Point3::new(1.0, 0.5, 10.0),
4632 Vec3::new(0.0, 0.0, 1.0),
4633 FRAC_PI_2 - half,
4634 )
4635 .unwrap();
4636 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4637 let step = TAU / 2048.0;
4641 let grazed =
4642 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4643 .unwrap();
4644 let wide =
4645 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4646 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4647 }
4648
4649 #[test]
4650 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4651 use crate::traits::ParametricCurve;
4652 let cone = ConicalSurface::new(
4654 Point3::new(0.0, 0.0, 3.0),
4655 Vec3::new(0.0, 0.0, -1.0),
4656 2.0_f64.atan(),
4657 )
4658 .unwrap();
4659 for (centre, radius) in [
4660 (Point3::new(1.0, 0.8, 1.2), 1.1),
4661 (Point3::new(1.5, 0.0, 0.0), 0.8),
4662 ] {
4663 let ball = SphericalSurface::new(centre, radius).unwrap();
4664 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4665 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4666 let (t0, t1) = curves[0].curve.domain();
4667 for k in 0..=64 {
4668 let p = ParametricCurve::evaluate(
4669 &curves[0].curve,
4670 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4671 );
4672 let on_ball = (p - centre).length() - radius;
4673 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4674 assert!(
4675 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4676 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4677 );
4678 }
4679 }
4680 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4682 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4683 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4684 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4685 }
4686
4687 #[test]
4688 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4689 use crate::traits::ParametricCurve;
4690 let cone = ConicalSurface::new(
4691 Point3::new(0.0, 0.0, 3.0),
4692 Vec3::new(0.0, 0.0, -1.0),
4693 2.0_f64.atan(),
4694 )
4695 .unwrap();
4696 for (centre, radius) in [
4699 (Point3::new(0.5, 0.0, 2.5), 2.0),
4700 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4701 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4702 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4703 ] {
4704 let ball = SphericalSurface::new(centre, radius).unwrap();
4705 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4706 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4707 let (t0, t1) = curves[0].curve.domain();
4708 for k in 0..=4096 {
4709 let p = ParametricCurve::evaluate(
4710 &curves[0].curve,
4711 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4712 );
4713 let on_ball = (p - centre).length() - radius;
4714 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4715 assert!(
4716 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4717 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4718 );
4719 }
4720 }
4721 }
4722
4723 #[test]
4724 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4725 let t = 2.0_f64.atan();
4726 let cone =
4727 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4728 let through_apex =
4730 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4731 .unwrap();
4732 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4733 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4735 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4736 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4737 let pin =
4740 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4741 let tube =
4742 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4743 .unwrap();
4744 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4745 }
4746
4747 #[test]
4748 fn parallel_cone_cylinder_gives_two_exact_branches() {
4749 use crate::traits::ParametricCurve;
4750 let cone = ConicalSurface::new(
4751 Point3::new(-5.45, -36.55, -4.85),
4752 Vec3::new(0.0, 0.0, 1.0),
4753 std::f64::consts::FRAC_PI_4,
4754 )
4755 .unwrap();
4756 let cyl = CylindricalSurface::new(
4757 Point3::new(-8.0, -34.0, -5.0),
4758 Vec3::new(0.0, 0.0, 1.0),
4759 4.45,
4760 )
4761 .unwrap();
4762 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4764 let curves = intersect_analytic_analytic_bounded(
4765 AnalyticSurface::Cone(&cone),
4766 AnalyticSurface::Cylinder(&cyl),
4767 32,
4768 Some(v_hint),
4769 Some((0.0, 2.5)),
4770 )
4771 .unwrap();
4772
4773 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4774 for c in &curves {
4775 let (t0, t1) = c.curve.domain();
4776 for k in 0..=32 {
4777 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4778 let p = ParametricCurve::evaluate(&c.curve, t);
4779 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4781 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4782 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4784 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4785 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4787 }
4788 }
4789 }
4790
4791 #[test]
4792 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4793 use crate::traits::ParametricCurve;
4794 let cone = ConicalSurface::new(
4796 Point3::new(0.0, 0.0, 3.0),
4797 Vec3::new(0.0, 0.0, -1.0),
4798 2.0_f64.atan(),
4799 )
4800 .unwrap();
4801 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4803 let rod =
4804 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4805 .unwrap();
4806 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4807 .unwrap()
4808 .unwrap();
4809 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4810 let (t0, t1) = curves[0].curve.domain();
4811 let (first, last) = (
4812 ParametricCurve::evaluate(&curves[0].curve, t0),
4813 ParametricCurve::evaluate(&curves[0].curve, t1),
4814 );
4815 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4816 for k in 0..=64 {
4817 let p = ParametricCurve::evaluate(
4818 &curves[0].curve,
4819 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4820 );
4821 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4822 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4823 assert!(
4824 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4825 "({x}, {y}): off by {on_rod}, {on_cone}"
4826 );
4827 }
4828 }
4829 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4832 let rod =
4833 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4834 .unwrap();
4835 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4836 .unwrap()
4837 .unwrap();
4838 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4839 }
4840 }
4841
4842 #[test]
4845 fn coaxial_cone_cylinder_defers_to_other_paths() {
4846 let cone = ConicalSurface::new(
4847 Point3::new(0.0, 0.0, 0.0),
4848 Vec3::new(0.0, 0.0, 1.0),
4849 std::f64::consts::FRAC_PI_4,
4850 )
4851 .unwrap();
4852 let cyl =
4853 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4854 .unwrap();
4855 assert!(
4856 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4857 .unwrap()
4858 .is_none()
4859 );
4860 }
4861
4862 #[test]
4863 fn oblique_cone_cylinder_defers_to_other_paths() {
4864 let cone = ConicalSurface::new(
4865 Point3::new(0.0, 0.0, 0.0),
4866 Vec3::new(0.0, 0.0, 1.0),
4867 std::f64::consts::FRAC_PI_4,
4868 )
4869 .unwrap();
4870 let cyl =
4871 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4872 .unwrap();
4873 assert!(
4874 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4875 .unwrap()
4876 .is_none()
4877 );
4878 }
4879
4880 #[test]
4881 fn plane_torus_lobe_closes_and_stays_on_surface() {
4882 use crate::traits::ParametricCurve;
4883 let (major, minor) = (10.0, 3.0);
4884 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4885
4886 for (n, d) in [
4890 (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), ] {
4894 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4895 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4896 for c in &curves {
4897 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4898 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4899 assert!(
4900 (p0 - p1).length() < 1e-7,
4901 "lobe not closed: gap={} (n={n:?} d={d})",
4902 (p0 - p1).length()
4903 );
4904 for k in 0..=64 {
4906 let t = f64::from(k) / 64.0;
4907 let p = ParametricCurve::evaluate(&c.curve, t);
4908 assert!(
4909 torus_implicit(p, major, minor).abs() < 1e-2,
4910 "off-surface point {p:?} implicit={}",
4911 torus_implicit(p, major, minor)
4912 );
4913 }
4914 }
4915 }
4916 }
4917
4918 #[test]
4923 fn plane_torus_inner_tangent_figure_eight_is_two_loops_through_its_node() {
4924 let (major, minor) = (10.0, 3.0);
4925 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4926 let curves =
4927 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4928 assert_eq!(curves.len(), 2, "one loop per lobe");
4929 let node = Point3::new(major - minor, 0.0, 0.0);
4930 let mut sides = Vec::new();
4931 for c in &curves {
4932 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
4933 assert!((first - node).length() < 1e-6, "starts at {first:?}");
4934 assert_eq!(first, last, "closes on the node");
4935 for p in c.points.iter().map(|p| p.point) {
4936 assert!((p.x() - node.x()).abs() < 1e-9, "off the plane at {p:?}");
4937 assert!(
4938 torus_implicit(p, major, minor).abs() < 1e-6,
4939 "off the torus at {p:?}"
4940 );
4941 }
4942 let y: f64 = c.points.iter().map(|p| p.point.y()).sum();
4943 sides.push(y > 0.0);
4944 assert!(
4945 c.points.iter().all(|p| p.point.y() * y >= -1e-9),
4946 "a lobe crosses y = 0"
4947 );
4948 }
4949 assert_ne!(sides[0], sides[1], "the lobes lie either side of y = 0");
4950 }
4951
4952 #[test]
4958 fn a_wall_just_off_the_inner_tangent_cuts_no_node() {
4959 let (major, minor) = (3.0, 1.0);
4960 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4961 for (x, loops) in [(2.0001, 1), (1.9999, 2)] {
4962 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), x).unwrap();
4963 assert_eq!(curves.len(), loops, "x = {x}");
4964 for c in &curves {
4965 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
4966 assert_eq!(first, last, "x = {x}: the loop closes");
4967 for p in c.points.iter().map(|p| p.point) {
4968 assert!((p.x() - x).abs() < 1e-9, "x = {x}: off the wall at {p:?}");
4969 assert!(
4970 torus_implicit(p, major, minor).abs() < 1e-6,
4971 "x = {x}: off the torus at {p:?}"
4972 );
4973 }
4974 }
4975 }
4976 let curves = intersect_plane_torus(&torus, Vec3::new(1.0, 0.0, 0.0), 2.0001).unwrap();
4978 let low = curves[0]
4979 .points
4980 .iter()
4981 .map(|p| p.point)
4982 .filter(|p| p.y().abs() < 1e-6)
4983 .map(Point3::z)
4984 .collect::<Vec<_>>();
4985 let gap = (2.0001_f64 - 2.0)
4986 .mul_add(2.0, -(2.0001_f64 - 2.0).powi(2))
4987 .sqrt();
4988 assert!(
4989 low.iter().any(|z| (z - gap).abs() < 1e-6)
4990 && low.iter().any(|z| (z + gap).abs() < 1e-6),
4991 "turns at {low:?}, not at z = ±{gap}"
4992 );
4993 }
4994
4995 #[test]
4999 fn a_tilted_plane_tangent_inside_the_tube_cuts_two_loops_through_its_node() {
5000 let (major, minor) = (3.0, 1.0);
5001 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5002 for v0 in [1.7_f64, 2.0, 2.5, 4.0] {
5003 let normal = Vec3::new(v0.cos(), 0.0, v0.sin());
5004 let node = torus.evaluate(0.0, v0);
5005 let d = normal.dot(Vec3::new(node.x(), node.y(), node.z()));
5006 let curves = intersect_plane_torus(&torus, normal, d).unwrap();
5007 assert_eq!(curves.len(), 2, "v0 {v0}: one loop per lobe");
5008 for c in &curves {
5009 let (first, last) = (c.points[0].point, c.points[c.points.len() - 1].point);
5010 assert!(
5011 (first - node).length() < 1e-6,
5012 "v0 {v0}: starts at {first:?}, not {node:?}"
5013 );
5014 assert_eq!(first, last, "v0 {v0}: closes on the node");
5015 for p in c.points.iter().map(|p| p.point) {
5016 let off = normal.dot(Vec3::new(p.x(), p.y(), p.z())) - d;
5017 assert!(off.abs() < 1e-9, "v0 {v0}: off the plane at {p:?}");
5018 assert!(
5019 torus_implicit(p, major, minor).abs() < 1e-6,
5020 "v0 {v0}: off the torus at {p:?}"
5021 );
5022 }
5023 }
5024 }
5025 }
5026
5027 #[test]
5032 fn plane_torus_wall_sections_close_into_their_loops() {
5033 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
5034 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5035 for k in 1..200 {
5036 let (d, want) = match k.cmp(&100) {
5037 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
5039 std::cmp::Ordering::Greater => (
5041 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
5042 1,
5043 ),
5044 std::cmp::Ordering::Equal => continue,
5045 };
5046 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
5047 let closed = loops
5048 .iter()
5049 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
5050 .count();
5051 assert_eq!(
5052 (loops.len(), closed),
5053 (want, want),
5054 "R {major} r {minor}, wall at {d}"
5055 );
5056 }
5057 }
5058 }
5059
5060 #[test]
5064 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
5065 let (major, minor) = (4.0, 1.5);
5066 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
5067 for tilt in [0.03_f64, 0.08, 0.2] {
5068 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
5069 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
5070 assert_eq!(curves.len(), 2, "tilt {tilt}");
5071 for c in &curves {
5072 let (t0, t1) = c.curve.domain();
5073 let off = (0..=400)
5074 .map(|k| {
5075 let p = c
5076 .curve
5077 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
5078 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
5079 })
5080 .fold(0.0_f64, |m, e| m.max(e.abs()));
5081 assert!(
5082 off < 1e-6,
5083 "tilt {tilt}: fitted section {off} off the torus"
5084 );
5085 }
5086 }
5087 }
5088
5089 #[test]
5090 fn line_torus_box_edge_crossing_is_exact() {
5091 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
5094 let ts = intersect_line_torus(
5095 &torus,
5096 Point3::new(6.0, -4.0, -5.0),
5097 Vec3::new(0.0, 0.0, 1.0),
5098 );
5099 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
5101 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
5102 let rho = 6.0_f64.hypot(4.0);
5103 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
5104 assert!(
5105 (zs[0] - (-z_exp)).abs() < 1e-9,
5106 "z0={} exp={}",
5107 zs[0],
5108 -z_exp
5109 );
5110 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
5111 for &t in &ts {
5113 let p = Point3::new(6.0, -4.0, -5.0 + t);
5114 let rho = p.x().hypot(p.y());
5115 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
5116 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
5117 }
5118 }
5119
5120 #[test]
5125 fn line_spindle_torus_roots_lie_on_their_tubes() {
5126 let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
5127 let origin = Point3::new(42.36, 18.31, 3.4);
5128 let dir = Vec3::new(
5129 0.573_576_436_351_046,
5130 0.740_535_693_464_567_5,
5131 0.350_889_803_483_932_2,
5132 );
5133 let ts = intersect_line_torus(&torus, origin, dir);
5134 let tube = |t: f64, across: f64| {
5135 let q = origin + dir * t;
5136 ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
5137 };
5138 for &t in &ts {
5139 assert!(
5140 tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
5141 "root {t} off both tubes in {ts:?}"
5142 );
5143 }
5144 for want in [0.124, 0.398] {
5145 assert!(
5146 ts.iter().any(|&t| (t - want).abs() < 1e-3),
5147 "no root near {want} in {ts:?}"
5148 );
5149 }
5150 }
5151
5152 #[test]
5153 fn line_torus_miss_and_tangent() {
5154 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
5155 let miss = intersect_line_torus(
5157 &torus,
5158 Point3::new(20.0, 0.0, 0.0),
5159 Vec3::new(0.0, 0.0, 1.0),
5160 );
5161 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
5162 let axis =
5164 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
5165 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
5166 }
5167
5168 #[test]
5169 fn dispatch_via_analytic_surface() {
5170 let cyl =
5171 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5172 .unwrap();
5173 let curves = intersect_plane_analytic(
5174 AnalyticSurface::Cylinder(&cyl),
5175 Vec3::new(0.0, 0.0, 1.0),
5176 0.0,
5177 )
5178 .unwrap();
5179 assert!(!curves.is_empty());
5180 }
5181
5182 #[test]
5183 fn perpendicular_cylinders_intersect() {
5184 let cyl_z =
5185 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5186 .unwrap();
5187 let cyl_x =
5188 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
5189 .unwrap();
5190
5191 let curves = intersect_analytic_analytic(
5192 AnalyticSurface::Cylinder(&cyl_z),
5193 AnalyticSurface::Cylinder(&cyl_x),
5194 16,
5195 )
5196 .unwrap();
5197
5198 assert!(
5199 !curves.is_empty(),
5200 "perpendicular cylinders should intersect"
5201 );
5202
5203 for c in &curves {
5204 assert!(
5205 c.points.len() >= 2,
5206 "intersection curve should have >= 2 points, got {}",
5207 c.points.len()
5208 );
5209 }
5210 }
5211
5212 #[test]
5215 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
5216 let cyl_z =
5217 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5218 .unwrap();
5219 let cyl_x =
5220 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
5221 .unwrap();
5222 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5223 .unwrap()
5224 .unwrap();
5225 assert_eq!(curves.len(), 1);
5226 let curve = &curves[0].curve;
5227 let (t0, t1) = curve.domain();
5228 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
5229 let off = |p: Point3| {
5230 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
5231 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
5232 on_z.max(on_x)
5233 };
5234 let worst = (0..=400)
5235 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
5236 .fold(0.0, f64::max);
5237 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
5238 }
5239
5240 #[test]
5244 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
5245 let cyl_z =
5246 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5247 .unwrap();
5248 let cyl_x =
5249 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
5250 .unwrap();
5251 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
5252 .unwrap()
5253 .expect("the thin cylinder's sweep finds the loop");
5254 assert_eq!(curves.len(), 1);
5255 }
5256
5257 #[test]
5258 fn sphere_cylinder_intersect() {
5259 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5260 let cyl =
5261 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5262 .unwrap();
5263
5264 let curves = intersect_analytic_analytic(
5265 AnalyticSurface::Sphere(&sphere),
5266 AnalyticSurface::Cylinder(&cyl),
5267 16,
5268 )
5269 .unwrap();
5270
5271 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
5275 }
5276
5277 #[test]
5278 fn exact_sphere_cylinder_coaxial_two_circles() {
5279 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5282 let cyl =
5283 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5284 .unwrap();
5285 let circles = exact_sphere_cylinder(&sphere, &cyl)
5286 .unwrap()
5287 .expect("coaxial case returns Some");
5288 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
5289 let mut zs: Vec<f64> = circles
5290 .iter()
5291 .filter_map(|c| match c {
5292 ExactIntersectionCurve::Circle(circle) => {
5293 assert!(
5294 (circle.radius() - 3.0).abs() < 1e-9,
5295 "rim radius == cyl radius"
5296 );
5297 Some(circle.center().z())
5298 }
5299 _ => None,
5300 })
5301 .collect();
5302 assert_eq!(zs.len(), 2, "both sections must be exact circles");
5303 zs.sort_by(f64::total_cmp);
5304 let z = 27.0_f64.sqrt();
5305 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
5306 }
5307
5308 #[test]
5309 fn exact_sphere_cylinder_non_coaxial_defers() {
5310 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
5312 let cyl =
5313 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
5314 .unwrap();
5315 assert!(
5316 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
5317 "non-coaxial sphere/cylinder defers to the marcher"
5318 );
5319 }
5320
5321 #[test]
5322 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
5323 let cone = ConicalSurface::new(
5325 Point3::new(0.0, 0.0, 3.0),
5326 Vec3::new(0.0, 0.0, -1.0),
5327 2.0_f64.atan(),
5328 )
5329 .unwrap();
5330 for (height, radius, count) in [
5331 (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), ] {
5337 let centre = Point3::new(0.0, 0.0, height);
5338 let ball = SphericalSurface::new(centre, radius).unwrap();
5339 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
5340 let circles = circles_of(&curves);
5341 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
5342 for circle in circles {
5343 for k in 0..16 {
5344 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5345 let on_ball = (p - centre).length() - radius;
5346 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
5347 assert!(
5348 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
5349 "ball at {height}: off by {on_ball}, {on_cone}"
5350 );
5351 }
5352 }
5353 }
5354 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
5355 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
5356 let wide =
5359 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
5360 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
5361 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
5362 let circles = circles_of(&curves);
5363 assert_eq!(circles.len(), 1, "the touch");
5364 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
5365 assert!(
5366 (circles[0].radius() - touch).abs() < 1e-3,
5367 "{}",
5368 circles[0].radius()
5369 );
5370 }
5371
5372 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5374 curves
5375 .iter()
5376 .filter_map(|c| match c {
5377 ExactIntersectionCurve::Circle(circle) => Some(circle),
5378 _ => None,
5379 })
5380 .collect()
5381 }
5382
5383 fn worst_off(
5386 circles: &[&Circle3D],
5387 torus: &ToroidalSurface,
5388 other: impl Fn(Point3) -> f64,
5389 ) -> f64 {
5390 let mut worst = 0.0_f64;
5391 for circle in circles {
5392 for k in 0..16 {
5393 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5394 let q = p - torus.center();
5395 let along = q.dot(torus.z_axis());
5396 let rho = (q - torus.z_axis() * along).length();
5397 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5398 worst = worst.max(off).max(other(p).abs());
5399 }
5400 }
5401 worst
5402 }
5403
5404 #[test]
5405 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5406 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5407 for height in [0.0, 1.0] {
5408 let centre = Point3::new(0.0, 0.0, height);
5409 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5410 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5411 let circles = circles_of(&curves);
5412 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5413 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5414 assert!(worst < 1e-9, "height {height}: {worst}");
5415 }
5416 }
5417
5418 #[test]
5419 fn exact_sphere_torus_misses_touches_and_defers() {
5420 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5421 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5422 assert!(
5423 exact_sphere_torus(&ball(0.0, 1.0), &torus)
5424 .unwrap()
5425 .unwrap()
5426 .is_empty(),
5427 "a small ball in the hole misses"
5428 );
5429 assert!(
5430 exact_sphere_torus(&ball(0.0, 2.5), &torus)
5431 .unwrap()
5432 .is_none(),
5433 "a ball touching the inner equator defers"
5434 );
5435 assert!(
5436 exact_sphere_torus(&ball(1.0, 3.0), &torus)
5437 .unwrap()
5438 .is_none(),
5439 "a ball off the axis defers"
5440 );
5441 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5442 Point3::new(0.0, 0.0, 0.0),
5443 1.0,
5444 2.0,
5445 Vec3::new(0.0, 0.0, 1.0),
5446 Vec3::new(1.0, 0.0, 0.0),
5447 )
5448 .unwrap();
5449 assert!(
5450 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5451 .unwrap()
5452 .is_none()
5453 );
5454 }
5455
5456 #[test]
5457 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5458 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5459 let z = Vec3::new(0.0, 0.0, 1.0);
5460 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5461 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5462 let circles = circles_of(&curves);
5463 assert_eq!((curves.len(), circles.len()), (2, 2));
5464 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5465 assert!(worst < 1e-9, "{worst}");
5466 assert!(
5467 exact_cylinder_torus(&rod(2.0), &torus)
5468 .unwrap()
5469 .unwrap()
5470 .is_empty(),
5471 "a rod clear in the hole misses"
5472 );
5473 for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5474 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5475 let circles = circles_of(&curves);
5476 assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5477 assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5478 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5479 assert!(worst < 1e-9, "{label}: {worst}");
5480 }
5481 assert!(
5482 exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5483 .unwrap()
5484 .unwrap()
5485 .is_empty(),
5486 "a wall clear of the tube by more than the tolerance misses it"
5487 );
5488 assert_eq!(
5489 exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5490 .unwrap()
5491 .unwrap()
5492 .len(),
5493 2,
5494 "a wall into the tube by more than the tolerance crosses it twice"
5495 );
5496 for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5500 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5501 assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5502 }
5503 let tilted =
5504 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5505 .unwrap();
5506 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5507 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5508 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5509 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5510 Point3::new(0.0, 0.0, 0.0),
5511 1.0,
5512 2.0,
5513 z,
5514 Vec3::new(1.0, 0.0, 0.0),
5515 )
5516 .unwrap();
5517 assert!(
5518 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5519 "a spindle torus's inner lemon also meets the rod"
5520 );
5521 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5524 Point3::new(0.0, 0.0, 4.7),
5525 0.1,
5526 2.45,
5527 z,
5528 Vec3::new(1.0, 0.0, 0.0),
5529 )
5530 .unwrap();
5531 let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5532 let circles = circles_of(&curves);
5533 assert_eq!(circles.len(), 1);
5534 assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5535 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5536 assert!(worst < 1e-9, "{worst}");
5537 let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5538 let circles = circles_of(&curves);
5539 assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5540 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5541 assert!(worst < 1e-9, "{worst}");
5542 }
5543
5544 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5547 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5548 let cyl =
5549 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5550 .unwrap();
5551 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5552 .unwrap()
5553 .unwrap();
5554 let mut worst: f64 = 0.0;
5555 for c in &curves {
5556 for ip in &c.points {
5557 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5558 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5559 worst = worst
5560 .max((on_sphere - ip.point).length())
5561 .max((on_cylinder - ip.point).length());
5562 }
5563 let (t0, t1) = c.curve.domain();
5564 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5565 for k in 0..=400 {
5566 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5567 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5568 let on_cylinder = ((p.x() - cylinder_origin.x())
5569 .hypot(p.y() - cylinder_origin.y())
5570 - cylinder_radius)
5571 .abs();
5572 worst = worst.max(on_sphere).max(on_cylinder);
5573 }
5574 }
5575 (curves.len(), worst)
5576 }
5577
5578 #[test]
5581 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5582 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5583 assert_eq!(count, 2);
5584 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5585 }
5586
5587 #[test]
5589 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5590 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5591 assert_eq!(count, 1);
5592 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5593 }
5594
5595 #[test]
5596 fn disjoint_cylinders_no_intersection() {
5597 let cyl_a =
5598 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5599 .unwrap();
5600 let cyl_b =
5601 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5602 .unwrap();
5603
5604 let curves = intersect_analytic_analytic(
5605 AnalyticSurface::Cylinder(&cyl_a),
5606 AnalyticSurface::Cylinder(&cyl_b),
5607 16,
5608 )
5609 .unwrap();
5610
5611 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5612 }
5613
5614 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5618 use crate::traits::ParametricCurve;
5619 match curve {
5620 ExactIntersectionCurve::Circle(c) => (0..=64)
5621 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5622 .collect(),
5623 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5624 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5625 .collect(),
5626 ExactIntersectionCurve::Points(pts) => pts.clone(),
5627 }
5628 }
5629
5630 fn assert_on_plane_and_cone(
5633 curves: &[ExactIntersectionCurve],
5634 cone: &ConicalSurface,
5635 n: Vec3,
5636 d: f64,
5637 z_bound: (f64, f64),
5638 ) {
5639 assert!(!curves.is_empty(), "expected at least one section curve");
5640 let mut total = 0;
5641 for curve in curves {
5642 for p in collect_points(curve) {
5643 total += 1;
5644 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5645 assert!(
5646 plane_err < 1e-9,
5647 "point off plane by {plane_err:.2e}: {p:?}"
5648 );
5649 let (u, v) = cone.project_point(p);
5650 let q = cone.evaluate(u, v);
5651 let cone_err =
5652 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5653 .sqrt();
5654 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5655 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5656 assert!(
5657 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5658 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5659 p.z()
5660 );
5661 }
5662 }
5663 assert!(total >= 8, "too few section points ({total})");
5664 }
5665
5666 #[test]
5667 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5668 let cone = ConicalSurface::new(
5672 Point3::new(0.0, 0.0, 0.0),
5673 Vec3::new(0.0, 0.0, 1.0),
5674 std::f64::consts::FRAC_PI_4,
5675 )
5676 .unwrap();
5677 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5678 let d = n.z() * 5.0;
5680 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5681 assert!(
5682 curves
5683 .iter()
5684 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5685 "oblique steep plane × cone must yield an exact Ellipse"
5686 );
5687 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5689 }
5690
5691 #[test]
5692 fn oblique_plane_cone_wrong_nappe_is_empty() {
5693 let cone = ConicalSurface::new(
5697 Point3::new(0.0, 0.0, 0.0),
5698 Vec3::new(0.0, 0.0, 1.0),
5699 std::f64::consts::FRAC_PI_4,
5700 )
5701 .unwrap();
5702 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5703 let d = n.z() * -5.0;
5704 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5705 assert!(
5706 curves.is_empty(),
5707 "plane on the phantom-nappe side must yield no real curve, got {}",
5708 curves.len()
5709 );
5710 }
5711
5712 #[test]
5713 fn oblique_plane_cone_parabola_on_both_single_branch() {
5714 let cone = ConicalSurface::new(
5717 Point3::new(0.0, 0.0, 0.0),
5718 Vec3::new(0.0, 0.0, 1.0),
5719 std::f64::consts::FRAC_PI_4,
5720 )
5721 .unwrap();
5722 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5723 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5725 assert_eq!(
5726 curves.len(),
5727 1,
5728 "a parabola is a single branch, got {}",
5729 curves.len()
5730 );
5731 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5733 }
5734
5735 #[test]
5736 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5737 let cone = ConicalSurface::new(
5745 Point3::new(-59.0, -59.0, 15.85),
5746 Vec3::new(0.0, 0.0, -1.0),
5747 std::f64::consts::FRAC_PI_4,
5748 )
5749 .unwrap();
5750 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5751 let d = -58.360_56;
5752 let cos_theta = n.dot(cone.axis()).abs();
5753 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5754 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5755 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5758 for c in &curves {
5760 assert!(
5761 matches!(c, ExactIntersectionCurve::Points(_)),
5762 "hyperbola must be sampled Points, not a closed conic"
5763 );
5764 }
5765 }
5766
5767 #[test]
5771 fn sphere_sphere_meets_in_a_circle_on_both() {
5772 let ball = |x: f64, y: f64, z: f64, r: f64| {
5773 SphericalSurface::new(Point3::new(x, y, z), r).unwrap()
5774 };
5775 let (a, b) = (ball(0.0, 0.0, 0.0, 2.0), ball(1.6, 0.3, 0.2, 1.5));
5776 let curves = exact_sphere_sphere(&a, &b).unwrap().unwrap();
5777 assert_eq!(curves.len(), 1);
5778 let circle = match &curves[0] {
5779 ExactIntersectionCurve::Circle(circle) => circle,
5780 other => unreachable!("expected a circle, got {other:?}"),
5781 };
5782 for k in 0..16 {
5783 let p = circle.evaluate(std::f64::consts::TAU * f64::from(k) / 16.0);
5784 assert!(((p - a.center()).length() - 2.0).abs() < 1e-12);
5785 assert!(((p - b.center()).length() - 1.5).abs() < 1e-12);
5786 }
5787 assert!(
5788 exact_sphere_sphere(&a, &ball(4.0, 0.0, 0.0, 1.5))
5789 .unwrap()
5790 .unwrap()
5791 .is_empty()
5792 );
5793 assert!(
5794 exact_sphere_sphere(&a, &ball(0.2, 0.0, 0.0, 1.0))
5795 .unwrap()
5796 .unwrap()
5797 .is_empty()
5798 );
5799 assert!(
5800 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 1.0))
5801 .unwrap()
5802 .unwrap()
5803 .is_empty()
5804 );
5805 assert!(
5806 exact_sphere_sphere(&a, &ball(0.0, 0.0, 0.0, 2.0))
5807 .unwrap()
5808 .is_none()
5809 );
5810
5811 let radius = |p: &SphericalSurface, q: &SphericalSurface| -> f64 {
5814 match exact_sphere_sphere(p, q).unwrap().unwrap().as_slice() {
5815 [ExactIntersectionCurve::Circle(c)] => c.radius(),
5816 other => unreachable!("expected one circle, got {other:?}"),
5817 }
5818 };
5819 let tiny = ball(2.0, 0.0, 0.0, 1e-5);
5820 assert!((radius(&a, &tiny) - 1e-5).abs() < 1e-9);
5821 assert!((radius(&tiny, &a) - 1e-5).abs() < 1e-9);
5822 let near = radius(&ball(0.0, 0.0, 0.0, 2.0), &ball(3.5, 0.0, 0.0, 1.500_001));
5823 let far = radius(
5824 &ball(1e10, 0.0, 0.0, 2.0),
5825 &ball(1e10 + 3.5, 0.0, 0.0, 1.500_001),
5826 );
5827 assert!(near > 1e-3);
5828 assert!((far - near).abs() < 1e-9 * near.max(1.0));
5829 }
5830
5831 #[allow(clippy::panic)]
5834 fn lines_of(sections: &[PlaneSection]) -> Vec<(Line3D, bool)> {
5835 sections
5836 .iter()
5837 .map(|s| match s {
5838 PlaneSection::Line { line, ray } => (line.clone(), *ray),
5839 other => panic!("expected a line, got {other:?}"),
5840 })
5841 .collect()
5842 }
5843
5844 #[test]
5849 fn a_plane_along_a_cylinder_cuts_its_rulings() {
5850 let cyl =
5851 CylindricalSurface::new(Point3::new(0.0, 0.0, -2.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
5852 .unwrap();
5853 let y = Vec3::new(0.0, 1.0, 0.0);
5854 let surface = AnalyticSurface::Cylinder(&cyl);
5855 let on_both = |p: Point3, d: f64| {
5856 assert!((p.y() - d).abs() < 1e-12, "{p:?} off the plane y = {d}");
5857 assert!(
5858 (p.x().hypot(p.y()) - 1.0).abs() < 1e-12,
5859 "{p:?} off the cylinder"
5860 );
5861 };
5862 for (d, count) in [(0.0, 2), (1e-6_f64.cos(), 2), (1.0, 1), (1.5, 0)] {
5863 assert!(exact_plane_analytic(surface, y, d).unwrap().is_empty());
5864 let lines = lines_of(&exact_plane_section(surface, y, d).unwrap());
5865 assert_eq!(lines.len(), count, "plane y = {d}");
5866 let mut xs = Vec::new();
5867 for (line, ray) in lines {
5868 assert!(!ray);
5869 assert!(line.direction().cross(cyl.axis()).length() < 1e-12);
5870 for t in [-3.0, 0.0, 3.0] {
5871 on_both(line.evaluate(t), d);
5872 }
5873 xs.push(line.evaluate(0.0).x());
5874 }
5875 if count == 2 {
5876 let apart = (xs[0] - xs[1]).abs();
5877 let want = 2.0 * (1.0 - d * d).sqrt();
5878 assert!(
5879 (apart - want).abs() < 1e-9 * want.max(1e-6),
5880 "{apart} apart"
5881 );
5882 }
5883 }
5884 }
5885
5886 #[test]
5891 #[allow(clippy::panic)]
5892 fn a_plane_through_a_cone_apex_cuts_its_rulings() {
5893 use std::f64::consts::FRAC_PI_4;
5894
5895 let cone = ConicalSurface::new(
5896 Point3::new(1.0, 2.0, 3.0),
5897 Vec3::new(0.0, 0.0, 1.0),
5898 FRAC_PI_4,
5899 )
5900 .unwrap();
5901 let surface = AnalyticSurface::Cone(&cone);
5902 let apex = cone.apex();
5903 let through = |n: Vec3| {
5904 let n = n.normalize().unwrap();
5905 let d = n.dot(apex - Point3::new(0.0, 0.0, 0.0));
5906 assert!(exact_plane_analytic(surface, n, d).unwrap().is_empty());
5907 (n, d, exact_plane_section(surface, n, d).unwrap())
5908 };
5909 for (n, count) in [
5910 (Vec3::new(1.0, 0.0, 0.0), 2),
5911 (Vec3::new(1.0, 0.0, -1.0), 1),
5912 ] {
5913 let (n, d, sections) = through(n);
5914 let rays = lines_of(§ions);
5915 assert_eq!(rays.len(), count, "plane normal {n:?}");
5916 for (line, ray) in rays {
5917 assert!(ray);
5918 assert!((line.evaluate(0.0) - apex).length() < 1e-12);
5919 let p = line.evaluate(2.0);
5920 assert!((n.dot(p - Point3::new(0.0, 0.0, 0.0)) - d).abs() < 1e-12);
5921 let rel = p - apex;
5922 assert!(rel.z() > 0.0, "{p:?} on the phantom nappe");
5923 assert!(
5924 (rel.x().hypot(rel.y()) - rel.z()).abs() < 1e-12,
5925 "{p:?} off the cone"
5926 );
5927 }
5928 }
5929 for n in [Vec3::new(0.1, 0.0, 1.0), Vec3::new(0.0, 0.0, 1.0)] {
5930 let (_, _, sections) = through(n);
5931 assert!(
5932 matches!(sections[..], [PlaneSection::Point(p)] if (p - apex).length() < 1e-12),
5933 "{sections:?}"
5934 );
5935 }
5936
5937 let upright = ConicalSurface::new(
5938 Point3::new(0.0, 0.0, 0.0),
5939 Vec3::new(0.0, 0.0, 1.0),
5940 FRAC_PI_4,
5941 )
5942 .unwrap();
5943 let n = Vec3::new((0.5 - 1e-8_f64).sqrt(), 0.0, (0.5 + 1e-8_f64).sqrt());
5944 let sections = exact_plane_section(AnalyticSurface::Cone(&upright), n, 5e-8).unwrap();
5945 let [PlaneSection::Ellipse(ellipse)] = §ions[..] else {
5946 panic!("{sections:?}");
5947 };
5948 assert!(
5949 ellipse.semi_major() > 1.0,
5950 "semi-major {}",
5951 ellipse.semi_major()
5952 );
5953 }
5954
5955 #[test]
5958 #[allow(clippy::panic)]
5959 fn a_plane_touching_a_sphere_meets_it_at_a_point() {
5960 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 1.0), 1.0).unwrap();
5961 let surface = AnalyticSurface::Sphere(&sphere);
5962 let z = Vec3::new(0.0, 0.0, 1.0);
5963 assert!(exact_plane_analytic(surface, z, 0.0).unwrap().is_empty());
5964 let sections = exact_plane_section(surface, z, 0.0).unwrap();
5965 assert!(
5966 matches!(sections[..], [PlaneSection::Point(p)] if p.z().abs() < 1e-15 && p.x() == 0.0),
5967 "{sections:?}"
5968 );
5969 assert!(exact_plane_section(surface, z, -1e-9).unwrap().is_empty());
5970 for height in [5e-15, 1e-12, 1e-8] {
5971 let sections = exact_plane_section(surface, z, height).unwrap();
5972 let [PlaneSection::Circle(circle)] = §ions[..] else {
5973 panic!("{sections:?}");
5974 };
5975 let want = (height * (2.0 - height)).sqrt();
5976 assert!(
5977 (circle.radius() - want).abs() < 1e-3 * want,
5978 "circle at z = {height}: radius {}, want {want}",
5979 circle.radius()
5980 );
5981 assert!((circle.center().z() - height).abs() < 1e-15);
5982 }
5983 }
5984
5985 fn farthest(curves: &[IntersectionCurve], off: impl Fn(Point3) -> f64) -> f64 {
5987 let mut far = 0.0_f64;
5988 for ic in curves {
5989 let (t0, t1) = ic.curve.domain();
5990 for i in 0..=2000 {
5991 let t = t0 + (t1 - t0) * f64::from(i) / 2000.0;
5992 far = far.max(off(ic.curve.evaluate(t)));
5993 }
5994 }
5995 far
5996 }
5997
5998 #[test]
6001 fn a_ball_in_its_bore_meets_it_on_an_exact_circle() {
6002 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
6003 let bore =
6004 CylindricalSurface::new(Point3::new(0.0, 0.0, -2.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
6005 .unwrap();
6006 let curves = intersect_analytic_analytic_bounded(
6007 AnalyticSurface::Sphere(&ball),
6008 AnalyticSurface::Cylinder(&bore),
6009 64,
6010 None,
6011 Some((0.0, 4.0)),
6012 )
6013 .unwrap();
6014 assert_eq!(curves.len(), 1);
6015 let far = farthest(&curves, |p| {
6016 let on_ball = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 1.0).abs();
6017 on_ball
6018 .max((p.x().hypot(p.y()) - 1.0).abs())
6019 .max(p.z().abs())
6020 });
6021 assert!(far < 1e-14, "circle strays {far:.2e}");
6022 }
6023
6024 #[test]
6027 fn overlapping_balls_meet_on_an_exact_circle() {
6028 let a = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
6029 let b = SphericalSurface::new(Point3::new(1.0, 0.0, 0.0), 1.0).unwrap();
6030 let curves = intersect_analytic_analytic_bounded(
6031 AnalyticSurface::Sphere(&a),
6032 AnalyticSurface::Sphere(&b),
6033 64,
6034 None,
6035 None,
6036 )
6037 .unwrap();
6038 assert_eq!(curves.len(), 1);
6039 let far = farthest(&curves, |p| {
6040 let on_a = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 1.0).abs();
6041 on_a.max(((p - Point3::new(1.0, 0.0, 0.0)).length() - 1.0).abs())
6042 });
6043 assert!(far < 1e-14, "circle strays {far:.2e}");
6044 }
6045
6046 #[test]
6049 fn a_conic_loop_is_its_ellipse() {
6050 let centre = Point3::new(1.0, -2.0, 0.5);
6051 let frame = Frame3::from_normal(centre, Vec3::new(0.3, -0.4, 1.0)).unwrap();
6052 let curve = conic_loop(centre, frame.x, frame.y, 3.0, 1.5).unwrap();
6053 let (t0, t1) = curve.domain();
6054 assert!((curve.evaluate(t0) - (centre + frame.x * 3.0)).length() < 1e-15);
6055 for i in 0..=2000 {
6056 let p = curve.evaluate(t0 + (t1 - t0) * f64::from(i) / 2000.0) - centre;
6057 let (x, y) = (p.dot(frame.x) / 3.0, p.dot(frame.y) / 1.5);
6058 assert!(
6059 (x.hypot(y) - 1.0).abs() < 1e-14,
6060 "off its ellipse by {}",
6061 x.hypot(y) - 1.0
6062 );
6063 assert!(p.dot(frame.z).abs() < 1e-14);
6064 }
6065 }
6066}