1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::aabb::Aabb3;
11use crate::curves::{Circle3D, Ellipse3D};
12use crate::frame::Frame3;
13use crate::nurbs::curve::NurbsCurve;
14use crate::nurbs::fitting::interpolate;
15use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
16use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
17use crate::tolerance::Tolerance;
18use crate::vec::{Point3, Vec3};
19
20#[derive(Debug, Clone)]
22pub enum ExactIntersectionCurve {
23 Circle(Circle3D),
25 Ellipse(Ellipse3D),
27 Points(Vec<Point3>),
29}
30
31pub fn exact_plane_analytic(
42 surface: AnalyticSurface<'_>,
43 plane_normal: Vec3,
44 plane_d: f64,
45) -> Result<Vec<ExactIntersectionCurve>, MathError> {
46 exact_plane_analytic_reaching(surface, plane_normal, plane_d, 0.0)
47}
48
49pub fn exact_plane_analytic_reaching(
57 surface: AnalyticSurface<'_>,
58 plane_normal: Vec3,
59 plane_d: f64,
60 reach: f64,
61) -> Result<Vec<ExactIntersectionCurve>, MathError> {
62 match surface {
63 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
64 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
65 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d, reach),
66 AnalyticSurface::Torus(torus) => {
67 if let Some(circles) = exact_plane_torus(torus, plane_normal, plane_d)? {
68 return Ok(circles);
69 }
70 if let Some(loops) = plane_torus_winding_loops(torus, plane_normal, plane_d, 128) {
71 return Ok(loops
72 .into_iter()
73 .map(ExactIntersectionCurve::Points)
74 .collect());
75 }
76 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
78 Ok(chains
79 .into_iter()
80 .map(ExactIntersectionCurve::Points)
81 .collect())
82 }
83 }
84}
85
86fn exact_plane_torus(
100 torus: &ToroidalSurface,
101 normal: Vec3,
102 d: f64,
103) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
104 let len = normal.length();
105 let n = normal.normalize()?;
106 let d = d / len;
107 let axis = torus.z_axis();
108 let center = torus.center();
109 let (big, small) = (torus.major_radius(), torus.minor_radius());
110 let height = d - dot_np(n, center);
111 let along = n.dot(axis);
112 if along.abs() > 1.0 - 1e-10 {
113 if height.abs() >= small - 1e-10 * small {
114 if height.abs() > small + 1e-10 * small {
115 return Ok(Some(Vec::new()));
116 }
117 if big <= 1e-10 * small
124 || axis.cross(n).length() > 1e-12
125 || (small - height.abs()).abs() > crate::tolerance::Tolerance::default().linear
126 {
127 return Ok(None);
128 }
129 let middle = center + n * height;
130 return Ok(Some(vec![ExactIntersectionCurve::Circle(Circle3D::new(
131 middle, n, big,
132 )?)]));
133 }
134 let reach = small.mul_add(small, -(height * height)).sqrt();
135 if big - reach <= 1e-10 * big {
136 return Ok(None);
137 }
138 let middle = center + n * height;
139 return Ok(Some(vec![
140 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
141 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
142 ]));
143 }
144 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
145 let out = axis.cross(n).normalize()?;
146 return Ok(Some(vec![
147 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
148 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
149 ]));
150 }
151 Ok(None)
152}
153
154fn exact_plane_cylinder(
160 cyl: &CylindricalSurface,
161 normal: Vec3,
162 d: f64,
163) -> Result<Vec<ExactIntersectionCurve>, MathError> {
164 let axis = cyl.axis();
165 let cos_theta = normal.dot(axis).abs();
166 let r = cyl.radius();
167
168 if cos_theta < 1e-10 {
169 let chains = sample_plane_cylinder(cyl, normal, d)?;
172 return Ok(chains
173 .into_iter()
174 .map(ExactIntersectionCurve::Points)
175 .collect());
176 }
177
178 let n_dot_axis = normal.dot(axis);
181 let n_dot_origin = dot_np(normal, cyl.origin());
182 let t = (d - n_dot_origin) / n_dot_axis;
183 let center_on_axis = Point3::new(
184 cyl.origin().x() + t * axis.x(),
185 cyl.origin().y() + t * axis.y(),
186 cyl.origin().z() + t * axis.z(),
187 );
188
189 if cos_theta > 1.0 - 1e-10 {
190 let circle = Circle3D::new(center_on_axis, normal, r)?;
192 Ok(vec![ExactIntersectionCurve::Circle(circle)])
193 } else {
194 let semi_minor = r;
198 let semi_major = r / cos_theta;
199
200 let axis_proj = Vec3::new(
204 axis.x() - n_dot_axis * normal.x(),
205 axis.y() - n_dot_axis * normal.y(),
206 axis.z() - n_dot_axis * normal.z(),
207 );
208 let u_axis = axis_proj.normalize()?;
209 let v_axis = normal.cross(u_axis);
210
211 let ellipse = Ellipse3D::with_axes(
212 center_on_axis,
213 normal,
214 semi_major,
215 semi_minor,
216 u_axis,
217 v_axis,
218 )?;
219 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
220 }
221}
222
223fn exact_plane_sphere(
227 sphere: &SphericalSurface,
228 normal: Vec3,
229 d: f64,
230) -> Result<Vec<ExactIntersectionCurve>, MathError> {
231 let h = dot_np(normal, sphere.center()) - d;
232 let r = sphere.radius();
233
234 if h.abs() > r - 1e-10 {
235 return Ok(vec![]);
236 }
237
238 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
239 let circle_center = Point3::new(
240 h.mul_add(-normal.x(), sphere.center().x()),
241 h.mul_add(-normal.y(), sphere.center().y()),
242 h.mul_add(-normal.z(), sphere.center().z()),
243 );
244
245 let circle = Circle3D::new(circle_center, normal, circle_r)?;
246 Ok(vec![ExactIntersectionCurve::Circle(circle)])
247}
248
249fn exact_plane_cone(
258 cone: &ConicalSurface,
259 normal: Vec3,
260 d: f64,
261 reach: f64,
262) -> Result<Vec<ExactIntersectionCurve>, MathError> {
263 let axis = cone.axis();
264 let cos_theta = normal.dot(axis).abs();
265 let half_angle = cone.half_angle();
266
267 if cos_theta > 1.0 - 1e-10 {
268 let n_dot_axis = normal.dot(axis);
271 let n_dot_apex = dot_np(normal, cone.apex());
272 let t = (d - n_dot_apex) / n_dot_axis;
273
274 if t.abs() < 1e-10 {
279 return Ok(vec![]);
280 }
281
282 let center = Point3::new(
283 cone.apex().x() + t * axis.x(),
284 cone.apex().y() + t * axis.y(),
285 cone.apex().z() + t * axis.z(),
286 );
287 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
291 if circle_r < 1e-15 {
292 return Ok(vec![]);
293 }
294
295 let circle = Circle3D::new(center, normal, circle_r)?;
296 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
297 }
298
299 let c = normal.dot(axis);
311 let p2 = (1.0 - c * c).max(0.0);
312 let p = p2.sqrt();
313 let k = half_angle.sin().powi(2);
314 let a_coeff = p2 - k;
315
316 let m = Vec3::new(
318 axis.x() - c * normal.x(),
319 axis.y() - c * normal.y(),
320 axis.z() - c * normal.z(),
321 );
322 let m_len = m.length();
323 if m_len < 1e-12 {
324 let chains = sample_plane_cone(cone, normal, d, reach)?;
327 return Ok(chains
328 .into_iter()
329 .map(ExactIntersectionCurve::Points)
330 .collect());
331 }
332 let e1 = m * (1.0 / m_len);
333 let e2 = normal.cross(e1);
334 let apex = cone.apex();
335 let e = d - dot_np(normal, apex);
336
337 if a_coeff < -1e-9 {
340 let abs_a = -a_coeff; if e * c < 0.0 {
347 return Ok(vec![]);
348 }
349 let s_c = e * c * p / abs_a;
352 let rhs = e * e * k * (1.0 - k) / abs_a;
353 if rhs <= 0.0 {
354 return Ok(vec![]);
355 }
356 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
359 return Ok(vec![]);
360 }
361 let center = apex + normal * e + e1 * s_c;
362 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
363 (semi_s, semi_t, e1, e2)
364 } else {
365 (semi_t, semi_s, e2, e1)
366 };
367 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
368 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
369 }
370
371 let chains = sample_plane_cone(cone, normal, d, reach)?;
374 Ok(chains
375 .into_iter()
376 .map(ExactIntersectionCurve::Points)
377 .collect())
378}
379
380#[allow(clippy::many_single_char_names)]
395pub fn plane_cone_conic_arc(
396 cone: &ConicalSurface,
397 normal: Vec3,
398 d: f64,
399 from: Point3,
400 to: Point3,
401) -> Result<Option<NurbsCurve>, MathError> {
402 let len = normal.length();
403 if len < 1e-15 {
404 return Err(MathError::ZeroVector);
405 }
406 let (normal, d) = (normal * (1.0 / len), d / len);
407 let axis = cone.axis();
408 let c = normal.dot(axis);
409 let p2 = (1.0 - c * c).max(0.0);
410 let p = p2.sqrt();
411 let k = cone.half_angle().sin().powi(2);
412 let a_coeff = p2 - k;
413 let m = Vec3::new(
414 axis.x() - c * normal.x(),
415 axis.y() - c * normal.y(),
416 axis.z() - c * normal.z(),
417 );
418 let m_len = m.length();
419 if m_len < 1e-12 || a_coeff < -1e-9 {
420 return Ok(None);
421 }
422 let e1 = m * (1.0 / m_len);
423 let e2 = normal.cross(e1);
424 let apex = cone.apex();
425 let e = d - dot_np(normal, apex);
426 let origin = apex + normal * e;
427 let plane_st = |q: Point3| {
428 let w = q - origin;
429 (w.dot(e1), w.dot(e2))
430 };
431 let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
432 let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
433 if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
434 return Ok(None);
435 }
436 let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
437 let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
438 let (control, weights) = if a_coeff.abs() <= 1e-9 {
439 let lin = 2.0 * e * c * p;
441 if lin.abs() < 1e-12 * scale {
442 return Ok(None);
443 }
444 let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
445 if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
446 || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
447 {
448 return Ok(None);
449 }
450 let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
451 (vec![from, mid, to], vec![1.0; 3])
452 } else {
453 let s_c = -e * c * p / a_coeff;
455 let r = e * e * k * (1.0 - k) / a_coeff;
456 if r <= 0.0 {
457 return Ok(None);
458 }
459 let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
460 let (x0, x1) = (s0 - s_c, s1 - s_c);
461 if x0 * x1 <= 0.0 {
462 return Ok(None);
463 }
464 let side = x0.signum();
465 let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
466 let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
467 if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
468 return Ok(None);
469 }
470 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
471 let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
472 let mut control = vec![from];
473 let mut weights = vec![1.0];
474 for i in 0..pieces {
475 #[allow(clippy::cast_precision_loss)]
476 let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
477 let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
478 let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
479 let w = half.cosh();
480 control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
481 weights.push(w);
482 control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
483 weights.push(1.0);
484 }
485 (control, weights)
486 };
487 let pieces = (control.len() - 1) / 2;
488 let mut knots = vec![0.0; 3];
489 for i in 1..pieces {
490 #[allow(clippy::cast_precision_loss)]
491 knots.extend([i as f64; 2]);
492 }
493 #[allow(clippy::cast_precision_loss)]
494 knots.extend([pieces as f64; 3]);
495 let curve = NurbsCurve::new(2, knots, control, weights)?;
496 let (sin_a, cos_a) = cone.half_angle().sin_cos();
501 let off_cone = |q: Point3| {
502 let w = q - apex;
503 let h = w.dot(axis);
504 (w - axis * h)
505 .length()
506 .mul_add(sin_a, -(h.abs() * cos_a))
507 .abs()
508 };
509 for i in 0..pieces {
510 for f in [0.25, 0.5, 0.75] {
511 #[allow(clippy::cast_precision_loss)]
512 if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
513 return Ok(None);
514 }
515 }
516 }
517 Ok(Some(curve))
518}
519
520#[derive(Clone, Copy)]
522pub enum AnalyticSurface<'a> {
523 Cylinder(&'a CylindricalSurface),
525 Cone(&'a ConicalSurface),
527 Sphere(&'a SphericalSurface),
529 Torus(&'a ToroidalSurface),
531}
532
533fn dot_np(n: Vec3, p: Point3) -> f64 {
535 n.dot(Vec3::new(p.x(), p.y(), p.z()))
536}
537
538pub fn intersect_plane_analytic(
546 surface: AnalyticSurface<'_>,
547 normal: Vec3,
548 d: f64,
549) -> Result<Vec<IntersectionCurve>, MathError> {
550 match surface {
551 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
552 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
553 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
554 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
555 }
556}
557
558pub fn sample_plane_analytic(
569 surface: AnalyticSurface<'_>,
570 normal: Vec3,
571 d: f64,
572) -> Result<Vec<Vec<Point3>>, MathError> {
573 match surface {
574 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
575 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
576 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
577 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
578 }
579}
580
581#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
583fn sample_plane_cylinder(
584 cyl: &CylindricalSurface,
585 normal: Vec3,
586 d: f64,
587) -> Result<Vec<Vec<Point3>>, MathError> {
588 let n_samples = 64_usize;
589 let mut points = Vec::with_capacity(n_samples + 1);
590
591 for i in 0..=n_samples {
592 let u = TAU * (i as f64) / (n_samples as f64);
593 let base = cyl.evaluate(u, 0.0);
594 let n_dot_axis = normal.dot(cyl.axis());
595 let n_dot_base = dot_np(normal, base);
596
597 if n_dot_axis.abs() < 1e-12 {
598 if (n_dot_base - d).abs() < 1e-6 {
599 points.push(base);
600 }
601 } else {
602 let v = (d - n_dot_base) / n_dot_axis;
603 if v.abs() <= 100.0 {
604 points.push(cyl.evaluate(u, v));
605 }
606 }
607 }
608
609 if points.len() < 2 {
610 Ok(vec![])
611 } else {
612 Ok(vec![points])
613 }
614}
615
616#[allow(clippy::cast_precision_loss)]
618fn sample_plane_sphere(
619 sphere: &SphericalSurface,
620 normal: Vec3,
621 d: f64,
622) -> Result<Vec<Vec<Point3>>, MathError> {
623 let h = dot_np(normal, sphere.center()) - d;
624 let r = sphere.radius();
625
626 if h.abs() > r - 1e-10 {
627 return Ok(vec![]);
628 }
629
630 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
631 let circle_center = Point3::new(
632 h.mul_add(-normal.x(), sphere.center().x()),
633 h.mul_add(-normal.y(), sphere.center().y()),
634 h.mul_add(-normal.z(), sphere.center().z()),
635 );
636
637 let basis = Frame3::from_normal(circle_center, normal)?;
638 let u_dir = basis.x;
639 let v_dir = basis.y;
640
641 let n_samples = 64_usize;
642 let mut points = Vec::with_capacity(n_samples + 1);
643
644 for i in 0..=n_samples {
645 let theta = TAU * (i as f64) / (n_samples as f64);
646 let (sin_t, cos_t) = theta.sin_cos();
647 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
648 }
649
650 Ok(vec![points])
651}
652
653#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
665fn sample_plane_cone(
666 cone: &ConicalSurface,
667 normal: Vec3,
668 d: f64,
669 reach: f64,
670) -> Result<Vec<Vec<Point3>>, MathError> {
671 let apex = cone.apex();
672 let n_dot_apex = dot_np(normal, apex);
673 let e = d - n_dot_apex;
674
675 let n_samples = 512_usize;
679 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
680 let mut v_min = f64::INFINITY;
681 for i in 0..n_samples {
682 let u = TAU * (i as f64) / (n_samples as f64);
683 let g = cone.evaluate(u, 1.0) - apex;
684 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
685 if n_dot_g.abs() < 1e-12 {
686 vs.push(None);
687 continue;
688 }
689 let v = e / n_dot_g;
690 if v >= -1e-12 {
691 let v = v.max(0.0);
692 v_min = v_min.min(v);
693 vs.push(Some(v));
694 } else {
695 vs.push(None);
696 }
697 }
698
699 if !v_min.is_finite() {
700 return Ok(Vec::new());
701 }
702
703 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
712
713 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
716
717 let point_at = |u: f64, v: f64| -> Point3 {
718 let g = cone.evaluate(u, 1.0) - apex;
719 apex + g * v
720 };
721 #[allow(clippy::cast_precision_loss)]
722 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
723 let n_dot_g_at = |u: f64| -> f64 {
724 let g = cone.evaluate(u, 1.0) - apex;
725 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
726 };
727
728 if kept.iter().all(Option::is_some) {
729 let mut pts: Vec<Point3> = kept
731 .iter()
732 .enumerate()
733 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
734 .collect();
735 if let Some(&first) = pts.first() {
736 pts.push(first);
737 }
738 return Ok(vec![pts]);
739 }
740
741 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
750 let Some(v_end) = kept[i_end] else {
751 return Vec::new();
752 };
753 let u_end = u_of(i_end);
754 #[allow(clippy::cast_precision_loss)]
755 let pitch = TAU / (n_samples as f64);
756 let u_next = if forward {
757 u_end + pitch
758 } else {
759 u_end - pitch
760 };
761 let target = e / v_max;
762 let h_end = n_dot_g_at(u_end) - target;
763 let h_next = n_dot_g_at(u_next) - target;
764 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
765 return Vec::new();
766 }
767 let (mut lo, mut hi) = (u_end, u_next);
768 for _ in 0..60 {
769 let mid = f64::midpoint(lo, hi);
770 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
771 lo = mid;
772 } else {
773 hi = mid;
774 }
775 }
776 let u_star = f64::midpoint(lo, hi);
777 let tail_n = 8_usize;
778 (1..=tail_n)
779 .filter_map(|k| {
780 #[allow(clippy::cast_precision_loss)]
781 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
782 let ng = n_dot_g_at(u);
783 if ng.abs() < 1e-12 {
784 return None;
785 }
786 let v = e / ng;
787 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
788 })
789 .collect()
790 };
791
792 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
795 let mut chains: Vec<Vec<Point3>> = Vec::new();
796 let mut run: Vec<usize> = Vec::new();
797 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
798 if run.len() >= 2 {
799 let first = run[0];
800 let last = run[run.len() - 1];
801 let mut pts: Vec<Point3> = tail(first, false, &kept);
802 pts.reverse();
803 pts.extend(
804 run.iter()
805 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
806 );
807 pts.extend(tail(last, true, &kept));
808 chains.push(pts);
809 }
810 run.clear();
811 };
812 for k in 0..n_samples {
813 let idx = (gap + k) % n_samples;
814 if kept[idx].is_some() {
815 run.push(idx);
816 } else {
817 flush(&mut run, &mut chains);
818 }
819 }
820 flush(&mut run, &mut chains);
821 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
822}
823
824#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
830 torus: &ToroidalSurface,
831 normal: Vec3,
832 d: f64,
833) -> Result<Vec<Vec<Point3>>, MathError> {
834 Ok(plane_torus_loops(torus, normal, d, 128)
835 .into_iter()
836 .map(|run| run.into_iter().map(|p| p.point).collect())
837 .collect())
838}
839
840#[allow(clippy::cast_precision_loss)]
850pub fn intersect_plane_cylinder(
851 cyl: &CylindricalSurface,
852 normal: Vec3,
853 d: f64,
854) -> Result<Vec<IntersectionCurve>, MathError> {
855 let n_samples = 64_usize;
856 let mut points_3d = Vec::new();
857 let mut ipoints = Vec::new();
858
859 for i in 0..=n_samples {
860 let u = TAU * (i as f64) / (n_samples as f64);
861 let base = cyl.evaluate(u, 0.0);
864 let n_dot_axis = normal.dot(cyl.axis());
865 let n_dot_base = dot_np(normal, base);
866
867 if n_dot_axis.abs() < 1e-12 {
868 if (n_dot_base - d).abs() < 1e-6 {
870 let pt = base;
871 points_3d.push(pt);
872 ipoints.push(IntersectionPoint {
873 point: pt,
874 param1: (u, 0.0),
875 param2: (0.0, 0.0),
876 });
877 }
878 } else {
879 let v = (d - n_dot_base) / n_dot_axis;
880 if v.abs() <= 100.0 {
882 let pt = cyl.evaluate(u, v);
883 points_3d.push(pt);
884 ipoints.push(IntersectionPoint {
885 point: pt,
886 param1: (u, v),
887 param2: (0.0, 0.0),
888 });
889 }
890 }
891 }
892
893 build_curves_from_points(&points_3d, ipoints)
894}
895
896#[allow(clippy::cast_precision_loss)]
905pub fn intersect_plane_sphere(
906 sphere: &SphericalSurface,
907 normal: Vec3,
908 d: f64,
909) -> Result<Vec<IntersectionCurve>, MathError> {
910 let h = dot_np(normal, sphere.center()) - d;
911 let r = sphere.radius();
912
913 if h.abs() > r - 1e-10 {
915 return Ok(vec![]);
916 }
917
918 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
919 let circle_center = Point3::new(
920 h.mul_add(-normal.x(), sphere.center().x()),
921 h.mul_add(-normal.y(), sphere.center().y()),
922 h.mul_add(-normal.z(), sphere.center().z()),
923 );
924
925 let basis = Frame3::from_normal(circle_center, normal)?;
927 let u_dir = basis.x;
928 let v_dir = basis.y;
929
930 let n_samples = 64_usize;
931 let mut points_3d = Vec::new();
932 let mut ipoints = Vec::new();
933
934 for i in 0..=n_samples {
935 let theta = TAU * (i as f64) / (n_samples as f64);
936 let (sin_t, cos_t) = theta.sin_cos();
937 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
938 points_3d.push(pt);
939 ipoints.push(IntersectionPoint {
940 point: pt,
941 param1: (theta, 0.0),
942 param2: (0.0, 0.0),
943 });
944 }
945
946 build_curves_from_points(&points_3d, ipoints)
947}
948
949#[allow(clippy::cast_precision_loss)]
958pub fn intersect_plane_cone(
959 cone: &ConicalSurface,
960 normal: Vec3,
961 d: f64,
962) -> Result<Vec<IntersectionCurve>, MathError> {
963 let n_samples = 64_usize;
964 let mut points_3d = Vec::new();
965 let mut ipoints = Vec::new();
966
967 for i in 0..n_samples {
968 let u = TAU * (i as f64) / (n_samples as f64);
969 let apex = cone.apex();
972 let n_dot_apex = dot_np(normal, apex);
973 let p1 = cone.evaluate(u, 1.0);
975 let dir = p1 - apex;
976 let n_dot_dir = normal.dot(dir);
977
978 if n_dot_dir.abs() < 1e-12 {
979 continue;
980 }
981
982 let v = (d - n_dot_apex) / n_dot_dir;
983 if v.abs() > 1e-10 && v.abs() < 100.0 {
985 let pt = cone.evaluate(u, v);
986 points_3d.push(pt);
987 ipoints.push(IntersectionPoint {
988 point: pt,
989 param1: (u, v),
990 param2: (0.0, 0.0),
991 });
992 }
993 }
994
995 build_curves_from_points(&points_3d, ipoints)
996}
997
998#[allow(clippy::unnecessary_wraps)]
1010pub fn intersect_plane_torus(
1011 torus: &ToroidalSurface,
1012 normal: Vec3,
1013 d: f64,
1014) -> Result<Vec<IntersectionCurve>, MathError> {
1015 let mut curves = Vec::new();
1019 for ipts in plane_torus_loops(torus, normal, d, 128) {
1020 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1021 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1022 curves.push(IntersectionCurve {
1023 curve,
1024 points: ipts,
1025 });
1026 }
1027 }
1028
1029 Ok(curves)
1030}
1031
1032const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1035
1036#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1058fn plane_torus_loops(
1059 torus: &ToroidalSurface,
1060 normal: Vec3,
1061 d: f64,
1062 n_v: usize,
1063) -> Vec<Vec<IntersectionPoint>> {
1064 let big_r = torus.major_radius();
1065 let small_r = torus.minor_radius();
1066 let a = normal.dot(torus.x_axis());
1067 let b = normal.dot(torus.y_axis());
1068 let c = normal.dot(torus.z_axis());
1069 let s = a.hypot(b);
1070 let phi = b.atan2(a);
1071 let d_local = d - dot_np(normal, torus.center());
1072 let point = |u: f64, v: f64| IntersectionPoint {
1073 point: torus.evaluate(u, v),
1074 param1: (u, v.rem_euclid(TAU)),
1075 param2: (0.0, 0.0),
1076 };
1077 let closed = |mut run: Vec<IntersectionPoint>| {
1078 run.push(run[0]);
1079 run
1080 };
1081
1082 if s < 1e-12 {
1084 if c.abs() < 1e-12 {
1085 return Vec::new();
1086 }
1087 let sin_v = d_local / (small_r * c);
1088 if sin_v.abs() > 1.0 + 1e-9 {
1089 return Vec::new();
1090 }
1091 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1092 let v1 = std::f64::consts::PI - v0;
1093 let mut vs = vec![v0];
1094 let apart = (v1 - v0).rem_euclid(TAU);
1097 if apart.min(TAU - apart) > 1e-9 {
1098 vs.push(v1);
1099 }
1100 return vs
1101 .into_iter()
1102 .map(|v| {
1103 closed(
1104 (0..n_v)
1105 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1106 .collect(),
1107 )
1108 })
1109 .collect();
1110 }
1111
1112 let step = TAU / (n_v as f64);
1115 let v_off = step * 0.5;
1116 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1118 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1119 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1120 let scan: Vec<f64> = (0..n_v).map(|i| (i as f64).mul_add(step, v_off)).collect();
1121 let touches = |lo: f64, hi: f64| {
1124 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1125 let (mut lo, mut hi) = (lo, hi);
1126 for _ in 0..80 {
1127 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1128 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1129 hi = m2;
1130 } else {
1131 lo = m1;
1132 }
1133 }
1134 1.0 - rhs_at(f64::midpoint(lo, hi)).abs() < 1e-12
1135 };
1136 let turn = |v_in: f64, v_out: f64| {
1138 let (mut lo, mut hi) = (v_in, v_out);
1139 for _ in 0..60 {
1140 let mid = f64::midpoint(lo, hi);
1141 if inside(mid) {
1142 lo = mid;
1143 } else {
1144 hi = mid;
1145 }
1146 }
1147 lo
1148 };
1149 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1150 if in_scan.iter().all(|&x| x) {
1151 let touching = scan.iter().any(|&v| touches(v, v + step));
1152 return [1.0, -1.0]
1153 .into_iter()
1154 .map(|sign| {
1155 let run: Vec<IntersectionPoint> = scan.iter().map(|&v| branch(v, sign)).collect();
1156 if touching { run } else { closed(run) }
1157 })
1158 .collect();
1159 }
1160 let Some(first) = (0..n_v).find(|&i| in_scan[i] && !in_scan[(i + n_v - 1) % n_v]) else {
1161 return Vec::new();
1162 };
1163 let mut loops = Vec::new();
1164 let mut k = 0;
1165 while k < n_v {
1166 let i = (first + k) % n_v;
1167 if !in_scan[i] {
1168 k += 1;
1169 continue;
1170 }
1171 let len = (0..n_v - k).take_while(|&j| in_scan[(i + j) % n_v]).count();
1173 let v_a = scan[i];
1174 let v_b = ((len - 1) as f64).mul_add(step, v_a);
1175 let run_v = |j: usize| (j as f64).mul_add(step, v_a);
1176 let (t_lo, t_hi) = (turn(v_a, v_a - step), turn(v_b, v_b + step));
1177 let touching = (0..len - 1).any(|j| touches(run_v(j), run_v(j + 1)));
1178 let (u_lo, u_hi) = (0..len)
1186 .map(run_v)
1187 .chain([t_lo, t_hi])
1188 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1189 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1190 (lo.min(u), hi.max(u))
1191 });
1192 let m = (len as f64)
1193 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1194 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1195 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1196 .ceil();
1197 let at = |k: f64| {
1198 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1199 (t_hi - t_lo).mul_add(f, t_lo)
1200 };
1201 let steps = m as usize;
1202 let mut pts: Vec<IntersectionPoint> =
1203 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1204 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1205 loops.push(if touching { pts } else { closed(pts) });
1206 k += len;
1207 }
1208 loops
1209}
1210
1211#[allow(clippy::cast_precision_loss)]
1220fn plane_torus_winding_loops(
1221 torus: &ToroidalSurface,
1222 normal: Vec3,
1223 d: f64,
1224 n_v: usize,
1225) -> Option<Vec<Vec<Point3>>> {
1226 let big_r = torus.major_radius();
1227 let small_r = torus.minor_radius();
1228 let a = normal.dot(torus.x_axis());
1229 let b = normal.dot(torus.y_axis());
1230 let c = normal.dot(torus.z_axis());
1231 let s = a.hypot(b);
1232 if s < 1e-12 * normal.length() || small_r >= big_r {
1233 return None;
1234 }
1235 let phi = b.atan2(a);
1236 let d_local = d - dot_np(normal, torus.center());
1237 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1238 let dense = 8 * n_v;
1239 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1240 return None;
1241 }
1242 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1243 for i in 0..n_v {
1244 let v = TAU * i as f64 / n_v as f64;
1245 let delta = rhs(v).acos();
1246 loops[0].push(torus.evaluate(phi + delta, v));
1247 loops[1].push(torus.evaluate(phi - delta, v));
1248 }
1249 Some(
1250 loops
1251 .into_iter()
1252 .map(|mut run| {
1253 run.push(run[0]);
1254 run
1255 })
1256 .collect(),
1257 )
1258}
1259
1260#[must_use]
1274pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1275 let c = torus.center();
1276 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1277 let big_r = torus.major_radius();
1278 let small_r = torus.minor_radius();
1279
1280 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1282 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1283 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1284 let (c0, c1) = (za.dot(o), za.dot(dir));
1285
1286 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1288 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1289 let g0 = a0.mul_add(
1290 a0,
1291 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1292 );
1293
1294 let four_rr = 4.0 * big_r * big_r;
1296 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1297 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1298 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1299
1300 let e4 = g2 * g2;
1302 let e3 = 2.0 * g2 * g1;
1303 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1304 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1305 let e0 = g0.mul_add(g0, -h0);
1306
1307 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1308 let tube = |t: f64, across: bool| -> f64 {
1314 let p = origin + dir * t;
1315 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1316 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1317 let centre = if across { -big_r } else { big_r };
1318 (a.hypot(b) - centre).hypot(cc) - small_r
1319 };
1320 for t in &mut roots {
1321 let across = big_r < small_r && tube(*t, true).abs() < tube(*t, false).abs();
1322 for _ in 0..8 {
1323 let eps = 1e-7;
1324 let f = tube(*t, across);
1325 let df = (tube(*t + eps, across) - tube(*t - eps, across)) / (2.0 * eps);
1326 if df.abs() <= 1e-12 {
1327 break;
1328 }
1329 let step = f / df;
1330 *t -= step;
1331 if step.abs() < 1e-14 * (1.0 + t.abs()) {
1332 break;
1333 }
1334 }
1335 }
1336 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1337 roots.dedup_by(|a, b| (*a - *b).abs() < 1e-9 * (1.0 + b.abs()));
1338 roots
1339}
1340
1341fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1344 if c4.abs() < 1e-14 {
1346 return real_roots_cubic(c3, c2, c1, c0);
1347 }
1348 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1350 let eval = |z: Complex| -> Complex {
1351 let mut acc = Complex::new(1.0, 0.0);
1353 acc = acc * z + Complex::new(a, 0.0);
1354 acc = acc * z + Complex::new(b, 0.0);
1355 acc = acc * z + Complex::new(c, 0.0);
1356 acc * z + Complex::new(d, 0.0)
1357 };
1358 let seed = Complex::new(0.4, 0.9);
1360 let mut r = [
1361 Complex::new(1.0, 0.0),
1362 seed,
1363 seed * seed,
1364 seed * seed * seed,
1365 ];
1366 for _ in 0..100 {
1367 let mut max_step = 0.0_f64;
1368 for i in 0..4 {
1369 let mut denom = Complex::new(1.0, 0.0);
1370 for j in 0..4 {
1371 if i != j {
1372 denom = denom * (r[i] - r[j]);
1373 }
1374 }
1375 if denom.norm() < 1e-300 {
1376 continue;
1377 }
1378 let step = eval(r[i]) / denom;
1379 r[i] = r[i] - step;
1380 max_step = max_step.max(step.norm());
1381 }
1382 if max_step < 1e-14 {
1383 break;
1384 }
1385 }
1386 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1393 let mut out: Vec<f64> = Vec::new();
1394 for z in r {
1395 if z.im.abs() >= 1e-7 {
1396 continue;
1397 }
1398 let x = z.re;
1399 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1402 if p_real(x).abs() > 1e-6 * scale {
1403 continue;
1404 }
1405 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1406 continue;
1407 }
1408 out.push(x);
1409 }
1410 out
1411}
1412
1413fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1415 if a.abs() < 1e-14 {
1416 return real_roots_quadratic(b, c, d);
1417 }
1418 let (b, c, d) = (b / a, c / a, d / a);
1420 let p = c - b * b / 3.0;
1421 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1422 let shift = -b / 3.0;
1423 let disc = q * q / 4.0 + p * p * p / 27.0;
1424 if disc > 1e-14 {
1425 let sq = disc.sqrt();
1426 let u = (-q / 2.0 + sq).cbrt();
1427 let v = (-q / 2.0 - sq).cbrt();
1428 vec![u + v + shift]
1429 } else if disc < -1e-14 {
1430 let m = 2.0 * (-p / 3.0).sqrt();
1432 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1433 (0..3)
1434 .map(|k| {
1435 m.mul_add(
1436 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1437 shift,
1438 )
1439 })
1440 .collect()
1441 } else {
1442 let u = (-q / 2.0).cbrt();
1444 vec![2.0 * u + shift, -u + shift]
1445 }
1446}
1447
1448fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1450 if a.abs() < 1e-14 {
1451 if b.abs() < 1e-14 {
1452 return Vec::new();
1453 }
1454 return vec![-c / b];
1455 }
1456 let disc = b * b - 4.0 * a * c;
1457 if disc < 0.0 {
1458 Vec::new()
1459 } else {
1460 let sq = disc.sqrt();
1461 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1462 }
1463}
1464
1465#[derive(Clone, Copy)]
1467struct Complex {
1468 re: f64,
1469 im: f64,
1470}
1471
1472impl Complex {
1473 const fn new(re: f64, im: f64) -> Self {
1474 Self { re, im }
1475 }
1476 fn norm(self) -> f64 {
1477 self.re.hypot(self.im)
1478 }
1479}
1480
1481impl std::ops::Add for Complex {
1482 type Output = Self;
1483 fn add(self, o: Self) -> Self {
1484 Self::new(self.re + o.re, self.im + o.im)
1485 }
1486}
1487
1488impl std::ops::Sub for Complex {
1489 type Output = Self;
1490 fn sub(self, o: Self) -> Self {
1491 Self::new(self.re - o.re, self.im - o.im)
1492 }
1493}
1494
1495impl std::ops::Mul for Complex {
1496 type Output = Self;
1497 fn mul(self, o: Self) -> Self {
1498 Self::new(
1499 self.re.mul_add(o.re, -(self.im * o.im)),
1500 self.re.mul_add(o.im, self.im * o.re),
1501 )
1502 }
1503}
1504
1505impl std::ops::Div for Complex {
1506 type Output = Self;
1507 fn div(self, o: Self) -> Self {
1508 let den = o.re.mul_add(o.re, o.im * o.im);
1509 Self::new(
1510 self.re.mul_add(o.re, self.im * o.im) / den,
1511 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1512 )
1513 }
1514}
1515
1516fn build_curves_from_points(
1520 points_3d: &[Point3],
1521 ipoints: Vec<IntersectionPoint>,
1522) -> Result<Vec<IntersectionCurve>, MathError> {
1523 if points_3d.len() < 2 {
1524 return Ok(vec![]);
1525 }
1526
1527 let degree = 3.min(points_3d.len() - 1);
1528 let curve = interpolate(points_3d, degree)?;
1529 Ok(vec![IntersectionCurve {
1530 curve,
1531 points: ipoints,
1532 }])
1533}
1534
1535#[allow(
1547 clippy::cast_precision_loss,
1548 clippy::too_many_lines,
1549 clippy::similar_names,
1550 clippy::unnecessary_wraps,
1551 clippy::type_complexity
1552)]
1553pub fn intersect_analytic_analytic(
1554 a: AnalyticSurface<'_>,
1555 b: AnalyticSurface<'_>,
1556 grid_res: usize,
1557) -> Result<Vec<IntersectionCurve>, MathError> {
1558 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1559}
1560
1561pub fn intersect_analytic_analytic_bounded(
1572 a: AnalyticSurface<'_>,
1573 b: AnalyticSurface<'_>,
1574 grid_res: usize,
1575 v_range_hint_a: Option<(f64, f64)>,
1576 v_range_hint_b: Option<(f64, f64)>,
1577) -> Result<Vec<IntersectionCurve>, MathError> {
1578 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1579}
1580
1581pub fn intersect_analytic_analytic_in_region(
1594 a: AnalyticSurface<'_>,
1595 b: AnalyticSurface<'_>,
1596 grid_res: usize,
1597 v_range_hint_a: Option<(f64, f64)>,
1598 v_range_hint_b: Option<(f64, f64)>,
1599 region: Aabb3,
1600) -> Result<Vec<IntersectionCurve>, MathError> {
1601 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1602}
1603
1604fn intersect_analytic_analytic_impl(
1605 a: AnalyticSurface<'_>,
1606 b: AnalyticSurface<'_>,
1607 grid_res: usize,
1608 v_range_hint_a: Option<(f64, f64)>,
1609 v_range_hint_b: Option<(f64, f64)>,
1610 region: Option<Aabb3>,
1611) -> Result<Vec<IntersectionCurve>, MathError> {
1612 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1615 return Ok(result);
1616 }
1617
1618 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1619 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1620 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1621 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1622
1623 let diag_a = {
1625 let p00 = surf_a(u_range_a.0, v_range_a.0);
1626 let p11 = surf_a(u_range_a.1, v_range_a.1);
1627 (p00 - p11).length()
1628 };
1629 let diag_b = {
1630 let p00 = surf_b(u_range_b.0, v_range_b.0);
1631 let p11 = surf_b(u_range_b.1, v_range_b.1);
1632 (p00 - p11).length()
1633 };
1634 let char_size = diag_a.min(diag_b).max(0.1);
1635
1636 #[allow(clippy::type_complexity)]
1640 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1641 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1645 let mut min_dist = f64::INFINITY;
1646
1647 #[allow(clippy::cast_precision_loss)]
1648 for ia in 0..grid_res {
1649 for ja in 0..grid_res {
1650 let ua =
1651 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1652 let va =
1653 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1654
1655 let pa = surf_a(ua, va);
1656
1657 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1659 let pb = surf_b(ub, vb);
1660 let dist = (pa - pb).length();
1661 min_dist = min_dist.min(dist);
1662
1663 if dist < seed_threshold {
1664 let mid = Point3::new(
1669 (pa.x() + pb.x()) * 0.5,
1670 (pa.y() + pb.y()) * 0.5,
1671 (pa.z() + pb.z()) * 0.5,
1672 );
1673 seeds.push((mid, (ua, va), (ub, vb)));
1674 }
1675 }
1676 }
1677
1678 let reject_dist = (char_size / grid_res as f64) * 3.0;
1687 if min_dist > reject_dist {
1688 return Ok(vec![]);
1689 }
1690
1691 if seeds.is_empty() {
1692 return Ok(vec![]);
1693 }
1694
1695 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1699 let dedup_radius = march_step * 10.0;
1700 let mut unique_seeds = Vec::new();
1701 for seed in &seeds {
1702 let dominated = unique_seeds
1703 .iter()
1704 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1705 if !dominated {
1706 unique_seeds.push(*seed);
1707 }
1708 }
1709
1710 let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1715 if let Some(r) = region {
1716 for seed in &mut unique_seeds {
1717 let mut p = seed.0;
1718 for _ in 0..8 {
1719 let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1720 let pa = surf_a(ua, va);
1721 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1722 let pb = surf_b(ub, vb);
1723 p = Point3::new(
1724 (pa.x() + pb.x()) * 0.5,
1725 (pa.y() + pb.y()) * 0.5,
1726 (pa.z() + pb.z()) * 0.5,
1727 );
1728 if (pa - pb).length() < 1e-9 {
1729 break;
1730 }
1731 }
1732 seed.0 = p;
1733 }
1734 unique_seeds.retain(|seed| r.contains_point(seed.0));
1735 }
1736
1737 let mut curves = Vec::new();
1739 let mut used_seeds = vec![false; unique_seeds.len()];
1740
1741 for si in 0..unique_seeds.len() {
1742 if used_seeds[si] {
1743 continue;
1744 }
1745 used_seeds[si] = true;
1746
1747 let march_result = march_analytic_intersection(
1748 &a,
1749 &b,
1750 surf_a.as_ref(),
1751 norm_a.as_ref(),
1752 surf_b.as_ref(),
1753 norm_b.as_ref(),
1754 unique_seeds[si].0,
1755 u_range_a,
1756 v_range_a,
1757 u_range_b,
1758 v_range_b,
1759 march_step,
1760 is_u_periodic(&a),
1761 is_u_periodic(&b),
1762 region,
1763 );
1764
1765 if march_result.len() >= 2 {
1766 for (sj, other) in unique_seeds.iter().enumerate() {
1767 if !used_seeds[sj]
1768 && march_result
1769 .iter()
1770 .any(|p| (*p - other.0).length() < dedup_radius)
1771 {
1772 used_seeds[sj] = true;
1773 }
1774 }
1775
1776 let ipts: Vec<IntersectionPoint> = march_result
1777 .iter()
1778 .map(|&pt| IntersectionPoint {
1779 point: pt,
1780 param1: (0.0, 0.0),
1781 param2: (0.0, 0.0),
1782 })
1783 .collect();
1784
1785 let degree = 3.min(march_result.len() - 1);
1786 if let Ok(curve) = interpolate(&march_result, degree) {
1787 curves.push(IntersectionCurve {
1788 curve,
1789 points: ipts,
1790 });
1791 }
1792 }
1793 }
1794
1795 Ok(curves)
1796}
1797
1798#[allow(clippy::too_many_lines)]
1812fn try_algebraic_intersection(
1813 a: &AnalyticSurface<'_>,
1814 b: &AnalyticSurface<'_>,
1815 v_range_a: Option<(f64, f64)>,
1816 v_range_b: Option<(f64, f64)>,
1817) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1818 match (a, b) {
1819 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1820 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1821 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1822 ),
1823 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1824 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1825 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1826 ),
1827 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1828 algebraic_sphere_sphere(s1, s2).map(Some)
1829 }
1830 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1831 let axis_dot = c1.axis().dot(c2.axis()).abs();
1832 if axis_dot > 1.0 - 1e-10 {
1833 let delta = c2.origin() - c1.origin();
1835 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1836 let along = delta_vec.dot(c1.axis());
1837 let perp = (delta_vec - c1.axis() * along).length();
1838 if perp < 1e-8 {
1839 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1842 return Ok(None); }
1844 return Ok(Some(vec![])); }
1846 }
1847 algebraic_cylinder_cylinder(c1, c2)
1849 }
1850 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1852 algebraic_sphere_cylinder(s, c, true)
1853 }
1854 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1855 algebraic_sphere_cylinder(s, c, false)
1856 }
1857 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1858 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1859 Ok(ruling_cone_sphere(cone, sphere, true))
1860 }
1861 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1862 Ok(ruling_cone_sphere(cone, sphere, false))
1863 }
1864 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1865 Ok(parallel_axis_torus_cylinder(t, c, true)
1866 .or_else(|| ruling_torus_cylinder(t, c, true)))
1867 }
1868 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1869 Ok(parallel_axis_torus_cylinder(t, c, false)
1870 .or_else(|| ruling_torus_cylinder(t, c, false)))
1871 }
1872 _ => Ok(None),
1873 }
1874}
1875
1876fn parallel_axis_torus_cylinder(
1883 torus: &ToroidalSurface,
1884 cyl: &CylindricalSurface,
1885 torus_first: bool,
1886) -> Option<Vec<IntersectionCurve>> {
1887 let axis = torus.z_axis();
1888 let along = cyl.axis().dot(axis);
1889 if along.abs() < 1.0 - 1e-10 {
1890 return None;
1891 }
1892 let offset = cyl.origin() - torus.center();
1893 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1894 return None;
1895 }
1896 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1897 let roots = |u: f64| {
1898 let q = cyl.evaluate(u, 0.0) - torus.center();
1899 let height = q.dot(axis);
1900 let rho = (q - axis * height).length();
1901 let reach = minor * minor - (rho - major) * (rho - major);
1902 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1903 };
1904 let samples = ruling_samples(cyl, &roots);
1905 let loops = if samples.iter().all(Option::is_some) {
1906 closed_ruling_loops(cyl, &roots, &samples)
1907 } else {
1908 partial_ruling_loops(cyl, &roots, &samples)
1909 };
1910 if loops.is_empty() {
1911 return None;
1912 }
1913 Some(fit_ruling_loops(&loops, |p| {
1914 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1915 }))
1916}
1917
1918fn meridian_crossings(
1924 first: (f64, f64, f64),
1925 second: (f64, f64, f64),
1926 scale: f64,
1927) -> Option<Vec<(f64, f64)>> {
1928 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1929 let (dx, dz) = (x2 - x1, z2 - z1);
1930 let dist = dx.hypot(dz);
1931 let slack = 1e-9 * scale;
1932 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1933 return None;
1934 }
1935 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1936 return Some(Vec::new());
1937 }
1938 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1939 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1940 let (ux, uz) = (dx / dist, dz / dist);
1941 let mut crossings = Vec::with_capacity(2);
1942 for side in [1.0, -1.0] {
1943 let rho = x1 + along * ux - side * across * uz;
1944 if rho <= slack {
1945 return None;
1946 }
1947 crossings.push((rho, z1 + along * uz + side * across * ux));
1948 }
1949 Some(crossings)
1950}
1951
1952fn circles_about_axis(
1954 base: Point3,
1955 axis: Vec3,
1956 crossings: &[(f64, f64)],
1957) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1958 crossings
1959 .iter()
1960 .map(|&(rho, z)| {
1961 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1962 })
1963 .collect()
1964}
1965
1966pub fn exact_torus_torus(
1977 first: &ToroidalSurface,
1978 second: &ToroidalSurface,
1979) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1980 let axis = first.z_axis();
1981 let scale = first.major_radius() + second.major_radius();
1982 let offset = second.center() - first.center();
1983 if first.minor_radius() >= first.major_radius()
1985 || second.minor_radius() >= second.major_radius()
1986 || axis.cross(second.z_axis()).length() > 1e-9
1987 || offset.cross(axis).length() > 1e-9 * scale
1988 {
1989 return Ok(None);
1990 }
1991 let Some(crossings) = meridian_crossings(
1992 (first.major_radius(), 0.0, first.minor_radius()),
1993 (
1994 second.major_radius(),
1995 offset.dot(axis),
1996 second.minor_radius(),
1997 ),
1998 scale,
1999 ) else {
2000 return Ok(None);
2001 };
2002 circles_about_axis(first.center(), axis, &crossings).map(Some)
2003}
2004
2005pub fn exact_cylinder_torus(
2019 cylinder: &CylindricalSurface,
2020 torus: &ToroidalSurface,
2021) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2022 let axis = torus.z_axis();
2023 let scale = torus.major_radius() + cylinder.radius();
2024 let offset = cylinder.origin() - torus.center();
2025 let linear = crate::tolerance::Tolerance::default().linear;
2026 if axis.cross(cylinder.axis()).length() > 1e-9
2027 || offset.cross(axis).length() > 1e-9 * scale
2028 || cylinder.radius() + torus.major_radius() <= torus.minor_radius() + linear
2029 {
2030 return Ok(None);
2031 }
2032 let gap = cylinder.radius() - torus.major_radius();
2033 let small = torus.minor_radius();
2034 if (gap.abs() - small).abs() <= linear {
2038 return circles_about_axis(torus.center(), axis, &[(cylinder.radius(), 0.0)]).map(Some);
2039 }
2040 if gap.abs() > small {
2041 return Ok(Some(Vec::new()));
2042 }
2043 let height = small.mul_add(small, -(gap * gap)).sqrt();
2044 circles_about_axis(
2045 torus.center(),
2046 axis,
2047 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2048 )
2049 .map(Some)
2050}
2051
2052pub fn exact_sphere_torus(
2065 sphere: &SphericalSurface,
2066 torus: &ToroidalSurface,
2067) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2068 let axis = torus.z_axis();
2069 let scale = torus.major_radius() + sphere.radius();
2070 let offset = sphere.center() - torus.center();
2071 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2073 return Ok(None);
2074 }
2075 let Some(crossings) = meridian_crossings(
2076 (0.0, offset.dot(axis), sphere.radius()),
2077 (torus.major_radius(), 0.0, torus.minor_radius()),
2078 scale,
2079 ) else {
2080 return Ok(None);
2081 };
2082 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2083}
2084
2085pub fn exact_cone_cone(
2110 c1: &ConicalSurface,
2111 c2: &ConicalSurface,
2112) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2113 let axis = c1.axis();
2114 let axis2 = c2.axis();
2115
2116 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2118 return Ok(None); }
2120 let apex1 = c1.apex();
2121 let apex2 = c2.apex();
2122 let delta = apex2 - apex1;
2123 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2124 let along = delta_v.dot(axis);
2125 if (delta_v - axis * along).length() > 1e-8 {
2126 return offset_parallel_cone_cone(c1, c2);
2127 }
2128
2129 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2130 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2131 return Ok(None); }
2133 let m1 = c1.half_angle().cos() / s1;
2134 let m2 = c2.half_angle().cos() / s2;
2135 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2136 let d2 = along; let denom = m1 - m2 * sigma;
2139 if denom.abs() < 1e-12 {
2140 if sigma > 0.0 && d2.abs() < 1e-9 {
2143 return Ok(None);
2144 }
2145 return Ok(Some(vec![]));
2146 }
2147
2148 let t_star = (-m2 * sigma * d2) / denom;
2149 let radius = m1 * t_star;
2150 if radius < 1e-12 {
2151 return Ok(Some(vec![])); }
2153
2154 let center = Point3::new(
2155 apex1.x() + axis.x() * t_star,
2156 apex1.y() + axis.y() * t_star,
2157 apex1.z() + axis.z() * t_star,
2158 );
2159 let circle = Circle3D::new(center, axis, radius)?;
2160 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2161}
2162
2163fn offset_parallel_cone_cone(
2174 c1: &ConicalSurface,
2175 c2: &ConicalSurface,
2176) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2177 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2178 return Ok(None); }
2180 let t1 = c1.half_angle().tan();
2181 let t2 = c2.half_angle().tan();
2182 if !t1.is_finite() || !t2.is_finite() {
2183 return Ok(None);
2184 }
2185 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2186 return Ok(None);
2187 }
2188
2189 let w = c1.axis();
2190 let apex1 = c1.apex();
2191 let apex2 = c2.apex();
2192 let delta = apex2 - apex1;
2193 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2194 let s = delta_v.dot(w);
2195 let tm = 0.5 * (t1 + t2);
2196 let k = 1.0 + tm * tm;
2197
2198 let n = (delta_v - w * (k * s)) * 2.0;
2202 let n_len = n.length();
2203 if n_len < 1e-12 {
2204 return Ok(None);
2205 }
2206 let n_hat = n * (1.0 / n_len);
2207 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2208
2209 let axis2 = c2.axis();
2215 let scale = 1.0 + delta_v.length();
2216 let mut out = Vec::new();
2217 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2218 let samples: Vec<Point3> = match &curve {
2219 ExactIntersectionCurve::Circle(c) => (0..4)
2220 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2221 .collect(),
2222 ExactIntersectionCurve::Ellipse(e) => (0..4)
2223 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2224 .collect(),
2225 ExactIntersectionCurve::Points(_) => return Ok(None),
2226 };
2227 let on_real_nappe = |p: &Point3| {
2228 let rel = *p - apex2;
2229 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2230 };
2231 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2232 match hits {
2233 0 => {}
2234 4 => out.push(curve),
2235 _ => return Ok(None),
2236 }
2237 }
2238 Ok(Some(out))
2239}
2240
2241pub fn exact_cone_cylinder(
2261 cone: &ConicalSurface,
2262 cyl: &CylindricalSurface,
2263) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2264 let axis = cone.axis();
2265 let cyl_axis = cyl.axis();
2266
2267 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2269 return Ok(None);
2270 }
2271 let apex = cone.apex();
2272 let delta = apex - cyl.origin();
2273 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2274 let along = delta_v.dot(cyl_axis);
2275 if (delta_v - cyl_axis * along).length() > 1e-8 {
2276 return Ok(None);
2277 }
2278
2279 let s = cone.half_angle().sin();
2280 if s.abs() < 1e-12 {
2281 return Ok(None); }
2283 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2285 return Ok(None); }
2287
2288 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2290 return Ok(Some(vec![])); }
2292 let center = Point3::new(
2293 apex.x() + axis.x() * t_star,
2294 apex.y() + axis.y() * t_star,
2295 apex.z() + axis.z() * t_star,
2296 );
2297 let circle = Circle3D::new(center, axis, cyl.radius())?;
2298 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2299}
2300
2301fn algebraic_cone_cone(
2310 c1: &ConicalSurface,
2311 c2: &ConicalSurface,
2312) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2313 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2314 return Ok(None);
2315 };
2316 let mut curves = Vec::new();
2317 for exact in exacts {
2318 let n_samples = 33;
2319 let mut positions = Vec::with_capacity(n_samples);
2320 let mut points = Vec::with_capacity(n_samples);
2321 #[allow(clippy::cast_precision_loss)]
2322 for i in 0..n_samples {
2323 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2324 let pt = match &exact {
2325 ExactIntersectionCurve::Circle(circle) => {
2326 crate::traits::ParametricCurve::evaluate(circle, theta)
2327 }
2328 ExactIntersectionCurve::Ellipse(ellipse) => {
2329 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2330 }
2331 ExactIntersectionCurve::Points(_) => break,
2332 };
2333 positions.push(pt);
2334 points.push(IntersectionPoint {
2335 point: pt,
2336 param1: (0.0, 0.0),
2337 param2: (0.0, 0.0),
2338 });
2339 }
2340 if positions.is_empty() {
2341 continue;
2342 }
2343 let degree = 3.min(positions.len() - 1);
2344 let curve = interpolate(&positions, degree)?;
2345 curves.push(IntersectionCurve { curve, points });
2346 }
2347 Ok(Some(curves))
2348}
2349
2350pub fn exact_sphere_cylinder(
2370 sphere: &SphericalSurface,
2371 cyl: &CylindricalSurface,
2372) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2373 let sc = sphere.center();
2374 let r_sphere = sphere.radius();
2375 let co = cyl.origin();
2376 let axis = cyl.axis();
2377 let r_cyl = cyl.radius();
2378
2379 let delta = sc - co;
2381 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2382 let along = delta_vec.dot(axis);
2383 let perp_vec = delta_vec - axis * along;
2384 let d_perp = perp_vec.length();
2385
2386 if d_perp > 1e-7 {
2389 return Ok(None);
2390 }
2391
2392 if r_cyl > r_sphere + 1e-10 {
2395 return Ok(Some(vec![]));
2396 }
2397 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2398 if z_sq < 0.0 {
2399 return Ok(Some(vec![]));
2400 }
2401 let z = z_sq.sqrt();
2402
2403 let center_axis_pt = Point3::new(
2406 co.x() + axis.x() * along,
2407 co.y() + axis.y() * along,
2408 co.z() + axis.z() * along,
2409 );
2410
2411 let mut circles = Vec::new();
2412 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2413 for &z_offset in offsets {
2414 let center = Point3::new(
2415 center_axis_pt.x() + axis.x() * z_offset,
2416 center_axis_pt.y() + axis.y() * z_offset,
2417 center_axis_pt.z() + axis.z() * z_offset,
2418 );
2419 let circle = Circle3D::new(center, axis, r_cyl)?;
2420 circles.push(ExactIntersectionCurve::Circle(circle));
2421 }
2422 Ok(Some(circles))
2423}
2424
2425pub fn exact_cone_sphere(
2443 cone: &ConicalSurface,
2444 sphere: &SphericalSurface,
2445) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2446 let offset = cone.apex() - sphere.center();
2447 let along = offset.dot(cone.axis());
2448 if (offset - cone.axis() * along).length() > 1e-7 {
2449 return Ok(None);
2450 }
2451 let lin_tol = Tolerance::new().linear;
2452 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2453 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2454 let b = 2.0 * sin_a * along;
2455 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2456 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2459 if disc < -noise {
2460 return Ok(Some(vec![]));
2461 }
2462 let roots: &[f64] = if far - near < lin_tol {
2463 &[far]
2464 } else {
2465 &[near, far]
2466 };
2467 let mut circles = Vec::new();
2468 for &v in roots {
2469 if v * cos_a > lin_tol {
2470 let centre = cone.apex() + cone.axis() * (v * sin_a);
2471 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2472 circles.push(ExactIntersectionCurve::Circle(circle));
2473 }
2474 }
2475 Ok(Some(circles))
2476}
2477
2478fn algebraic_sphere_cylinder(
2487 sphere: &SphericalSurface,
2488 cyl: &CylindricalSurface,
2489 sphere_first: bool,
2490) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2491 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2492 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2493 };
2494
2495 let mut curves = Vec::new();
2496 for exact in exacts {
2497 let ExactIntersectionCurve::Circle(circle) = exact else {
2498 continue;
2499 };
2500 let n_samples = 33;
2501 let mut points = Vec::with_capacity(n_samples);
2502 let mut positions = Vec::with_capacity(n_samples);
2503 #[allow(clippy::cast_precision_loss)]
2504 for i in 0..n_samples {
2505 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2506 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2507 positions.push(pt);
2508 let (param1, param2) = in_order(
2509 sphere.project_point(pt),
2510 cyl.project_point(pt),
2511 sphere_first,
2512 );
2513 points.push(IntersectionPoint {
2514 point: pt,
2515 param1,
2516 param2,
2517 });
2518 }
2519 let degree = 3.min(positions.len() - 1);
2520 let curve = interpolate(&positions, degree)?;
2521 curves.push(IntersectionCurve { curve, points });
2522 }
2523
2524 Ok(Some(curves))
2525}
2526
2527fn off_axis_sphere_cylinder(
2536 sphere: &SphericalSurface,
2537 cyl: &CylindricalSurface,
2538 sphere_first: bool,
2539) -> Option<Vec<IntersectionCurve>> {
2540 let (centre, radius) = (sphere.center(), sphere.radius());
2541 let axis = cyl.axis();
2542 let offset = centre - cyl.origin();
2543 let axis_distance = (offset - axis * offset.dot(axis)).length();
2544 let lin_tol = Tolerance::new().linear;
2545 if axis_distance > radius + cyl.radius() + lin_tol
2546 || axis_distance + radius < cyl.radius() - lin_tol
2547 {
2548 return Some(Vec::new());
2549 }
2550 let roots = |u: f64| {
2551 let q = cyl.evaluate(u, 0.0) - centre;
2552 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2553 };
2554 let samples = ruling_samples(cyl, &roots);
2555 let loops = if samples.iter().all(Option::is_some) {
2556 closed_ruling_loops(cyl, &roots, &samples)
2557 } else {
2558 partial_ruling_loops(cyl, &roots, &samples)
2559 };
2560 if loops.is_empty() {
2561 return None;
2562 }
2563 Some(fit_ruling_loops(&loops, |p| {
2564 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2565 }))
2566}
2567
2568const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2571 if a_first { (a, b) } else { (b, a) }
2572}
2573
2574#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2588fn algebraic_cylinder_cylinder(
2589 c1: &CylindricalSurface,
2590 c2: &CylindricalSurface,
2591) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2592 let alpha = c1.axis().dot(c2.axis());
2593 let a_coeff = 1.0 - alpha * alpha;
2594
2595 if a_coeff.abs() < 1e-12 {
2597 return Ok(None);
2598 }
2599
2600 let r1 = c1.radius();
2601 let r2 = c2.radius();
2602 let o1 = c1.origin();
2603 let o2 = c2.origin();
2604 let a1 = c1.axis();
2605 let a2 = c2.axis();
2606
2607 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2610 let cross = a1.cross(a2);
2611 let cross_len = cross.length();
2612 if cross_len > 1e-12 {
2613 let axis_dist = delta.dot(cross).abs() / cross_len;
2614 if axis_dist > r1 + r2 + Tolerance::new().linear {
2615 return Ok(Some(vec![])); }
2617 }
2618
2619 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2625 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2626 let alpha = sweep.axis().dot(a);
2627 let quad = 1.0 - alpha * alpha;
2628 let (axis, sweep) = (sweep.axis(), sweep.clone());
2629 move |u: f64| {
2630 let q = sweep.evaluate(u, 0.0) - o;
2631 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2632 let b = 2.0 * (q_a1 - alpha * q_a2);
2633 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2634 ruling_quadratic(quad, b, c)
2635 }
2636 };
2637 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2638 let samples1 = ruling_samples(c1, &roots1);
2639 let loops = if samples1.iter().all(Option::is_some) {
2640 closed_ruling_loops(c1, &roots1, &samples1)
2641 } else {
2642 let samples2 = ruling_samples(c2, &roots2);
2643 if samples2.iter().all(Option::is_some) {
2644 closed_ruling_loops(c2, &roots2, &samples2)
2645 } else if samples1.iter().any(Option::is_some) {
2646 partial_ruling_loops(c1, &roots1, &samples1)
2647 } else {
2648 partial_ruling_loops(c2, &roots2, &samples2)
2649 }
2650 };
2651 if loops.is_empty() {
2652 return Ok(None);
2653 }
2654 Ok(Some(fit_ruling_loops(&loops, |p| {
2655 (c1.project_point(p), c2.project_point(p))
2656 })))
2657}
2658
2659fn ruling_cone_cylinder(
2666 cone: &ConicalSurface,
2667 cyl: &CylindricalSurface,
2668 cone_first: bool,
2669) -> Option<Vec<IntersectionCurve>> {
2670 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2671 if sin_t < 1e-12 || cos_t < 1e-12 {
2672 return None;
2673 }
2674 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2675 let s = 1.0 / (sin_t * sin_t);
2676 let alpha = w.dot(d);
2677 let quad = 1.0 - s * alpha * alpha;
2678 if quad.abs() < 1e-9 {
2679 return None;
2680 }
2681 let roots = |u: f64| {
2682 let delta = cyl.evaluate(u, 0.0) - apex;
2683 let (dd, dw) = (delta.dot(d), delta.dot(w));
2684 let b = 2.0 * (dw - s * dd * alpha);
2685 let c = delta.dot(delta) - s * dd * dd;
2686 ruling_quadratic(quad, b, c)
2687 };
2688 let lin_tol = Tolerance::new().linear;
2689 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2690 #[allow(clippy::cast_precision_loss)]
2691 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2692 let (disc, vp, vm) = roots(u);
2693 disc >= -lin_tol
2694 && [vp, vm]
2695 .iter()
2696 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2697 });
2698 if far_nappe {
2699 return None;
2700 }
2701 let samples = ruling_samples(cyl, &roots);
2702 let scan = WINDOW_SCAN * RULING_SAMPLES;
2707 #[allow(clippy::cast_precision_loss)]
2710 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2711 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2712 let mut k = start;
2713 while k < start + scan {
2714 if !meets(k) {
2715 k += 1;
2716 continue;
2717 }
2718 let first = k;
2719 while k < start + scan && meets(k) {
2720 k += 1;
2721 }
2722 let covered = (first..k)
2723 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2724 .count();
2725 if covered < WINDOW_MIN_SAMPLES {
2726 return None;
2727 }
2728 }
2729 }
2730 let loops = if samples.iter().all(Option::is_some) {
2731 closed_ruling_loops(cyl, &roots, &samples)
2732 } else {
2733 partial_ruling_loops(cyl, &roots, &samples)
2734 };
2735 if loops.is_empty() {
2736 return None;
2737 }
2738 Some(fit_ruling_loops(&loops, |p| {
2739 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2740 }))
2741}
2742
2743fn ruling_torus_cylinder(
2753 torus: &ToroidalSurface,
2754 cyl: &CylindricalSurface,
2755 torus_first: bool,
2756) -> Option<Vec<IntersectionCurve>> {
2757 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2758 || torus.minor_radius() >= torus.major_radius()
2759 {
2760 return None;
2761 }
2762 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2763 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2764 let count = rows[0].len();
2765 let scan = WINDOW_SCAN * RULING_SAMPLES;
2766 #[allow(clippy::cast_precision_loss)]
2767 if count == 0
2768 || count % 2 == 1
2769 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2770 {
2771 return None;
2772 }
2773 let loops: Vec<Vec<Point3>> = (0..count)
2774 .map(|j| {
2775 let mut pts: Vec<Point3> = rows
2776 .iter()
2777 .enumerate()
2778 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2779 .collect();
2780 pts.push(pts[0]);
2781 pts
2782 })
2783 .collect();
2784 Some(fit_ruling_loops(&loops, |p| {
2785 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2786 }))
2787}
2788
2789fn ruling_cone_sphere(
2798 cone: &ConicalSurface,
2799 sphere: &SphericalSurface,
2800 cone_first: bool,
2801) -> Option<Vec<IntersectionCurve>> {
2802 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2803 let offset = apex - centre;
2804 let lin_tol = Tolerance::new().linear;
2805 let along = offset.dot(cone.axis());
2806 let across = (offset - cone.axis() * along).length();
2807 if across < lin_tol {
2808 return None;
2809 }
2810 let k = offset.dot(offset) - radius * radius;
2816 if radius - offset.length() > lin_tol {
2817 let exit = |u: f64| {
2818 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2819 let root = h.mul_add(h, -k).sqrt();
2820 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2821 };
2822 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2827 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2828 .collect();
2829 for _ in 0..10 {
2830 let mut refined = Vec::with_capacity(2 * samples.len());
2831 for pair in samples.windows(2) {
2832 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2833 refined.push(pair[0]);
2834 let um = 0.5 * (u0 + u1);
2835 let pm = exit(um);
2836 let chord = (p1 - p0).length();
2837 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2838 refined.push((um, pm));
2839 }
2840 }
2841 refined.extend(samples.last().copied());
2842 if refined.len() == samples.len() {
2843 break;
2844 }
2845 samples = refined;
2846 }
2847 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2848 if let Some(last) = pts.last_mut() {
2849 *last = samples[0].1;
2850 }
2851 return Some(fit_ruling_loops(&[pts], |p| {
2852 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2853 }));
2854 }
2855 let crossing = |h: f64| {
2859 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2860 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2861 };
2862 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2867 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2868 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2869 {
2870 return window_cone_sphere(cone, sphere, cone_first);
2871 }
2872 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2873 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2874 .collect::<Option<_>>()?;
2875 let loops: Vec<Vec<Point3>> = [0, 1]
2876 .iter()
2877 .map(|&j| {
2878 let mut pts: Vec<Point3> = rows
2879 .iter()
2880 .enumerate()
2881 .map(|(i, &(near, far))| {
2882 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2883 })
2884 .collect();
2885 pts.push(pts[0]);
2886 pts
2887 })
2888 .collect();
2889 Some(fit_ruling_loops(&loops, |p| {
2890 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2891 }))
2892}
2893
2894fn window_cone_sphere(
2909 cone: &ConicalSurface,
2910 sphere: &SphericalSurface,
2911 cone_first: bool,
2912) -> Option<Vec<IntersectionCurve>> {
2913 let offset = cone.apex() - sphere.center();
2914 let lin_tol = Tolerance::new().linear;
2915 if offset.length() - sphere.radius() <= lin_tol {
2916 return None;
2917 }
2918 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2919 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2920 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2921 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2922 if a < lin_tol {
2923 return None;
2924 }
2925 let reach = (-k.sqrt() - c) / a;
2926 if reach <= -1.0 {
2927 return Some(Vec::new());
2928 }
2929 if reach >= 1.0 {
2930 return None;
2931 }
2932 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2933 let half = std::f64::consts::PI - half;
2934 let n = RULING_SAMPLES;
2935 let mut pts: Vec<Point3> = (0..n)
2936 .map(|i| {
2937 #[allow(clippy::cast_precision_loss)]
2938 let theta = TAU * i as f64 / n as f64;
2939 let u = half.mul_add(-theta.cos(), mid);
2940 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2941 let split = h.mul_add(h, -k).max(0.0).sqrt();
2942 cone.evaluate(u, -h - split.copysign(theta.sin()))
2943 })
2944 .collect();
2945 pts.push(pts[0]);
2946 Some(fit_ruling_loops(&[pts], |p| {
2947 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2948 }))
2949}
2950
2951const WINDOW_SCAN: usize = 16;
2954const WINDOW_MIN_SAMPLES: usize = 8;
2955
2956const RULING_SAMPLES: usize = 128;
2960
2961#[allow(clippy::cast_precision_loss)]
2962fn ruling_u(i: usize) -> f64 {
2963 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2964}
2965
2966fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2968 let disc = b * b - 4.0 * quad * c;
2969 let root = disc.max(0.0).sqrt();
2970 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2971}
2972
2973fn ruling_samples(
2977 sweep: &CylindricalSurface,
2978 roots: &impl Fn(f64) -> (f64, f64, f64),
2979) -> Vec<Option<(Point3, Point3)>> {
2980 let lin_tol = Tolerance::new().linear;
2981 (0..RULING_SAMPLES)
2982 .map(|i| {
2983 let u = ruling_u(i);
2984 let (disc, vp, vm) = roots(u);
2985 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2986 })
2987 .collect()
2988}
2989
2990fn closed_ruling_loops(
2997 sweep: &CylindricalSurface,
2998 roots: &impl Fn(f64) -> (f64, f64, f64),
2999 samples: &[Option<(Point3, Point3)>],
3000) -> Vec<Vec<Point3>> {
3001 let (mut plus, mut minus): (Vec<Point3>, Vec<Point3>) =
3002 if let Some((neck, touching)) = narrowest_ruling(roots) {
3003 let count = 2 * RULING_SAMPLES;
3004 #[allow(clippy::cast_precision_loss)]
3005 (0..count)
3006 .map(|i| {
3007 let t = i as f64 / count as f64;
3008 let u = TAU.mul_add(t - 0.9 * (TAU * t).sin() / TAU, neck);
3009 let (_, vp, vm) = roots(u);
3010 if i == 0 && touching {
3011 let at = sweep.evaluate(u, 0.5 * (vp + vm));
3012 (at, at)
3013 } else {
3014 (sweep.evaluate(u, vp), sweep.evaluate(u, vm))
3015 }
3016 })
3017 .unzip()
3018 } else {
3019 samples.iter().flatten().copied().unzip()
3020 };
3021 plus.push(plus[0]);
3022 minus.push(minus[0]);
3023 vec![plus, minus]
3024}
3025
3026fn narrowest_ruling(roots: &impl Fn(f64) -> (f64, f64, f64)) -> Option<(f64, bool)> {
3034 let scan = WINDOW_SCAN * RULING_SAMPLES;
3035 #[allow(clippy::cast_precision_loss)]
3036 let step = TAU / scan as f64;
3037 let gap = |u: f64| {
3038 let (_, vp, vm) = roots(u);
3039 (vp - vm).abs()
3040 };
3041 #[allow(clippy::cast_precision_loss)]
3042 let gaps: Vec<f64> = (0..scan).map(|k| gap(step * k as f64)).collect();
3043 let widest = gaps.iter().copied().fold(0.0, f64::max);
3044 let tol = Tolerance::new().linear * (1.0 + widest);
3045 let mut necks = Vec::new();
3046 for k in 0..scan {
3047 let (before, here, after) = (gaps[(k + scan - 1) % scan], gaps[k], gaps[(k + 1) % scan]);
3048 if here > before || here >= after || here >= 0.25 * widest {
3049 continue;
3050 }
3051 #[allow(clippy::cast_precision_loss)]
3052 let (mut lo, mut hi) = (step * (k as f64 - 1.0), step * (k as f64 + 1.0));
3053 for _ in 0..100 {
3054 let (a, b) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3055 if gap(a) < gap(b) {
3056 hi = b;
3057 } else {
3058 lo = a;
3059 }
3060 }
3061 let u = 0.5 * (lo + hi);
3062 necks.push((u, gap(u) <= tol));
3063 }
3064 match necks[..] {
3065 [neck] => Some(neck),
3066 _ => None,
3067 }
3068}
3069
3070fn partial_ruling_loops(
3075 sweep: &CylindricalSurface,
3076 roots: &impl Fn(f64) -> (f64, f64, f64),
3077 samples: &[Option<(Point3, Point3)>],
3078) -> Vec<Vec<Point3>> {
3079 let branch_point = |inside: usize, outside: usize| -> Point3 {
3080 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
3081 if (hi - lo).abs() > std::f64::consts::PI {
3082 hi += if hi < lo { TAU } else { -TAU };
3083 }
3084 for _ in 0..60 {
3085 let mid = 0.5 * (lo + hi);
3086 if roots(mid).0 >= 0.0 {
3087 lo = mid;
3088 } else {
3089 hi = mid;
3090 }
3091 }
3092 let (_, vp, vm) = roots(lo);
3093 sweep.evaluate(lo, 0.5 * (vp + vm))
3094 };
3095 let Some(first_gap) = samples.iter().position(Option::is_none) else {
3096 return Vec::new();
3097 };
3098 let mut loops = Vec::new();
3099 let mut k = 0;
3100 while k < RULING_SAMPLES {
3101 let i = (first_gap + k) % RULING_SAMPLES;
3102 if samples[i].is_none() {
3103 k += 1;
3104 continue;
3105 }
3106 let start = i;
3107 let mut run = Vec::new();
3108 while k < RULING_SAMPLES {
3109 let j = (first_gap + k) % RULING_SAMPLES;
3110 let Some(pair) = samples[j] else { break };
3111 run.push(pair);
3112 k += 1;
3113 }
3114 let end = (start + run.len() - 1) % RULING_SAMPLES;
3115 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3116 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3117 let mut pts = vec![head];
3118 pts.extend(run.iter().map(|p| p.0));
3119 pts.push(tail);
3120 pts.extend(run.iter().rev().map(|p| p.1));
3121 pts.push(head);
3122 loops.push(pts);
3123 }
3124 loops
3125}
3126
3127fn fit_ruling_loops(
3130 loops: &[Vec<Point3>],
3131 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3132) -> Vec<IntersectionCurve> {
3133 let mut curves = Vec::new();
3134 for pts in loops {
3135 if pts.len() < 4 {
3136 continue;
3137 }
3138 let ipts: Vec<IntersectionPoint> = pts
3139 .iter()
3140 .map(|&p| {
3141 let (param1, param2) = params(p);
3142 IntersectionPoint {
3143 point: p,
3144 param1,
3145 param2,
3146 }
3147 })
3148 .collect();
3149 let degree = 3.min(pts.len() - 1);
3150 if let Ok(curve) = interpolate(pts, degree) {
3151 curves.push(IntersectionCurve {
3152 curve,
3153 points: ipts,
3154 });
3155 }
3156 }
3157 curves
3158}
3159
3160#[allow(clippy::unnecessary_wraps)]
3186fn algebraic_parallel_cone_cylinder(
3187 cone: &ConicalSurface,
3188 cyl: &CylindricalSurface,
3189 v_range_cone: Option<(f64, f64)>,
3190 v_range_cyl: Option<(f64, f64)>,
3191) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3192 let axis = cone.axis();
3193 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3194 return Ok(None); }
3196
3197 let apex = cone.apex();
3198 let delta = cyl.origin() - apex;
3199 let along = delta.dot(axis);
3200 let perp = delta - axis * along;
3201 let d = perp.length();
3202 if d < 1e-9 {
3203 return Ok(None); }
3205
3206 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3207 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3208
3209 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3210 if cos_t < 1e-12 || sin_t < 1e-12 {
3211 return Ok(None);
3212 }
3213 let r = cyl.radius();
3214
3215 let mut v_min = (d - r).abs() / cos_t;
3217 let mut v_max = (d + r) / cos_t;
3218 if v_max <= v_min {
3219 return Ok(Some(vec![]));
3220 }
3221
3222 let mut lo = v_min;
3228 let mut hi = v_max;
3229 if let Some((a, b)) = v_range_cone {
3234 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3235 lo = lo.max(a);
3236 hi = hi.min(b);
3237 }
3238 if let Some((a, b)) = v_range_cyl {
3239 let flip = cyl.axis().dot(axis);
3242 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3243 let (a, b) = (to_cone_v(a), to_cone_v(b));
3244 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3245 lo = lo.max(a);
3246 hi = hi.min(b);
3247 }
3248 let (turn_lo, turn_hi) = (v_min, v_max);
3249 v_min = lo.max(v_min);
3250 v_max = hi.min(v_max);
3251 if v_max - v_min <= 1e-12 {
3252 return Ok(Some(vec![]));
3253 }
3254 let slack = Tolerance::new().linear;
3262 #[allow(clippy::cast_precision_loss)]
3263 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3264 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3265 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3266 .map(|i| {
3267 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3268 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3269 let off = foot - apex;
3270 let across = off - axis * off.dot(axis);
3271 apex + across + axis * (across.length() * sin_t / cos_t)
3272 })
3273 .collect();
3274 pts.push(pts[0]);
3275 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3276 (cone.project_point(p), cyl.project_point(p))
3277 })));
3278 }
3279
3280 let n_samples = 128;
3281 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3282 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3283 #[allow(clippy::cast_precision_loss)]
3284 for i in 0..=n_samples {
3285 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3286 let rho = v * cos_t;
3287 if rho < 1e-12 {
3288 if (d - r).abs() < 1e-12 {
3296 let apex = cone.evaluate(phi0, v);
3297 plus.push(apex);
3298 minus.push(apex);
3299 }
3300 continue;
3301 }
3302 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3303 let alpha = cos_alpha.acos();
3304 plus.push(cone.evaluate(phi0 + alpha, v));
3305 minus.push(cone.evaluate(phi0 - alpha, v));
3306 }
3307
3308 let mut curves = Vec::new();
3309 for pts in [&plus, &minus] {
3310 if pts.len() < 4 {
3313 continue;
3314 }
3315 let ipts: Vec<IntersectionPoint> = pts
3316 .iter()
3317 .map(|&p| IntersectionPoint {
3318 point: p,
3319 param1: cone.project_point(p),
3320 param2: cyl.project_point(p),
3321 })
3322 .collect();
3323 let degree = 3.min(pts.len() - 1);
3324 match interpolate(pts, degree) {
3325 Ok(curve) => curves.push(IntersectionCurve {
3326 curve,
3327 points: ipts,
3328 }),
3329 Err(_) => return Ok(None),
3334 }
3335 }
3336
3337 Ok(Some(curves))
3338}
3339
3340fn algebraic_sphere_sphere(
3348 s1: &SphericalSurface,
3349 s2: &SphericalSurface,
3350) -> Result<Vec<IntersectionCurve>, MathError> {
3351 let c1 = s1.center();
3352 let c2 = s2.center();
3353 let r1 = s1.radius();
3354 let r2 = s2.radius();
3355
3356 let delta = c2 - c1;
3357 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3358 let d = d_sq.sqrt();
3359
3360 if d < 1e-12 {
3361 return Ok(vec![]);
3363 }
3364
3365 if d > r1 + r2 + 1e-10 {
3367 return Ok(vec![]); }
3369 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3370 return Ok(vec![]); }
3372
3373 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3375
3376 let r_circle_sq = r1 * r1 - d1 * d1;
3378 if r_circle_sq < 0.0 {
3379 if r_circle_sq > -1e-10 {
3381 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3383 let tangent_pt = Point3::new(
3384 c1.x() + axis.x() * d1,
3385 c1.y() + axis.y() * d1,
3386 c1.z() + axis.z() * d1,
3387 );
3388 let ipt = IntersectionPoint {
3389 point: tangent_pt,
3390 param1: (0.0, 0.0),
3391 param2: (0.0, 0.0),
3392 };
3393 return Ok(vec![IntersectionCurve {
3395 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3396 points: vec![ipt],
3397 }]);
3398 }
3399 return Ok(vec![]);
3400 }
3401
3402 let r_circle = r_circle_sq.sqrt();
3403 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3404 let center = Point3::new(
3405 c1.x() + axis.x() * d1,
3406 c1.y() + axis.y() * d1,
3407 c1.z() + axis.z() * d1,
3408 );
3409
3410 let basis = Frame3::from_normal(center, axis)?;
3412 let u_dir = basis.x;
3413 let v_dir = basis.y;
3414
3415 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3418 let mut positions = Vec::with_capacity(n_samples);
3419 #[allow(clippy::cast_precision_loss)]
3420 for i in 0..n_samples {
3421 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3422 let (sin_t, cos_t) = theta.sin_cos();
3423 let pt = Point3::new(
3424 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3425 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3426 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3427 );
3428 positions.push(pt);
3429 points.push(IntersectionPoint {
3430 point: pt,
3431 param1: (0.0, 0.0),
3432 param2: (0.0, 0.0),
3433 });
3434 }
3435
3436 let degree = 3.min(positions.len() - 1);
3437 let curve = interpolate(&positions, degree)?;
3438
3439 Ok(vec![IntersectionCurve { curve, points }])
3440}
3441
3442#[allow(clippy::too_many_arguments)]
3448fn correct_to_intersection(
3449 a: &AnalyticSurface<'_>,
3450 b: &AnalyticSurface<'_>,
3451 surf_a: &dyn Fn(f64, f64) -> Point3,
3452 norm_a: &dyn Fn(f64, f64) -> Vec3,
3453 surf_b: &dyn Fn(f64, f64) -> Point3,
3454 norm_b: &dyn Fn(f64, f64) -> Vec3,
3455 point: Point3,
3456 u_range_a: (f64, f64),
3457 v_range_a: (f64, f64),
3458 u_range_b: (f64, f64),
3459 v_range_b: (f64, f64),
3460 max_iters: usize,
3461) -> Point3 {
3462 let mut p = point;
3463 for _ in 0..max_iters {
3464 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3465 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3466 let pa = surf_a(ua, va);
3467 let pb = surf_b(ub, vb);
3468 let na = norm_a(ua, va);
3469 let nb = norm_b(ub, vb);
3470 let pv = Vec3::new(p.x(), p.y(), p.z());
3471
3472 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3473 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3474
3475 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3476 break;
3477 }
3478
3479 let t = na.cross(nb);
3480 let t_len = t.length();
3481 if t_len < 1e-10 {
3482 return Point3::new(
3484 (pa.x() + pb.x()) * 0.5,
3485 (pa.y() + pb.y()) * 0.5,
3486 (pa.z() + pb.z()) * 0.5,
3487 );
3488 }
3489 let t_hat = t * (1.0 / t_len);
3490
3491 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3493 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3494 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3495 if det.abs() < 1e-15 {
3496 return Point3::new(
3497 (pa.x() + pb.x()) * 0.5,
3498 (pa.y() + pb.y()) * 0.5,
3499 (pa.z() + pb.z()) * 0.5,
3500 );
3501 }
3502 let inv = 1.0 / det;
3503 let dx = inv
3505 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3506 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3507 let dy = inv
3508 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3509 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3510 let dz = inv
3511 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3512 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3513 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3514
3515 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3518 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3519 let pc_a = surf_a(uc, vc);
3520 let pc_b = surf_b(ud, vd);
3521 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3522 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3523 .dot(norm_a(uc, vc))
3524 .abs();
3525 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3526 .dot(norm_b(ud, vd))
3527 .abs();
3528 if da_new > da.abs() && db_new > db.abs() {
3529 return p;
3530 }
3531
3532 p = candidate;
3533 }
3534 p
3535}
3536
3537#[allow(clippy::too_many_arguments)]
3543fn march_analytic_intersection(
3544 a: &AnalyticSurface<'_>,
3545 b: &AnalyticSurface<'_>,
3546 surf_a: &dyn Fn(f64, f64) -> Point3,
3547 norm_a: &dyn Fn(f64, f64) -> Vec3,
3548 surf_b: &dyn Fn(f64, f64) -> Point3,
3549 norm_b: &dyn Fn(f64, f64) -> Vec3,
3550 seed: Point3,
3551 u_range_a: (f64, f64),
3552 v_range_a: (f64, f64),
3553 u_range_b: (f64, f64),
3554 v_range_b: (f64, f64),
3555 initial_step: f64,
3556 u_periodic_a: bool,
3557 u_periodic_b: bool,
3558 region: Option<Aabb3>,
3559) -> Vec<Point3> {
3560 let max_steps = 500;
3561 let h_min = 1e-6;
3562 let h_max = initial_step * 4.0;
3563 let closure_dist = initial_step * 5.0;
3567 let max_angle = 10.0_f64.to_radians();
3569 let min_angle = 2.0_f64.to_radians();
3570
3571 let mut forward = Vec::new();
3573 let mut backward = Vec::new();
3575
3576 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3577 let mut current = seed;
3578 let mut h = initial_step;
3579 let mut prev_tangent: Option<Vec3> = None;
3580
3581 for _ in 0..max_steps {
3582 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3583 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3584
3585 let na = norm_a(ua, va);
3586 let nb = norm_b(ub, vb);
3587
3588 let tangent = na.cross(nb);
3589 let t_len = tangent.length();
3590 if t_len < 1e-10 {
3591 break;
3592 }
3593 let t_dir = tangent * (direction / t_len);
3594
3595 if let Some(prev_t) = prev_tangent {
3597 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3598 let angle = cos_angle.acos();
3599 if angle > max_angle && h > h_min {
3600 h = (h * 0.5).max(h_min);
3601 } else if angle < min_angle {
3602 h = (h * 2.0).min(h_max);
3603 }
3604 }
3605 prev_tangent = Some(t_dir);
3606
3607 let next = Point3::new(
3608 h.mul_add(t_dir.x(), current.x()),
3609 h.mul_add(t_dir.y(), current.y()),
3610 h.mul_add(t_dir.z(), current.z()),
3611 );
3612
3613 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3614 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3615
3616 let pa = surf_a(ua2, va2);
3617 let pb = surf_b(ub2, vb2);
3618 let mid = Point3::new(
3619 (pa.x() + pb.x()) * 0.5,
3620 (pa.y() + pb.y()) * 0.5,
3621 (pa.z() + pb.z()) * 0.5,
3622 );
3623 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3624 || va2 <= v_range_a.0
3625 || va2 >= v_range_a.1;
3626 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3627 || vb2 <= v_range_b.0
3628 || vb2 >= v_range_b.1;
3629
3630 if out_a || out_b {
3631 break;
3632 }
3633 if region.is_some_and(|r| !r.contains_point(mid)) {
3634 points.push(mid);
3635 break;
3636 }
3637
3638 let dist_to_seed = (mid - seed).length();
3642 if points.len() > 10 && dist_to_seed < closure_dist {
3643 points.push(seed);
3644 break;
3645 }
3646
3647 points.push(mid);
3648 current = mid;
3649 }
3650 }
3651
3652 backward.reverse();
3654 let mut result = backward;
3655 result.push(seed);
3656 result.append(&mut forward);
3657
3658 for pt in &mut result {
3660 *pt = correct_to_intersection(
3661 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3662 5,
3663 );
3664 }
3665
3666 result
3667}
3668
3669fn project_analytic(
3673 surface: &AnalyticSurface<'_>,
3674 point: Point3,
3675 u_range: (f64, f64),
3676 v_range: (f64, f64),
3677) -> (f64, f64) {
3678 match surface {
3679 AnalyticSurface::Cylinder(cyl) => {
3680 let (u, v) = cyl.project_point(point);
3681 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3682 }
3683 AnalyticSurface::Sphere(sphere) => {
3684 let (u, v) = sphere.project_point(point);
3685 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3686 }
3687 AnalyticSurface::Cone(cone) => {
3688 let (u, v) = cone.project_point(point);
3689 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3690 }
3691 AnalyticSurface::Torus(torus) => {
3692 let (u, v) = torus.project_point(point);
3693 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3694 }
3695 }
3696}
3697
3698fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3702 matches!(
3703 surface,
3704 AnalyticSurface::Cylinder(_)
3705 | AnalyticSurface::Cone(_)
3706 | AnalyticSurface::Sphere(_)
3707 | AnalyticSurface::Torus(_)
3708 )
3709}
3710
3711#[allow(clippy::type_complexity)]
3713fn surface_closures<'a>(
3714 surface: &'a AnalyticSurface<'a>,
3715) -> (
3716 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3717 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3718 (f64, f64),
3719 (f64, f64),
3720) {
3721 match surface {
3722 AnalyticSurface::Cylinder(cyl) => (
3723 Box::new(|u, v| cyl.evaluate(u, v)),
3724 Box::new(|u, v| cyl.normal(u, v)),
3725 (0.0, TAU),
3726 (-1.0, 1.0),
3727 ),
3728 AnalyticSurface::Cone(cone) => (
3729 Box::new(|u, v| cone.evaluate(u, v)),
3730 Box::new(|u, v| cone.normal(u, v)),
3731 (0.0, TAU),
3732 (0.01, 2.0),
3733 ),
3734 AnalyticSurface::Sphere(sphere) => (
3735 Box::new(|u, v| sphere.evaluate(u, v)),
3736 Box::new(|u, v| sphere.normal(u, v)),
3737 (0.0, TAU),
3738 (-FRAC_PI_2, FRAC_PI_2),
3739 ),
3740 AnalyticSurface::Torus(torus) => (
3741 Box::new(|u, v| torus.evaluate(u, v)),
3742 Box::new(|u, v| torus.normal(u, v)),
3743 (0.0, TAU),
3744 (0.0, TAU),
3745 ),
3746 }
3747}
3748
3749#[cfg(test)]
3750#[allow(clippy::unwrap_used, clippy::expect_used)]
3751mod tests {
3752 use super::*;
3753 use crate::tolerance::Tolerance;
3754
3755 #[test]
3759 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3760 let half_angle = 1.1_f64;
3761 let cone = ConicalSurface::new(
3762 Point3::new(0.0, 0.0, 0.0),
3763 Vec3::new(0.0, 0.0, 1.0),
3764 half_angle,
3765 )
3766 .unwrap();
3767 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3768 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3769 let chains =
3770 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3771 .unwrap();
3772 let chain = chains
3773 .iter()
3774 .find_map(|c| match c {
3775 ExactIntersectionCurve::Points(chain) => Some(chain),
3776 _ => None,
3777 })
3778 .expect("a parabola or hyperbola section is sampled");
3779 let (from, to) = (chain[2], chain[chain.len() - 3]);
3780 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3781 .unwrap()
3782 .expect("an exact arc");
3783 let (t0, t1) = arc.domain();
3784 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3785 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3786 for i in 0..=200 {
3787 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3788 let w = q - Point3::new(0.0, 0.0, 0.0);
3789 let off_plane = (normal.dot(w) - d).abs();
3790 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3791 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3792 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3793 }
3794 }
3795 }
3796
3797 #[test]
3801 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3802 let half_angle = 1.1_f64;
3803 let cone = ConicalSurface::new(
3804 Point3::new(0.0, 0.0, 0.0),
3805 Vec3::new(0.0, 0.0, 1.0),
3806 half_angle,
3807 )
3808 .unwrap();
3809 for shortfall in [1e-10, 3e-10, 8e-10] {
3810 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3811 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3812 let chains =
3813 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3814 .unwrap();
3815 let Some(chain) = chains.iter().find_map(|c| match c {
3816 ExactIntersectionCurve::Points(chain) => Some(chain),
3817 _ => None,
3818 }) else {
3819 continue;
3820 };
3821 let (from, to) = (chain[2], chain[chain.len() - 3]);
3822 assert!(
3823 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3824 .unwrap()
3825 .is_none(),
3826 "coincident ends"
3827 );
3828 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3829 continue;
3830 };
3831 let (t0, t1) = arc.domain();
3832 for i in 0..=200 {
3833 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3834 - Point3::new(0.0, 0.0, 0.0);
3835 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3836 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3837 }
3838 }
3839 }
3840
3841 #[test]
3842 fn plane_cylinder_perpendicular() {
3843 let cyl =
3844 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3845 .unwrap();
3846
3847 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3849 assert!(!curves.is_empty(), "should find intersection curve");
3850 assert!(
3851 curves[0].points.len() > 10,
3852 "should have many sample points"
3853 );
3854
3855 let tol = Tolerance::loose();
3856 for pt in &curves[0].points {
3857 assert!(
3858 tol.approx_eq(pt.point.z(), 3.0),
3859 "z should be ~3.0, got {}",
3860 pt.point.z()
3861 );
3862 let r = pt.point.x().hypot(pt.point.y());
3863 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3864 }
3865 }
3866
3867 #[test]
3868 fn plane_sphere_equator() {
3869 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3870
3871 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3872 assert!(!curves.is_empty());
3873
3874 let tol = Tolerance::loose();
3875 for pt in &curves[0].points {
3876 assert!(
3877 tol.approx_eq(pt.point.z(), 0.0),
3878 "z should be ~0, got {}",
3879 pt.point.z()
3880 );
3881 let r = pt.point.x().hypot(pt.point.y());
3882 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3883 }
3884 }
3885
3886 #[test]
3887 fn plane_sphere_no_intersection() {
3888 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3889
3890 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3891 assert!(curves.is_empty());
3892 }
3893
3894 #[test]
3895 fn plane_cone_cross_section() {
3896 let cone = ConicalSurface::new(
3897 Point3::new(0.0, 0.0, 0.0),
3898 Vec3::new(0.0, 0.0, 1.0),
3899 std::f64::consts::FRAC_PI_4,
3900 )
3901 .unwrap();
3902
3903 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3904 assert!(!curves.is_empty(), "should find intersection with cone");
3905 }
3906
3907 #[test]
3914 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3915 let c1 = ConicalSurface::new(
3916 Point3::new(
3917 -16.999_999_999_999_975,
3918 -16.999_999_999_999_975,
3919 5.849_999_999_999_951,
3920 ),
3921 Vec3::new(0.0, 0.0, -1.0),
3922 0.785_398_163_397_433_5,
3923 )
3924 .unwrap();
3925 let c2 = ConicalSurface::new(
3926 Point3::new(
3927 -16.750_000_000_000_036,
3928 -16.750_000_000_000_018,
3929 0.749_999_999_999_881,
3930 ),
3931 Vec3::new(0.0, 0.0, 1.0),
3932 0.785_398_163_397_467_6,
3933 )
3934 .unwrap();
3935
3936 let curves = exact_cone_cone(&c1, &c2)
3937 .unwrap()
3938 .expect("offset parallel equal-angle cones must take the radical-plane path");
3939 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3940 assert!(
3941 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3942 "expected an ellipse section, got {:?}",
3943 curves[0]
3944 );
3945 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3946 return;
3947 };
3948
3949 for i in 0..16 {
3953 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3954 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3955 let rel = p - cone.apex();
3956 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3957 let axial = rel_v.dot(cone.axis());
3958 let radial = (rel_v - cone.axis() * axial).length();
3959 assert!(
3960 axial > 0.0,
3961 "{label}: sample on phantom nappe (axial {axial})"
3962 );
3963 let expect = cone.half_angle().tan() * axial;
3964 assert!(
3965 (radial - expect).abs() < 1e-9,
3966 "{label}: sample off surface by {}",
3967 (radial - expect).abs()
3968 );
3969 }
3970 }
3971 }
3972
3973 #[test]
3977 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3978 let c1 = ConicalSurface::new(
3979 Point3::new(0.0, 0.0, 5.0),
3980 Vec3::new(0.0, 0.0, -1.0),
3981 std::f64::consts::FRAC_PI_4,
3982 )
3983 .unwrap();
3984 let c2 = ConicalSurface::new(
3985 Point3::new(0.25, 0.25, 20.0),
3986 Vec3::new(0.0, 0.0, 1.0),
3987 std::f64::consts::FRAC_PI_4,
3988 )
3989 .unwrap();
3990 let curves = exact_cone_cone(&c1, &c2)
3991 .unwrap()
3992 .expect("radical-plane path");
3993 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3994 }
3995
3996 #[test]
3999 fn offset_parallel_cones_with_unequal_angles_defer() {
4000 let c1 = ConicalSurface::new(
4001 Point3::new(0.0, 0.0, 5.0),
4002 Vec3::new(0.0, 0.0, -1.0),
4003 std::f64::consts::FRAC_PI_4,
4004 )
4005 .unwrap();
4006 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
4007 .unwrap();
4008 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
4009 }
4010
4011 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
4015 let cone = ConicalSurface::new(
4016 Point3::new(0.0, 0.0, 0.0),
4017 Vec3::new(0.0, 0.0, 1.0),
4018 std::f64::consts::FRAC_PI_4,
4019 )
4020 .unwrap();
4021 let (s, c) = 40.0_f64.to_radians().sin_cos();
4022 let tube =
4023 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
4024 (cone, tube)
4025 }
4026
4027 #[test]
4028 fn marcher_keeps_to_its_region() {
4029 let (cone, tube) = cone_and_tilted_tube();
4030 let run = |region: Option<Aabb3>| {
4031 let (a, b) = (
4032 AnalyticSurface::Cone(&cone),
4033 AnalyticSurface::Cylinder(&tube),
4034 );
4035 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
4036 match region {
4037 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
4038 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
4039 }
4040 .unwrap()
4041 };
4042 assert!(!run(None).is_empty());
4043 let near = Aabb3 {
4044 min: Point3::new(-0.5, -2.0, 0.8),
4045 max: Point3::new(0.7, -0.7, 2.0),
4046 };
4047 let curves = run(Some(near));
4048 assert!(!curves.is_empty(), "the loop through the region is kept");
4049 let reach = near.expanded(0.6);
4051 for curve in &curves {
4052 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
4053 }
4054 let away = Aabb3 {
4055 min: Point3::new(5.0, 5.0, 5.0),
4056 max: Point3::new(6.0, 6.0, 6.0),
4057 };
4058 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
4059 }
4060
4061 #[test]
4062 fn coaxial_cones_cross_at_single_circle() {
4063 let outer = ConicalSurface::new(
4068 Point3::new(0.0, 0.0, 50.0),
4069 Vec3::new(0.0, 0.0, -1.0),
4070 5.0_f64.atan(),
4071 )
4072 .unwrap();
4073 let inner = ConicalSurface::new(
4074 Point3::new(0.0, 0.0, 90.0),
4075 Vec3::new(0.0, 0.0, -1.0),
4076 10.0_f64.atan(),
4077 )
4078 .unwrap();
4079
4080 let curves = intersect_analytic_analytic_bounded(
4081 AnalyticSurface::Cone(&outer),
4082 AnalyticSurface::Cone(&inner),
4083 32,
4084 None,
4085 None,
4086 )
4087 .unwrap();
4088
4089 assert_eq!(
4090 curves.len(),
4091 1,
4092 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
4093 curves.len()
4094 );
4095 for p in &curves[0].points {
4096 let r = p.point.x().hypot(p.point.y());
4097 assert!(
4098 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
4099 "intersection point off the expected z=10,r=8 circle: {:?}",
4100 p.point
4101 );
4102 }
4103 }
4104
4105 #[test]
4106 fn plane_torus_cross_section() {
4107 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
4108
4109 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4110 assert!(
4111 !curves.is_empty(),
4112 "should find intersection curves with torus"
4113 );
4114 }
4115
4116 #[test]
4119 fn plane_tangent_to_a_tube_touches_it_along_one_circle() {
4120 for (major, minor) in [(5.0, 1.0), (0.1, 2.45)] {
4121 let torus = ToroidalSurface::new(Point3::new(1.0, 2.0, 3.0), major, minor).unwrap();
4122 for z in [3.0 - minor, 3.0 + minor] {
4123 let exact = exact_plane_analytic(
4124 AnalyticSurface::Torus(&torus),
4125 Vec3::new(0.0, 0.0, 1.0),
4126 z,
4127 )
4128 .unwrap();
4129 assert_eq!(exact.len(), 1, "R={major} r={minor} z={z}");
4130 let circle = match &exact[0] {
4131 ExactIntersectionCurve::Circle(c) => Some(c),
4132 _ => None,
4133 };
4134 let c = circle.expect("a circle");
4135 assert!((c.radius() - major).abs() < 1e-12);
4136 assert!((c.center() - Point3::new(1.0, 2.0, z)).length() < 1e-12);
4137
4138 let sampled = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), z).unwrap();
4139 assert_eq!(sampled.len(), 1, "R={major} r={minor} z={z}");
4140 }
4141 let single = |normal: Vec3, d: f64| {
4143 matches!(
4144 exact_plane_analytic(AnalyticSurface::Torus(&torus), normal, d)
4145 .unwrap()
4146 .as_slice(),
4147 [ExactIntersectionCurve::Circle(_)]
4148 )
4149 };
4150 assert!(!single(Vec3::new(1e-6, 0.0, 1.0), 3.0 + minor));
4151 }
4152 }
4153
4154 #[test]
4158 fn plane_tangent_to_a_large_tube_is_read_through_rounding() {
4159 let torus = ToroidalSurface::new(Point3::new(0.3, -0.7, 3.0), 200.0, 100.1).unwrap();
4160 let level = Vec3::new(0.0, 0.0, 1.0);
4161 let curves =
4162 |d: f64| exact_plane_analytic(AnalyticSurface::Torus(&torus), level, d).unwrap();
4163 assert!(matches!(
4164 curves(3.0 + 100.1).as_slice(),
4165 [ExactIntersectionCurve::Circle(_)]
4166 ));
4167 let inside = curves(3.0 + 100.1 - 4e-7);
4168 assert!(!matches!(
4169 inside.as_slice(),
4170 [ExactIntersectionCurve::Circle(_)]
4171 ));
4172 }
4173
4174 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4177 let rho = p.x().hypot(p.y());
4178 ((rho - major).hypot(p.z())) - minor
4179 }
4180
4181 #[test]
4187 fn oblique_cone_cylinder_traces_curves_on_both() {
4188 use crate::traits::ParametricCurve;
4189 let cone = ConicalSurface::new(
4193 Point3::new(0.0, 0.0, 3.0),
4194 Vec3::new(0.0, 0.0, -1.0),
4195 2.0_f64.atan(),
4196 )
4197 .unwrap();
4198 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4199 let cyl =
4200 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4201 .unwrap();
4202 for cone_first in [true, false] {
4203 let (a, b) = if cone_first {
4204 (
4205 AnalyticSurface::Cone(&cone),
4206 AnalyticSurface::Cylinder(&cyl),
4207 )
4208 } else {
4209 (
4210 AnalyticSurface::Cylinder(&cyl),
4211 AnalyticSurface::Cone(&cone),
4212 )
4213 };
4214 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4215 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4216 for c in &curves {
4217 let (t0, t1) = c.curve.domain();
4218 for k in 0..=64 {
4219 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4220 let p = ParametricCurve::evaluate(&c.curve, t);
4221 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4224 assert!(
4225 (rod - 0.6).abs() < 1e-4,
4226 "x0 {x0}: off the rod by {}",
4227 rod - 0.6
4228 );
4229 let cone_r = p.x().hypot(p.y());
4230 assert!(
4231 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4232 "x0 {x0}: off the cone at {p:?}"
4233 );
4234 }
4235 }
4236 }
4237 }
4238 }
4239
4240 #[test]
4241 fn a_rod_through_a_rings_tube_traces_four_loops() {
4242 use crate::traits::ParametricCurve;
4243 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4244 let rod =
4247 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4248 .unwrap();
4249 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4250 assert_eq!(curves.len(), 4);
4251 for c in &curves {
4252 let (t0, t1) = c.curve.domain();
4253 for k in 0..=64 {
4254 let p =
4255 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4256 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4257 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4258 assert!(
4259 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4260 "off by {on_rod}, {on_ring}"
4261 );
4262 }
4263 }
4264 let high =
4266 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4267 .unwrap();
4268 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4269 let grazing =
4271 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4272 .unwrap();
4273 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4274 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4276 let thin =
4277 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4278 .unwrap();
4279 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4280 }
4281
4282 #[test]
4283 fn a_pin_through_a_ball_traces_two_loops() {
4284 use crate::traits::ParametricCurve;
4285 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4286 let half = 0.08_f64.atan();
4289 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4290 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4291 for cone_first in [true, false] {
4292 let (a, b) = if cone_first {
4293 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4294 } else {
4295 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4296 };
4297 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4298 assert_eq!(curves.len(), 2, "entry and exit loops");
4299 for c in &curves {
4300 let (t0, t1) = c.curve.domain();
4301 for k in 0..=64 {
4302 let p = ParametricCurve::evaluate(
4303 &c.curve,
4304 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4305 );
4306 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4307 let axial = apex.z() - p.z();
4308 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4309 assert!(
4310 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4311 "off by {on_ball}, {on_pin}"
4312 );
4313 }
4314 }
4315 }
4316 let coaxial =
4321 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4322 .unwrap();
4323 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4324 let aside = ConicalSurface::new(
4325 Point3::new(2.8, 0.0, 10.0),
4326 Vec3::new(0.0, 0.0, -1.0),
4327 FRAC_PI_2 - half,
4328 )
4329 .unwrap();
4330 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4331 let holding = ConicalSurface::new(
4332 Point3::new(1.0, 0.5, 1.0),
4333 Vec3::new(0.0, 0.0, -1.0),
4334 FRAC_PI_2 - half,
4335 )
4336 .unwrap();
4337 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4338 let away = ConicalSurface::new(
4339 Point3::new(1.0, 0.5, 10.0),
4340 Vec3::new(0.0, 0.0, 1.0),
4341 FRAC_PI_2 - half,
4342 )
4343 .unwrap();
4344 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4345 let step = TAU / 2048.0;
4349 let grazed =
4350 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4351 .unwrap();
4352 let wide =
4353 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4354 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4355 }
4356
4357 #[test]
4358 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4359 use crate::traits::ParametricCurve;
4360 let cone = ConicalSurface::new(
4362 Point3::new(0.0, 0.0, 3.0),
4363 Vec3::new(0.0, 0.0, -1.0),
4364 2.0_f64.atan(),
4365 )
4366 .unwrap();
4367 for (centre, radius) in [
4368 (Point3::new(1.0, 0.8, 1.2), 1.1),
4369 (Point3::new(1.5, 0.0, 0.0), 0.8),
4370 ] {
4371 let ball = SphericalSurface::new(centre, radius).unwrap();
4372 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4373 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4374 let (t0, t1) = curves[0].curve.domain();
4375 for k in 0..=64 {
4376 let p = ParametricCurve::evaluate(
4377 &curves[0].curve,
4378 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4379 );
4380 let on_ball = (p - centre).length() - radius;
4381 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4382 assert!(
4383 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4384 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4385 );
4386 }
4387 }
4388 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4390 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4391 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4392 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4393 }
4394
4395 #[test]
4396 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4397 use crate::traits::ParametricCurve;
4398 let cone = ConicalSurface::new(
4399 Point3::new(0.0, 0.0, 3.0),
4400 Vec3::new(0.0, 0.0, -1.0),
4401 2.0_f64.atan(),
4402 )
4403 .unwrap();
4404 for (centre, radius) in [
4407 (Point3::new(0.5, 0.0, 2.5), 2.0),
4408 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4409 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4410 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4411 ] {
4412 let ball = SphericalSurface::new(centre, radius).unwrap();
4413 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4414 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4415 let (t0, t1) = curves[0].curve.domain();
4416 for k in 0..=4096 {
4417 let p = ParametricCurve::evaluate(
4418 &curves[0].curve,
4419 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4420 );
4421 let on_ball = (p - centre).length() - radius;
4422 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4423 assert!(
4424 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4425 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4426 );
4427 }
4428 }
4429 }
4430
4431 #[test]
4432 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4433 let t = 2.0_f64.atan();
4434 let cone =
4435 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4436 let through_apex =
4438 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4439 .unwrap();
4440 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4441 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4443 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4444 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4445 let pin =
4448 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4449 let tube =
4450 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4451 .unwrap();
4452 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4453 }
4454
4455 #[test]
4456 fn parallel_cone_cylinder_gives_two_exact_branches() {
4457 use crate::traits::ParametricCurve;
4458 let cone = ConicalSurface::new(
4459 Point3::new(-5.45, -36.55, -4.85),
4460 Vec3::new(0.0, 0.0, 1.0),
4461 std::f64::consts::FRAC_PI_4,
4462 )
4463 .unwrap();
4464 let cyl = CylindricalSurface::new(
4465 Point3::new(-8.0, -34.0, -5.0),
4466 Vec3::new(0.0, 0.0, 1.0),
4467 4.45,
4468 )
4469 .unwrap();
4470 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4472 let curves = intersect_analytic_analytic_bounded(
4473 AnalyticSurface::Cone(&cone),
4474 AnalyticSurface::Cylinder(&cyl),
4475 32,
4476 Some(v_hint),
4477 Some((0.0, 2.5)),
4478 )
4479 .unwrap();
4480
4481 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4482 for c in &curves {
4483 let (t0, t1) = c.curve.domain();
4484 for k in 0..=32 {
4485 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4486 let p = ParametricCurve::evaluate(&c.curve, t);
4487 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4489 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4490 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4492 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4493 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4495 }
4496 }
4497 }
4498
4499 #[test]
4500 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4501 use crate::traits::ParametricCurve;
4502 let cone = ConicalSurface::new(
4504 Point3::new(0.0, 0.0, 3.0),
4505 Vec3::new(0.0, 0.0, -1.0),
4506 2.0_f64.atan(),
4507 )
4508 .unwrap();
4509 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4511 let rod =
4512 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4513 .unwrap();
4514 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4515 .unwrap()
4516 .unwrap();
4517 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4518 let (t0, t1) = curves[0].curve.domain();
4519 let (first, last) = (
4520 ParametricCurve::evaluate(&curves[0].curve, t0),
4521 ParametricCurve::evaluate(&curves[0].curve, t1),
4522 );
4523 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4524 for k in 0..=64 {
4525 let p = ParametricCurve::evaluate(
4526 &curves[0].curve,
4527 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4528 );
4529 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4530 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4531 assert!(
4532 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4533 "({x}, {y}): off by {on_rod}, {on_cone}"
4534 );
4535 }
4536 }
4537 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4540 let rod =
4541 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4542 .unwrap();
4543 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4544 .unwrap()
4545 .unwrap();
4546 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4547 }
4548 }
4549
4550 #[test]
4553 fn coaxial_cone_cylinder_defers_to_other_paths() {
4554 let cone = ConicalSurface::new(
4555 Point3::new(0.0, 0.0, 0.0),
4556 Vec3::new(0.0, 0.0, 1.0),
4557 std::f64::consts::FRAC_PI_4,
4558 )
4559 .unwrap();
4560 let cyl =
4561 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4562 .unwrap();
4563 assert!(
4564 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4565 .unwrap()
4566 .is_none()
4567 );
4568 }
4569
4570 #[test]
4571 fn oblique_cone_cylinder_defers_to_other_paths() {
4572 let cone = ConicalSurface::new(
4573 Point3::new(0.0, 0.0, 0.0),
4574 Vec3::new(0.0, 0.0, 1.0),
4575 std::f64::consts::FRAC_PI_4,
4576 )
4577 .unwrap();
4578 let cyl =
4579 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4580 .unwrap();
4581 assert!(
4582 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4583 .unwrap()
4584 .is_none()
4585 );
4586 }
4587
4588 #[test]
4589 fn plane_torus_lobe_closes_and_stays_on_surface() {
4590 use crate::traits::ParametricCurve;
4591 let (major, minor) = (10.0, 3.0);
4592 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4593
4594 for (n, d) in [
4598 (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), ] {
4602 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4603 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4604 for c in &curves {
4605 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4606 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4607 assert!(
4608 (p0 - p1).length() < 1e-7,
4609 "lobe not closed: gap={} (n={n:?} d={d})",
4610 (p0 - p1).length()
4611 );
4612 for k in 0..=64 {
4614 let t = f64::from(k) / 64.0;
4615 let p = ParametricCurve::evaluate(&c.curve, t);
4616 assert!(
4617 torus_implicit(p, major, minor).abs() < 1e-2,
4618 "off-surface point {p:?} implicit={}",
4619 torus_implicit(p, major, minor)
4620 );
4621 }
4622 }
4623 }
4624 }
4625
4626 #[test]
4627 fn plane_torus_inner_tangent_figure_eight_stays_open() {
4628 use crate::traits::ParametricCurve;
4629 let (major, minor) = (10.0, 3.0);
4630 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4631
4632 let curves =
4637 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4638 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4639 let max_gap = curves
4640 .iter()
4641 .map(|c| {
4642 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4643 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4644 (p0 - p1).length()
4645 })
4646 .fold(0.0_f64, f64::max);
4647 assert!(
4648 max_gap > 1e-2,
4649 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4650 );
4651 }
4652
4653 #[test]
4658 fn plane_torus_wall_sections_close_into_their_loops() {
4659 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4660 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4661 for k in 1..200 {
4662 let (d, want) = match k.cmp(&100) {
4663 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4665 std::cmp::Ordering::Greater => (
4667 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4668 1,
4669 ),
4670 std::cmp::Ordering::Equal => continue,
4671 };
4672 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4673 let closed = loops
4674 .iter()
4675 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4676 .count();
4677 assert_eq!(
4678 (loops.len(), closed),
4679 (want, want),
4680 "R {major} r {minor}, wall at {d}"
4681 );
4682 }
4683 }
4684 }
4685
4686 #[test]
4690 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4691 let (major, minor) = (4.0, 1.5);
4692 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4693 for tilt in [0.03_f64, 0.08, 0.2] {
4694 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4695 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4696 assert_eq!(curves.len(), 2, "tilt {tilt}");
4697 for c in &curves {
4698 let (t0, t1) = c.curve.domain();
4699 let off = (0..=400)
4700 .map(|k| {
4701 let p = c
4702 .curve
4703 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4704 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4705 })
4706 .fold(0.0_f64, |m, e| m.max(e.abs()));
4707 assert!(
4708 off < 1e-6,
4709 "tilt {tilt}: fitted section {off} off the torus"
4710 );
4711 }
4712 }
4713 }
4714
4715 #[test]
4716 fn line_torus_box_edge_crossing_is_exact() {
4717 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4720 let ts = intersect_line_torus(
4721 &torus,
4722 Point3::new(6.0, -4.0, -5.0),
4723 Vec3::new(0.0, 0.0, 1.0),
4724 );
4725 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4727 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4728 let rho = 6.0_f64.hypot(4.0);
4729 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4730 assert!(
4731 (zs[0] - (-z_exp)).abs() < 1e-9,
4732 "z0={} exp={}",
4733 zs[0],
4734 -z_exp
4735 );
4736 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4737 for &t in &ts {
4739 let p = Point3::new(6.0, -4.0, -5.0 + t);
4740 let rho = p.x().hypot(p.y());
4741 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4742 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4743 }
4744 }
4745
4746 #[test]
4751 fn line_spindle_torus_roots_lie_on_their_tubes() {
4752 let torus = ToroidalSurface::new(Point3::new(41.0, 17.0, 4.7), 0.1, 2.45).unwrap();
4753 let origin = Point3::new(42.36, 18.31, 3.4);
4754 let dir = Vec3::new(
4755 0.573_576_436_351_046,
4756 0.740_535_693_464_567_5,
4757 0.350_889_803_483_932_2,
4758 );
4759 let ts = intersect_line_torus(&torus, origin, dir);
4760 let tube = |t: f64, across: f64| {
4761 let q = origin + dir * t;
4762 ((q.x() - 41.0).hypot(q.y() - 17.0) - across * 0.1).hypot(q.z() - 4.7) - 2.45
4763 };
4764 for &t in &ts {
4765 assert!(
4766 tube(t, 1.0).abs().min(tube(t, -1.0).abs()) < 1e-9,
4767 "root {t} off both tubes in {ts:?}"
4768 );
4769 }
4770 for want in [0.124, 0.398] {
4771 assert!(
4772 ts.iter().any(|&t| (t - want).abs() < 1e-3),
4773 "no root near {want} in {ts:?}"
4774 );
4775 }
4776 }
4777
4778 #[test]
4779 fn line_torus_miss_and_tangent() {
4780 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4781 let miss = intersect_line_torus(
4783 &torus,
4784 Point3::new(20.0, 0.0, 0.0),
4785 Vec3::new(0.0, 0.0, 1.0),
4786 );
4787 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4788 let axis =
4790 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4791 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4792 }
4793
4794 #[test]
4795 fn dispatch_via_analytic_surface() {
4796 let cyl =
4797 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4798 .unwrap();
4799 let curves = intersect_plane_analytic(
4800 AnalyticSurface::Cylinder(&cyl),
4801 Vec3::new(0.0, 0.0, 1.0),
4802 0.0,
4803 )
4804 .unwrap();
4805 assert!(!curves.is_empty());
4806 }
4807
4808 #[test]
4809 fn perpendicular_cylinders_intersect() {
4810 let cyl_z =
4811 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4812 .unwrap();
4813 let cyl_x =
4814 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4815 .unwrap();
4816
4817 let curves = intersect_analytic_analytic(
4818 AnalyticSurface::Cylinder(&cyl_z),
4819 AnalyticSurface::Cylinder(&cyl_x),
4820 16,
4821 )
4822 .unwrap();
4823
4824 assert!(
4825 !curves.is_empty(),
4826 "perpendicular cylinders should intersect"
4827 );
4828
4829 for c in &curves {
4830 assert!(
4831 c.points.len() >= 2,
4832 "intersection curve should have >= 2 points, got {}",
4833 c.points.len()
4834 );
4835 }
4836 }
4837
4838 #[test]
4841 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4842 let cyl_z =
4843 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4844 .unwrap();
4845 let cyl_x =
4846 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4847 .unwrap();
4848 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4849 .unwrap()
4850 .unwrap();
4851 assert_eq!(curves.len(), 1);
4852 let curve = &curves[0].curve;
4853 let (t0, t1) = curve.domain();
4854 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4855 let off = |p: Point3| {
4856 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4857 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4858 on_z.max(on_x)
4859 };
4860 let worst = (0..=400)
4861 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4862 .fold(0.0, f64::max);
4863 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4864 }
4865
4866 #[test]
4870 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4871 let cyl_z =
4872 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4873 .unwrap();
4874 let cyl_x =
4875 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4876 .unwrap();
4877 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4878 .unwrap()
4879 .expect("the thin cylinder's sweep finds the loop");
4880 assert_eq!(curves.len(), 1);
4881 }
4882
4883 #[test]
4884 fn sphere_cylinder_intersect() {
4885 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4886 let cyl =
4887 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4888 .unwrap();
4889
4890 let curves = intersect_analytic_analytic(
4891 AnalyticSurface::Sphere(&sphere),
4892 AnalyticSurface::Cylinder(&cyl),
4893 16,
4894 )
4895 .unwrap();
4896
4897 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4901 }
4902
4903 #[test]
4904 fn exact_sphere_cylinder_coaxial_two_circles() {
4905 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4908 let cyl =
4909 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4910 .unwrap();
4911 let circles = exact_sphere_cylinder(&sphere, &cyl)
4912 .unwrap()
4913 .expect("coaxial case returns Some");
4914 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4915 let mut zs: Vec<f64> = circles
4916 .iter()
4917 .filter_map(|c| match c {
4918 ExactIntersectionCurve::Circle(circle) => {
4919 assert!(
4920 (circle.radius() - 3.0).abs() < 1e-9,
4921 "rim radius == cyl radius"
4922 );
4923 Some(circle.center().z())
4924 }
4925 _ => None,
4926 })
4927 .collect();
4928 assert_eq!(zs.len(), 2, "both sections must be exact circles");
4929 zs.sort_by(f64::total_cmp);
4930 let z = 27.0_f64.sqrt();
4931 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4932 }
4933
4934 #[test]
4935 fn exact_sphere_cylinder_non_coaxial_defers() {
4936 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4938 let cyl =
4939 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4940 .unwrap();
4941 assert!(
4942 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4943 "non-coaxial sphere/cylinder defers to the marcher"
4944 );
4945 }
4946
4947 #[test]
4948 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4949 let cone = ConicalSurface::new(
4951 Point3::new(0.0, 0.0, 3.0),
4952 Vec3::new(0.0, 0.0, -1.0),
4953 2.0_f64.atan(),
4954 )
4955 .unwrap();
4956 for (height, radius, count) in [
4957 (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), ] {
4963 let centre = Point3::new(0.0, 0.0, height);
4964 let ball = SphericalSurface::new(centre, radius).unwrap();
4965 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4966 let circles = circles_of(&curves);
4967 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4968 for circle in circles {
4969 for k in 0..16 {
4970 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4971 let on_ball = (p - centre).length() - radius;
4972 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4973 assert!(
4974 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4975 "ball at {height}: off by {on_ball}, {on_cone}"
4976 );
4977 }
4978 }
4979 }
4980 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4981 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4982 let wide =
4985 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4986 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4987 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4988 let circles = circles_of(&curves);
4989 assert_eq!(circles.len(), 1, "the touch");
4990 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4991 assert!(
4992 (circles[0].radius() - touch).abs() < 1e-3,
4993 "{}",
4994 circles[0].radius()
4995 );
4996 }
4997
4998 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
5000 curves
5001 .iter()
5002 .filter_map(|c| match c {
5003 ExactIntersectionCurve::Circle(circle) => Some(circle),
5004 _ => None,
5005 })
5006 .collect()
5007 }
5008
5009 fn worst_off(
5012 circles: &[&Circle3D],
5013 torus: &ToroidalSurface,
5014 other: impl Fn(Point3) -> f64,
5015 ) -> f64 {
5016 let mut worst = 0.0_f64;
5017 for circle in circles {
5018 for k in 0..16 {
5019 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
5020 let q = p - torus.center();
5021 let along = q.dot(torus.z_axis());
5022 let rho = (q - torus.z_axis() * along).length();
5023 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
5024 worst = worst.max(off).max(other(p).abs());
5025 }
5026 }
5027 worst
5028 }
5029
5030 #[test]
5031 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
5032 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5033 for height in [0.0, 1.0] {
5034 let centre = Point3::new(0.0, 0.0, height);
5035 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
5036 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
5037 let circles = circles_of(&curves);
5038 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
5039 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
5040 assert!(worst < 1e-9, "height {height}: {worst}");
5041 }
5042 }
5043
5044 #[test]
5045 fn exact_sphere_torus_misses_touches_and_defers() {
5046 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5047 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
5048 assert!(
5049 exact_sphere_torus(&ball(0.0, 1.0), &torus)
5050 .unwrap()
5051 .unwrap()
5052 .is_empty(),
5053 "a small ball in the hole misses"
5054 );
5055 assert!(
5056 exact_sphere_torus(&ball(0.0, 2.5), &torus)
5057 .unwrap()
5058 .is_none(),
5059 "a ball touching the inner equator defers"
5060 );
5061 assert!(
5062 exact_sphere_torus(&ball(1.0, 3.0), &torus)
5063 .unwrap()
5064 .is_none(),
5065 "a ball off the axis defers"
5066 );
5067 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5068 Point3::new(0.0, 0.0, 0.0),
5069 1.0,
5070 2.0,
5071 Vec3::new(0.0, 0.0, 1.0),
5072 Vec3::new(1.0, 0.0, 0.0),
5073 )
5074 .unwrap();
5075 assert!(
5076 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
5077 .unwrap()
5078 .is_none()
5079 );
5080 }
5081
5082 #[test]
5083 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
5084 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
5085 let z = Vec3::new(0.0, 0.0, 1.0);
5086 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
5087 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
5088 let circles = circles_of(&curves);
5089 assert_eq!((curves.len(), circles.len()), (2, 2));
5090 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
5091 assert!(worst < 1e-9, "{worst}");
5092 assert!(
5093 exact_cylinder_torus(&rod(2.0), &torus)
5094 .unwrap()
5095 .unwrap()
5096 .is_empty(),
5097 "a rod clear in the hole misses"
5098 );
5099 for (wall, label) in [(5.5, "outer"), (2.5, "inner")] {
5100 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5101 let circles = circles_of(&curves);
5102 assert_eq!(circles.len(), 1, "a wall touching the {label} equator");
5103 assert!(circles[0].center().z().abs() < 1e-12, "{label}");
5104 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - wall);
5105 assert!(worst < 1e-9, "{label}: {worst}");
5106 }
5107 assert!(
5108 exact_cylinder_torus(&rod(5.5 + 1e-6), &torus)
5109 .unwrap()
5110 .unwrap()
5111 .is_empty(),
5112 "a wall clear of the tube by more than the tolerance misses it"
5113 );
5114 assert_eq!(
5115 exact_cylinder_torus(&rod(5.5 - 1e-6), &torus)
5116 .unwrap()
5117 .unwrap()
5118 .len(),
5119 2,
5120 "a wall into the tube by more than the tolerance crosses it twice"
5121 );
5122 for wall in [5.5 - 5e-8, 5.5 + 5e-8] {
5126 let curves = exact_cylinder_torus(&rod(wall), &torus).unwrap().unwrap();
5127 assert_eq!(circles_of(&curves).len(), 1, "a wall at {wall}");
5128 }
5129 let tilted =
5130 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
5131 .unwrap();
5132 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
5133 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
5134 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
5135 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5136 Point3::new(0.0, 0.0, 0.0),
5137 1.0,
5138 2.0,
5139 z,
5140 Vec3::new(1.0, 0.0, 0.0),
5141 )
5142 .unwrap();
5143 assert!(
5144 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
5145 "a spindle torus's inner lemon also meets the rod"
5146 );
5147 let spindle = ToroidalSurface::with_axis_and_ref_dir(
5150 Point3::new(0.0, 0.0, 4.7),
5151 0.1,
5152 2.45,
5153 z,
5154 Vec3::new(1.0, 0.0, 0.0),
5155 )
5156 .unwrap();
5157 let curves = exact_cylinder_torus(&rod(2.55), &spindle).unwrap().unwrap();
5158 let circles = circles_of(&curves);
5159 assert_eq!(circles.len(), 1);
5160 assert!((circles[0].center().z() - 4.7).abs() < 1e-12);
5161 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.55);
5162 assert!(worst < 1e-9, "{worst}");
5163 let curves = exact_cylinder_torus(&rod(2.4), &spindle).unwrap().unwrap();
5164 let circles = circles_of(&curves);
5165 assert_eq!(circles.len(), 2, "a wall into the spindle's outer tube");
5166 let worst = worst_off(&circles, &spindle, |p| p.x().hypot(p.y()) - 2.4);
5167 assert!(worst < 1e-9, "{worst}");
5168 }
5169
5170 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
5173 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
5174 let cyl =
5175 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
5176 .unwrap();
5177 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
5178 .unwrap()
5179 .unwrap();
5180 let mut worst: f64 = 0.0;
5181 for c in &curves {
5182 for ip in &c.points {
5183 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
5184 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
5185 worst = worst
5186 .max((on_sphere - ip.point).length())
5187 .max((on_cylinder - ip.point).length());
5188 }
5189 let (t0, t1) = c.curve.domain();
5190 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
5191 for k in 0..=400 {
5192 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
5193 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
5194 let on_cylinder = ((p.x() - cylinder_origin.x())
5195 .hypot(p.y() - cylinder_origin.y())
5196 - cylinder_radius)
5197 .abs();
5198 worst = worst.max(on_sphere).max(on_cylinder);
5199 }
5200 }
5201 (curves.len(), worst)
5202 }
5203
5204 #[test]
5207 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
5208 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
5209 assert_eq!(count, 2);
5210 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
5211 }
5212
5213 #[test]
5215 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
5216 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
5217 assert_eq!(count, 1);
5218 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
5219 }
5220
5221 #[test]
5222 fn disjoint_cylinders_no_intersection() {
5223 let cyl_a =
5224 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5225 .unwrap();
5226 let cyl_b =
5227 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
5228 .unwrap();
5229
5230 let curves = intersect_analytic_analytic(
5231 AnalyticSurface::Cylinder(&cyl_a),
5232 AnalyticSurface::Cylinder(&cyl_b),
5233 16,
5234 )
5235 .unwrap();
5236
5237 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
5238 }
5239
5240 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
5244 use crate::traits::ParametricCurve;
5245 match curve {
5246 ExactIntersectionCurve::Circle(c) => (0..=64)
5247 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5248 .collect(),
5249 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5250 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5251 .collect(),
5252 ExactIntersectionCurve::Points(pts) => pts.clone(),
5253 }
5254 }
5255
5256 fn assert_on_plane_and_cone(
5259 curves: &[ExactIntersectionCurve],
5260 cone: &ConicalSurface,
5261 n: Vec3,
5262 d: f64,
5263 z_bound: (f64, f64),
5264 ) {
5265 assert!(!curves.is_empty(), "expected at least one section curve");
5266 let mut total = 0;
5267 for curve in curves {
5268 for p in collect_points(curve) {
5269 total += 1;
5270 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5271 assert!(
5272 plane_err < 1e-9,
5273 "point off plane by {plane_err:.2e}: {p:?}"
5274 );
5275 let (u, v) = cone.project_point(p);
5276 let q = cone.evaluate(u, v);
5277 let cone_err =
5278 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5279 .sqrt();
5280 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5281 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5282 assert!(
5283 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5284 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5285 p.z()
5286 );
5287 }
5288 }
5289 assert!(total >= 8, "too few section points ({total})");
5290 }
5291
5292 #[test]
5293 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5294 let cone = ConicalSurface::new(
5298 Point3::new(0.0, 0.0, 0.0),
5299 Vec3::new(0.0, 0.0, 1.0),
5300 std::f64::consts::FRAC_PI_4,
5301 )
5302 .unwrap();
5303 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5304 let d = n.z() * 5.0;
5306 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5307 assert!(
5308 curves
5309 .iter()
5310 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5311 "oblique steep plane × cone must yield an exact Ellipse"
5312 );
5313 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5315 }
5316
5317 #[test]
5318 fn oblique_plane_cone_wrong_nappe_is_empty() {
5319 let cone = ConicalSurface::new(
5323 Point3::new(0.0, 0.0, 0.0),
5324 Vec3::new(0.0, 0.0, 1.0),
5325 std::f64::consts::FRAC_PI_4,
5326 )
5327 .unwrap();
5328 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5329 let d = n.z() * -5.0;
5330 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5331 assert!(
5332 curves.is_empty(),
5333 "plane on the phantom-nappe side must yield no real curve, got {}",
5334 curves.len()
5335 );
5336 }
5337
5338 #[test]
5339 fn oblique_plane_cone_parabola_on_both_single_branch() {
5340 let cone = ConicalSurface::new(
5343 Point3::new(0.0, 0.0, 0.0),
5344 Vec3::new(0.0, 0.0, 1.0),
5345 std::f64::consts::FRAC_PI_4,
5346 )
5347 .unwrap();
5348 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5349 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5351 assert_eq!(
5352 curves.len(),
5353 1,
5354 "a parabola is a single branch, got {}",
5355 curves.len()
5356 );
5357 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5359 }
5360
5361 #[test]
5362 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5363 let cone = ConicalSurface::new(
5371 Point3::new(-59.0, -59.0, 15.85),
5372 Vec3::new(0.0, 0.0, -1.0),
5373 std::f64::consts::FRAC_PI_4,
5374 )
5375 .unwrap();
5376 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5377 let d = -58.360_56;
5378 let cos_theta = n.dot(cone.axis()).abs();
5379 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5380 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5381 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5384 for c in &curves {
5386 assert!(
5387 matches!(c, ExactIntersectionCurve::Points(_)),
5388 "hyperbola must be sampled Points, not a closed conic"
5389 );
5390 }
5391 }
5392}