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(
97 torus: &ToroidalSurface,
98 normal: Vec3,
99 d: f64,
100) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
101 let len = normal.length();
102 let n = normal.normalize()?;
103 let d = d / len;
104 let axis = torus.z_axis();
105 let center = torus.center();
106 let (big, small) = (torus.major_radius(), torus.minor_radius());
107 let height = d - dot_np(n, center);
108 let along = n.dot(axis);
109 if along.abs() > 1.0 - 1e-10 {
110 if height.abs() >= small - 1e-10 * small {
111 return Ok(if height.abs() > small + 1e-10 * small {
112 Some(Vec::new())
113 } else {
114 None
115 });
116 }
117 let reach = small.mul_add(small, -(height * height)).sqrt();
118 if big - reach <= 1e-10 * big {
119 return Ok(None);
120 }
121 let middle = center + n * height;
122 return Ok(Some(vec![
123 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big + reach)?),
124 ExactIntersectionCurve::Circle(Circle3D::new(middle, n, big - reach)?),
125 ]));
126 }
127 if along.abs() < 1e-10 && height.abs() < 1e-10 * (big + small) {
128 let out = axis.cross(n).normalize()?;
129 return Ok(Some(vec![
130 ExactIntersectionCurve::Circle(Circle3D::new(center + out * big, n, small)?),
131 ExactIntersectionCurve::Circle(Circle3D::new(center - out * big, n, small)?),
132 ]));
133 }
134 Ok(None)
135}
136
137fn exact_plane_cylinder(
143 cyl: &CylindricalSurface,
144 normal: Vec3,
145 d: f64,
146) -> Result<Vec<ExactIntersectionCurve>, MathError> {
147 let axis = cyl.axis();
148 let cos_theta = normal.dot(axis).abs();
149 let r = cyl.radius();
150
151 if cos_theta < 1e-10 {
152 let chains = sample_plane_cylinder(cyl, normal, d)?;
155 return Ok(chains
156 .into_iter()
157 .map(ExactIntersectionCurve::Points)
158 .collect());
159 }
160
161 let n_dot_axis = normal.dot(axis);
164 let n_dot_origin = dot_np(normal, cyl.origin());
165 let t = (d - n_dot_origin) / n_dot_axis;
166 let center_on_axis = Point3::new(
167 cyl.origin().x() + t * axis.x(),
168 cyl.origin().y() + t * axis.y(),
169 cyl.origin().z() + t * axis.z(),
170 );
171
172 if cos_theta > 1.0 - 1e-10 {
173 let circle = Circle3D::new(center_on_axis, normal, r)?;
175 Ok(vec![ExactIntersectionCurve::Circle(circle)])
176 } else {
177 let semi_minor = r;
181 let semi_major = r / cos_theta;
182
183 let axis_proj = Vec3::new(
187 axis.x() - n_dot_axis * normal.x(),
188 axis.y() - n_dot_axis * normal.y(),
189 axis.z() - n_dot_axis * normal.z(),
190 );
191 let u_axis = axis_proj.normalize()?;
192 let v_axis = normal.cross(u_axis);
193
194 let ellipse = Ellipse3D::with_axes(
195 center_on_axis,
196 normal,
197 semi_major,
198 semi_minor,
199 u_axis,
200 v_axis,
201 )?;
202 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
203 }
204}
205
206fn exact_plane_sphere(
210 sphere: &SphericalSurface,
211 normal: Vec3,
212 d: f64,
213) -> Result<Vec<ExactIntersectionCurve>, MathError> {
214 let h = dot_np(normal, sphere.center()) - d;
215 let r = sphere.radius();
216
217 if h.abs() > r - 1e-10 {
218 return Ok(vec![]);
219 }
220
221 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
222 let circle_center = Point3::new(
223 h.mul_add(-normal.x(), sphere.center().x()),
224 h.mul_add(-normal.y(), sphere.center().y()),
225 h.mul_add(-normal.z(), sphere.center().z()),
226 );
227
228 let circle = Circle3D::new(circle_center, normal, circle_r)?;
229 Ok(vec![ExactIntersectionCurve::Circle(circle)])
230}
231
232fn exact_plane_cone(
241 cone: &ConicalSurface,
242 normal: Vec3,
243 d: f64,
244 reach: f64,
245) -> Result<Vec<ExactIntersectionCurve>, MathError> {
246 let axis = cone.axis();
247 let cos_theta = normal.dot(axis).abs();
248 let half_angle = cone.half_angle();
249
250 if cos_theta > 1.0 - 1e-10 {
251 let n_dot_axis = normal.dot(axis);
254 let n_dot_apex = dot_np(normal, cone.apex());
255 let t = (d - n_dot_apex) / n_dot_axis;
256
257 if t.abs() < 1e-10 {
262 return Ok(vec![]);
263 }
264
265 let center = Point3::new(
266 cone.apex().x() + t * axis.x(),
267 cone.apex().y() + t * axis.y(),
268 cone.apex().z() + t * axis.z(),
269 );
270 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
274 if circle_r < 1e-15 {
275 return Ok(vec![]);
276 }
277
278 let circle = Circle3D::new(center, normal, circle_r)?;
279 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
280 }
281
282 let c = normal.dot(axis);
294 let p2 = (1.0 - c * c).max(0.0);
295 let p = p2.sqrt();
296 let k = half_angle.sin().powi(2);
297 let a_coeff = p2 - k;
298
299 let m = Vec3::new(
301 axis.x() - c * normal.x(),
302 axis.y() - c * normal.y(),
303 axis.z() - c * normal.z(),
304 );
305 let m_len = m.length();
306 if m_len < 1e-12 {
307 let chains = sample_plane_cone(cone, normal, d, reach)?;
310 return Ok(chains
311 .into_iter()
312 .map(ExactIntersectionCurve::Points)
313 .collect());
314 }
315 let e1 = m * (1.0 / m_len);
316 let e2 = normal.cross(e1);
317 let apex = cone.apex();
318 let e = d - dot_np(normal, apex);
319
320 if a_coeff < -1e-9 {
323 let abs_a = -a_coeff; if e * c < 0.0 {
330 return Ok(vec![]);
331 }
332 let s_c = e * c * p / abs_a;
335 let rhs = e * e * k * (1.0 - k) / abs_a;
336 if rhs <= 0.0 {
337 return Ok(vec![]);
338 }
339 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
342 return Ok(vec![]);
343 }
344 let center = apex + normal * e + e1 * s_c;
345 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
346 (semi_s, semi_t, e1, e2)
347 } else {
348 (semi_t, semi_s, e2, e1)
349 };
350 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
351 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
352 }
353
354 let chains = sample_plane_cone(cone, normal, d, reach)?;
357 Ok(chains
358 .into_iter()
359 .map(ExactIntersectionCurve::Points)
360 .collect())
361}
362
363#[allow(clippy::many_single_char_names)]
378pub fn plane_cone_conic_arc(
379 cone: &ConicalSurface,
380 normal: Vec3,
381 d: f64,
382 from: Point3,
383 to: Point3,
384) -> Result<Option<NurbsCurve>, MathError> {
385 let len = normal.length();
386 if len < 1e-15 {
387 return Err(MathError::ZeroVector);
388 }
389 let (normal, d) = (normal * (1.0 / len), d / len);
390 let axis = cone.axis();
391 let c = normal.dot(axis);
392 let p2 = (1.0 - c * c).max(0.0);
393 let p = p2.sqrt();
394 let k = cone.half_angle().sin().powi(2);
395 let a_coeff = p2 - k;
396 let m = Vec3::new(
397 axis.x() - c * normal.x(),
398 axis.y() - c * normal.y(),
399 axis.z() - c * normal.z(),
400 );
401 let m_len = m.length();
402 if m_len < 1e-12 || a_coeff < -1e-9 {
403 return Ok(None);
404 }
405 let e1 = m * (1.0 / m_len);
406 let e2 = normal.cross(e1);
407 let apex = cone.apex();
408 let e = d - dot_np(normal, apex);
409 let origin = apex + normal * e;
410 let plane_st = |q: Point3| {
411 let w = q - origin;
412 (w.dot(e1), w.dot(e2))
413 };
414 let ((s0, t0), (s1, t1)) = (plane_st(from), plane_st(to));
415 let scale = s0.abs().max(t0.abs()).max(s1.abs()).max(t1.abs()).max(1.0);
416 if e.abs() < 1e-9 * scale || (from - to).length() <= 1e-9 * scale {
417 return Ok(None);
418 }
419 let point = |s: f64, t: f64| origin + e1 * s + e2 * t;
420 let on_curve = |q: Point3, r: Point3| (q - r).length() <= 1e-6 * scale;
421 let (control, weights) = if a_coeff.abs() <= 1e-9 {
422 let lin = 2.0 * e * c * p;
424 if lin.abs() < 1e-12 * scale {
425 return Ok(None);
426 }
427 let (alpha, beta) = (k / lin, -e * e * (c * c - k) / lin);
428 if !on_curve(point(alpha * t0 * t0 + beta, t0), from)
429 || !on_curve(point(alpha * t1 * t1 + beta, t1), to)
430 {
431 return Ok(None);
432 }
433 let mid = point(alpha * t0 * t1 + beta, 0.5 * (t0 + t1));
434 (vec![from, mid, to], vec![1.0; 3])
435 } else {
436 let s_c = -e * c * p / a_coeff;
438 let r = e * e * k * (1.0 - k) / a_coeff;
439 if r <= 0.0 {
440 return Ok(None);
441 }
442 let (a, b) = ((r / a_coeff).sqrt(), (r / k).sqrt());
443 let (x0, x1) = (s0 - s_c, s1 - s_c);
444 if x0 * x1 <= 0.0 {
445 return Ok(None);
446 }
447 let side = x0.signum();
448 let hyperbola = |phi: f64| point(s_c + side * a * phi.cosh(), b * phi.sinh());
449 let (phi0, phi1) = ((t0 / b).asinh(), (t1 / b).asinh());
450 if !on_curve(hyperbola(phi0), from) || !on_curve(hyperbola(phi1), to) {
451 return Ok(None);
452 }
453 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
454 let pieces = ((phi1 - phi0).abs().ceil() as usize).max(1);
455 let mut control = vec![from];
456 let mut weights = vec![1.0];
457 for i in 0..pieces {
458 #[allow(clippy::cast_precision_loss)]
459 let (fa, fb) = (i as f64 / pieces as f64, (i + 1) as f64 / pieces as f64);
460 let (pa, pb) = (phi0 + (phi1 - phi0) * fa, phi0 + (phi1 - phi0) * fb);
461 let (mid, half) = (0.5 * (pa + pb), 0.5 * (pb - pa));
462 let w = half.cosh();
463 control.push(point(s_c + side * a * mid.cosh() / w, b * mid.sinh() / w));
464 weights.push(w);
465 control.push(if i + 1 == pieces { to } else { hyperbola(pb) });
466 weights.push(1.0);
467 }
468 (control, weights)
469 };
470 let pieces = (control.len() - 1) / 2;
471 let mut knots = vec![0.0; 3];
472 for i in 1..pieces {
473 #[allow(clippy::cast_precision_loss)]
474 knots.extend([i as f64; 2]);
475 }
476 #[allow(clippy::cast_precision_loss)]
477 knots.extend([pieces as f64; 3]);
478 let curve = NurbsCurve::new(2, knots, control, weights)?;
479 let (sin_a, cos_a) = cone.half_angle().sin_cos();
484 let off_cone = |q: Point3| {
485 let w = q - apex;
486 let h = w.dot(axis);
487 (w - axis * h)
488 .length()
489 .mul_add(sin_a, -(h.abs() * cos_a))
490 .abs()
491 };
492 for i in 0..pieces {
493 for f in [0.25, 0.5, 0.75] {
494 #[allow(clippy::cast_precision_loss)]
495 if off_cone(curve.evaluate(i as f64 + f)) > 1e-9 * scale {
496 return Ok(None);
497 }
498 }
499 }
500 Ok(Some(curve))
501}
502
503#[derive(Clone, Copy)]
505pub enum AnalyticSurface<'a> {
506 Cylinder(&'a CylindricalSurface),
508 Cone(&'a ConicalSurface),
510 Sphere(&'a SphericalSurface),
512 Torus(&'a ToroidalSurface),
514}
515
516fn dot_np(n: Vec3, p: Point3) -> f64 {
518 n.dot(Vec3::new(p.x(), p.y(), p.z()))
519}
520
521pub fn intersect_plane_analytic(
529 surface: AnalyticSurface<'_>,
530 normal: Vec3,
531 d: f64,
532) -> Result<Vec<IntersectionCurve>, MathError> {
533 match surface {
534 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
535 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
536 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
537 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
538 }
539}
540
541pub fn sample_plane_analytic(
552 surface: AnalyticSurface<'_>,
553 normal: Vec3,
554 d: f64,
555) -> Result<Vec<Vec<Point3>>, MathError> {
556 match surface {
557 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
558 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d, 0.0),
559 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
560 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
561 }
562}
563
564#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
566fn sample_plane_cylinder(
567 cyl: &CylindricalSurface,
568 normal: Vec3,
569 d: f64,
570) -> Result<Vec<Vec<Point3>>, MathError> {
571 let n_samples = 64_usize;
572 let mut points = Vec::with_capacity(n_samples + 1);
573
574 for i in 0..=n_samples {
575 let u = TAU * (i as f64) / (n_samples as f64);
576 let base = cyl.evaluate(u, 0.0);
577 let n_dot_axis = normal.dot(cyl.axis());
578 let n_dot_base = dot_np(normal, base);
579
580 if n_dot_axis.abs() < 1e-12 {
581 if (n_dot_base - d).abs() < 1e-6 {
582 points.push(base);
583 }
584 } else {
585 let v = (d - n_dot_base) / n_dot_axis;
586 if v.abs() <= 100.0 {
587 points.push(cyl.evaluate(u, v));
588 }
589 }
590 }
591
592 if points.len() < 2 {
593 Ok(vec![])
594 } else {
595 Ok(vec![points])
596 }
597}
598
599#[allow(clippy::cast_precision_loss)]
601fn sample_plane_sphere(
602 sphere: &SphericalSurface,
603 normal: Vec3,
604 d: f64,
605) -> Result<Vec<Vec<Point3>>, MathError> {
606 let h = dot_np(normal, sphere.center()) - d;
607 let r = sphere.radius();
608
609 if h.abs() > r - 1e-10 {
610 return Ok(vec![]);
611 }
612
613 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
614 let circle_center = Point3::new(
615 h.mul_add(-normal.x(), sphere.center().x()),
616 h.mul_add(-normal.y(), sphere.center().y()),
617 h.mul_add(-normal.z(), sphere.center().z()),
618 );
619
620 let basis = Frame3::from_normal(circle_center, normal)?;
621 let u_dir = basis.x;
622 let v_dir = basis.y;
623
624 let n_samples = 64_usize;
625 let mut points = Vec::with_capacity(n_samples + 1);
626
627 for i in 0..=n_samples {
628 let theta = TAU * (i as f64) / (n_samples as f64);
629 let (sin_t, cos_t) = theta.sin_cos();
630 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
631 }
632
633 Ok(vec![points])
634}
635
636#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
648fn sample_plane_cone(
649 cone: &ConicalSurface,
650 normal: Vec3,
651 d: f64,
652 reach: f64,
653) -> Result<Vec<Vec<Point3>>, MathError> {
654 let apex = cone.apex();
655 let n_dot_apex = dot_np(normal, apex);
656 let e = d - n_dot_apex;
657
658 let n_samples = 512_usize;
662 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
663 let mut v_min = f64::INFINITY;
664 for i in 0..n_samples {
665 let u = TAU * (i as f64) / (n_samples as f64);
666 let g = cone.evaluate(u, 1.0) - apex;
667 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
668 if n_dot_g.abs() < 1e-12 {
669 vs.push(None);
670 continue;
671 }
672 let v = e / n_dot_g;
673 if v >= -1e-12 {
674 let v = v.max(0.0);
675 v_min = v_min.min(v);
676 vs.push(Some(v));
677 } else {
678 vs.push(None);
679 }
680 }
681
682 if !v_min.is_finite() {
683 return Ok(Vec::new());
684 }
685
686 let v_max = (8.0 * v_min).max(v_min + 4.0).max(reach);
695
696 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
699
700 let point_at = |u: f64, v: f64| -> Point3 {
701 let g = cone.evaluate(u, 1.0) - apex;
702 apex + g * v
703 };
704 #[allow(clippy::cast_precision_loss)]
705 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
706 let n_dot_g_at = |u: f64| -> f64 {
707 let g = cone.evaluate(u, 1.0) - apex;
708 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
709 };
710
711 if kept.iter().all(Option::is_some) {
712 let mut pts: Vec<Point3> = kept
714 .iter()
715 .enumerate()
716 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
717 .collect();
718 if let Some(&first) = pts.first() {
719 pts.push(first);
720 }
721 return Ok(vec![pts]);
722 }
723
724 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
733 let Some(v_end) = kept[i_end] else {
734 return Vec::new();
735 };
736 let u_end = u_of(i_end);
737 #[allow(clippy::cast_precision_loss)]
738 let pitch = TAU / (n_samples as f64);
739 let u_next = if forward {
740 u_end + pitch
741 } else {
742 u_end - pitch
743 };
744 let target = e / v_max;
745 let h_end = n_dot_g_at(u_end) - target;
746 let h_next = n_dot_g_at(u_next) - target;
747 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
748 return Vec::new();
749 }
750 let (mut lo, mut hi) = (u_end, u_next);
751 for _ in 0..60 {
752 let mid = f64::midpoint(lo, hi);
753 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
754 lo = mid;
755 } else {
756 hi = mid;
757 }
758 }
759 let u_star = f64::midpoint(lo, hi);
760 let tail_n = 8_usize;
761 (1..=tail_n)
762 .filter_map(|k| {
763 #[allow(clippy::cast_precision_loss)]
764 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
765 let ng = n_dot_g_at(u);
766 if ng.abs() < 1e-12 {
767 return None;
768 }
769 let v = e / ng;
770 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
771 })
772 .collect()
773 };
774
775 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
778 let mut chains: Vec<Vec<Point3>> = Vec::new();
779 let mut run: Vec<usize> = Vec::new();
780 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
781 if run.len() >= 2 {
782 let first = run[0];
783 let last = run[run.len() - 1];
784 let mut pts: Vec<Point3> = tail(first, false, &kept);
785 pts.reverse();
786 pts.extend(
787 run.iter()
788 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
789 );
790 pts.extend(tail(last, true, &kept));
791 chains.push(pts);
792 }
793 run.clear();
794 };
795 for k in 0..n_samples {
796 let idx = (gap + k) % n_samples;
797 if kept[idx].is_some() {
798 run.push(idx);
799 } else {
800 flush(&mut run, &mut chains);
801 }
802 }
803 flush(&mut run, &mut chains);
804 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
805}
806
807#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
813 torus: &ToroidalSurface,
814 normal: Vec3,
815 d: f64,
816) -> Result<Vec<Vec<Point3>>, MathError> {
817 Ok(plane_torus_loops(torus, normal, d, 128)
818 .into_iter()
819 .map(|run| run.into_iter().map(|p| p.point).collect())
820 .collect())
821}
822
823#[allow(clippy::cast_precision_loss)]
833pub fn intersect_plane_cylinder(
834 cyl: &CylindricalSurface,
835 normal: Vec3,
836 d: f64,
837) -> Result<Vec<IntersectionCurve>, MathError> {
838 let n_samples = 64_usize;
839 let mut points_3d = Vec::new();
840 let mut ipoints = Vec::new();
841
842 for i in 0..=n_samples {
843 let u = TAU * (i as f64) / (n_samples as f64);
844 let base = cyl.evaluate(u, 0.0);
847 let n_dot_axis = normal.dot(cyl.axis());
848 let n_dot_base = dot_np(normal, base);
849
850 if n_dot_axis.abs() < 1e-12 {
851 if (n_dot_base - d).abs() < 1e-6 {
853 let pt = base;
854 points_3d.push(pt);
855 ipoints.push(IntersectionPoint {
856 point: pt,
857 param1: (u, 0.0),
858 param2: (0.0, 0.0),
859 });
860 }
861 } else {
862 let v = (d - n_dot_base) / n_dot_axis;
863 if v.abs() <= 100.0 {
865 let pt = cyl.evaluate(u, v);
866 points_3d.push(pt);
867 ipoints.push(IntersectionPoint {
868 point: pt,
869 param1: (u, v),
870 param2: (0.0, 0.0),
871 });
872 }
873 }
874 }
875
876 build_curves_from_points(&points_3d, ipoints)
877}
878
879#[allow(clippy::cast_precision_loss)]
888pub fn intersect_plane_sphere(
889 sphere: &SphericalSurface,
890 normal: Vec3,
891 d: f64,
892) -> Result<Vec<IntersectionCurve>, MathError> {
893 let h = dot_np(normal, sphere.center()) - d;
894 let r = sphere.radius();
895
896 if h.abs() > r - 1e-10 {
898 return Ok(vec![]);
899 }
900
901 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
902 let circle_center = Point3::new(
903 h.mul_add(-normal.x(), sphere.center().x()),
904 h.mul_add(-normal.y(), sphere.center().y()),
905 h.mul_add(-normal.z(), sphere.center().z()),
906 );
907
908 let basis = Frame3::from_normal(circle_center, normal)?;
910 let u_dir = basis.x;
911 let v_dir = basis.y;
912
913 let n_samples = 64_usize;
914 let mut points_3d = Vec::new();
915 let mut ipoints = Vec::new();
916
917 for i in 0..=n_samples {
918 let theta = TAU * (i as f64) / (n_samples as f64);
919 let (sin_t, cos_t) = theta.sin_cos();
920 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
921 points_3d.push(pt);
922 ipoints.push(IntersectionPoint {
923 point: pt,
924 param1: (theta, 0.0),
925 param2: (0.0, 0.0),
926 });
927 }
928
929 build_curves_from_points(&points_3d, ipoints)
930}
931
932#[allow(clippy::cast_precision_loss)]
941pub fn intersect_plane_cone(
942 cone: &ConicalSurface,
943 normal: Vec3,
944 d: f64,
945) -> Result<Vec<IntersectionCurve>, MathError> {
946 let n_samples = 64_usize;
947 let mut points_3d = Vec::new();
948 let mut ipoints = Vec::new();
949
950 for i in 0..n_samples {
951 let u = TAU * (i as f64) / (n_samples as f64);
952 let apex = cone.apex();
955 let n_dot_apex = dot_np(normal, apex);
956 let p1 = cone.evaluate(u, 1.0);
958 let dir = p1 - apex;
959 let n_dot_dir = normal.dot(dir);
960
961 if n_dot_dir.abs() < 1e-12 {
962 continue;
963 }
964
965 let v = (d - n_dot_apex) / n_dot_dir;
966 if v.abs() > 1e-10 && v.abs() < 100.0 {
968 let pt = cone.evaluate(u, v);
969 points_3d.push(pt);
970 ipoints.push(IntersectionPoint {
971 point: pt,
972 param1: (u, v),
973 param2: (0.0, 0.0),
974 });
975 }
976 }
977
978 build_curves_from_points(&points_3d, ipoints)
979}
980
981#[allow(clippy::unnecessary_wraps)]
993pub fn intersect_plane_torus(
994 torus: &ToroidalSurface,
995 normal: Vec3,
996 d: f64,
997) -> Result<Vec<IntersectionCurve>, MathError> {
998 let mut curves = Vec::new();
1002 for ipts in plane_torus_loops(torus, normal, d, 128) {
1003 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
1004 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
1005 curves.push(IntersectionCurve {
1006 curve,
1007 points: ipts,
1008 });
1009 }
1010 }
1011
1012 Ok(curves)
1013}
1014
1015const PLANE_TORUS_LOOP_SAMPLES: (f64, f64) = (24.0, 512.0);
1018
1019#[allow(clippy::cast_precision_loss, clippy::too_many_lines)]
1041fn plane_torus_loops(
1042 torus: &ToroidalSurface,
1043 normal: Vec3,
1044 d: f64,
1045 n_v: usize,
1046) -> Vec<Vec<IntersectionPoint>> {
1047 let big_r = torus.major_radius();
1048 let small_r = torus.minor_radius();
1049 let a = normal.dot(torus.x_axis());
1050 let b = normal.dot(torus.y_axis());
1051 let c = normal.dot(torus.z_axis());
1052 let s = a.hypot(b);
1053 let phi = b.atan2(a);
1054 let d_local = d - dot_np(normal, torus.center());
1055 let point = |u: f64, v: f64| IntersectionPoint {
1056 point: torus.evaluate(u, v),
1057 param1: (u, v.rem_euclid(TAU)),
1058 param2: (0.0, 0.0),
1059 };
1060 let closed = |mut run: Vec<IntersectionPoint>| {
1061 run.push(run[0]);
1062 run
1063 };
1064
1065 if s < 1e-12 {
1067 if c.abs() < 1e-12 {
1068 return Vec::new();
1069 }
1070 let sin_v = d_local / (small_r * c);
1071 if sin_v.abs() > 1.0 + 1e-9 {
1072 return Vec::new();
1073 }
1074 let v0 = sin_v.clamp(-1.0, 1.0).asin();
1075 let v1 = std::f64::consts::PI - v0;
1076 let mut vs = vec![v0];
1077 if (v1 - v0).abs() > 1e-9 {
1079 vs.push(v1);
1080 }
1081 return vs
1082 .into_iter()
1083 .map(|v| {
1084 closed(
1085 (0..n_v)
1086 .map(|i| point(TAU * (i as f64) / (n_v as f64), v))
1087 .collect(),
1088 )
1089 })
1090 .collect();
1091 }
1092
1093 let step = TAU / (n_v as f64);
1096 let v_off = step * 0.5;
1097 let rhs_at = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1099 let branch = |v: f64, sign: f64| point(sign.mul_add(rhs_at(v).clamp(-1.0, 1.0).acos(), phi), v);
1100 let inside = |v: f64| rhs_at(v).abs() <= 1.0;
1101 let scan: Vec<f64> = (0..n_v).map(|i| (i as f64).mul_add(step, v_off)).collect();
1102 let touches = |lo: f64, hi: f64| {
1105 let golden = 0.5 * (5.0_f64.sqrt() - 1.0);
1106 let (mut lo, mut hi) = (lo, hi);
1107 for _ in 0..80 {
1108 let (m1, m2) = (hi - golden * (hi - lo), lo + golden * (hi - lo));
1109 if rhs_at(m1).abs() > rhs_at(m2).abs() {
1110 hi = m2;
1111 } else {
1112 lo = m1;
1113 }
1114 }
1115 1.0 - rhs_at(f64::midpoint(lo, hi)).abs() < 1e-12
1116 };
1117 let turn = |v_in: f64, v_out: f64| {
1119 let (mut lo, mut hi) = (v_in, v_out);
1120 for _ in 0..60 {
1121 let mid = f64::midpoint(lo, hi);
1122 if inside(mid) {
1123 lo = mid;
1124 } else {
1125 hi = mid;
1126 }
1127 }
1128 lo
1129 };
1130 let in_scan: Vec<bool> = scan.iter().map(|&v| inside(v)).collect();
1131 if in_scan.iter().all(|&x| x) {
1132 let touching = scan.iter().any(|&v| touches(v, v + step));
1133 return [1.0, -1.0]
1134 .into_iter()
1135 .map(|sign| {
1136 let run: Vec<IntersectionPoint> = scan.iter().map(|&v| branch(v, sign)).collect();
1137 if touching { run } else { closed(run) }
1138 })
1139 .collect();
1140 }
1141 let Some(first) = (0..n_v).find(|&i| in_scan[i] && !in_scan[(i + n_v - 1) % n_v]) else {
1142 return Vec::new();
1143 };
1144 let mut loops = Vec::new();
1145 let mut k = 0;
1146 while k < n_v {
1147 let i = (first + k) % n_v;
1148 if !in_scan[i] {
1149 k += 1;
1150 continue;
1151 }
1152 let len = (0..n_v - k).take_while(|&j| in_scan[(i + j) % n_v]).count();
1154 let v_a = scan[i];
1155 let v_b = ((len - 1) as f64).mul_add(step, v_a);
1156 let run_v = |j: usize| (j as f64).mul_add(step, v_a);
1157 let (t_lo, t_hi) = (turn(v_a, v_a - step), turn(v_b, v_b + step));
1158 let touching = (0..len - 1).any(|j| touches(run_v(j), run_v(j + 1)));
1159 let (u_lo, u_hi) = (0..len)
1167 .map(run_v)
1168 .chain([t_lo, t_hi])
1169 .map(|v| rhs_at(v).clamp(-1.0, 1.0).acos())
1170 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), u| {
1171 (lo.min(u), hi.max(u))
1172 });
1173 let m = (len as f64)
1174 .max((n_v as f64) * (u_hi - u_lo) / std::f64::consts::PI)
1175 .max(PLANE_TORUS_LOOP_SAMPLES.0)
1176 .min(PLANE_TORUS_LOOP_SAMPLES.1)
1177 .ceil();
1178 let at = |k: f64| {
1179 let f = 0.5 * (1.0 - (std::f64::consts::PI * k / m).cos());
1180 (t_hi - t_lo).mul_add(f, t_lo)
1181 };
1182 let steps = m as usize;
1183 let mut pts: Vec<IntersectionPoint> =
1184 (0..=steps).map(|k| branch(at(k as f64), 1.0)).collect();
1185 pts.extend((1..steps).rev().map(|k| branch(at(k as f64), -1.0)));
1186 loops.push(if touching { pts } else { closed(pts) });
1187 k += len;
1188 }
1189 loops
1190}
1191
1192#[allow(clippy::cast_precision_loss)]
1201fn plane_torus_winding_loops(
1202 torus: &ToroidalSurface,
1203 normal: Vec3,
1204 d: f64,
1205 n_v: usize,
1206) -> Option<Vec<Vec<Point3>>> {
1207 let big_r = torus.major_radius();
1208 let small_r = torus.minor_radius();
1209 let a = normal.dot(torus.x_axis());
1210 let b = normal.dot(torus.y_axis());
1211 let c = normal.dot(torus.z_axis());
1212 let s = a.hypot(b);
1213 if s < 1e-12 * normal.length() || small_r >= big_r {
1214 return None;
1215 }
1216 let phi = b.atan2(a);
1217 let d_local = d - dot_np(normal, torus.center());
1218 let rhs = |v: f64| (d_local - small_r * c * v.sin()) / (s * small_r.mul_add(v.cos(), big_r));
1219 let dense = 8 * n_v;
1220 if (0..dense).any(|i| rhs(TAU * i as f64 / dense as f64).abs() > 1.0 - 1e-3) {
1221 return None;
1222 }
1223 let mut loops = [Vec::with_capacity(n_v + 1), Vec::with_capacity(n_v + 1)];
1224 for i in 0..n_v {
1225 let v = TAU * i as f64 / n_v as f64;
1226 let delta = rhs(v).acos();
1227 loops[0].push(torus.evaluate(phi + delta, v));
1228 loops[1].push(torus.evaluate(phi - delta, v));
1229 }
1230 Some(
1231 loops
1232 .into_iter()
1233 .map(|mut run| {
1234 run.push(run[0]);
1235 run
1236 })
1237 .collect(),
1238 )
1239}
1240
1241#[must_use]
1254pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1255 let c = torus.center();
1256 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1257 let big_r = torus.major_radius();
1258 let small_r = torus.minor_radius();
1259
1260 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1262 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1263 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1264 let (c0, c1) = (za.dot(o), za.dot(dir));
1265
1266 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1268 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1269 let g0 = a0.mul_add(
1270 a0,
1271 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1272 );
1273
1274 let four_rr = 4.0 * big_r * big_r;
1276 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1277 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1278 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1279
1280 let e4 = g2 * g2;
1282 let e3 = 2.0 * g2 * g1;
1283 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1284 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1285 let e0 = g0.mul_add(g0, -h0);
1286
1287 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1288 let impl_f = |t: f64| -> f64 {
1290 let p = origin + dir * t;
1291 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1292 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1293 (a.hypot(b) - big_r).hypot(cc) - small_r
1294 };
1295 for t in &mut roots {
1296 let eps = 1e-7;
1297 let f = impl_f(*t);
1298 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1299 if df.abs() > 1e-12 {
1300 *t -= f / df;
1301 }
1302 }
1303 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1304 roots
1305}
1306
1307fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1310 if c4.abs() < 1e-14 {
1312 return real_roots_cubic(c3, c2, c1, c0);
1313 }
1314 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1316 let eval = |z: Complex| -> Complex {
1317 let mut acc = Complex::new(1.0, 0.0);
1319 acc = acc * z + Complex::new(a, 0.0);
1320 acc = acc * z + Complex::new(b, 0.0);
1321 acc = acc * z + Complex::new(c, 0.0);
1322 acc * z + Complex::new(d, 0.0)
1323 };
1324 let seed = Complex::new(0.4, 0.9);
1326 let mut r = [
1327 Complex::new(1.0, 0.0),
1328 seed,
1329 seed * seed,
1330 seed * seed * seed,
1331 ];
1332 for _ in 0..100 {
1333 let mut max_step = 0.0_f64;
1334 for i in 0..4 {
1335 let mut denom = Complex::new(1.0, 0.0);
1336 for j in 0..4 {
1337 if i != j {
1338 denom = denom * (r[i] - r[j]);
1339 }
1340 }
1341 if denom.norm() < 1e-300 {
1342 continue;
1343 }
1344 let step = eval(r[i]) / denom;
1345 r[i] = r[i] - step;
1346 max_step = max_step.max(step.norm());
1347 }
1348 if max_step < 1e-14 {
1349 break;
1350 }
1351 }
1352 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1359 let mut out: Vec<f64> = Vec::new();
1360 for z in r {
1361 if z.im.abs() >= 1e-7 {
1362 continue;
1363 }
1364 let x = z.re;
1365 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1368 if p_real(x).abs() > 1e-6 * scale {
1369 continue;
1370 }
1371 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1372 continue;
1373 }
1374 out.push(x);
1375 }
1376 out
1377}
1378
1379fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1381 if a.abs() < 1e-14 {
1382 return real_roots_quadratic(b, c, d);
1383 }
1384 let (b, c, d) = (b / a, c / a, d / a);
1386 let p = c - b * b / 3.0;
1387 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1388 let shift = -b / 3.0;
1389 let disc = q * q / 4.0 + p * p * p / 27.0;
1390 if disc > 1e-14 {
1391 let sq = disc.sqrt();
1392 let u = (-q / 2.0 + sq).cbrt();
1393 let v = (-q / 2.0 - sq).cbrt();
1394 vec![u + v + shift]
1395 } else if disc < -1e-14 {
1396 let m = 2.0 * (-p / 3.0).sqrt();
1398 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1399 (0..3)
1400 .map(|k| {
1401 m.mul_add(
1402 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1403 shift,
1404 )
1405 })
1406 .collect()
1407 } else {
1408 let u = (-q / 2.0).cbrt();
1410 vec![2.0 * u + shift, -u + shift]
1411 }
1412}
1413
1414fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1416 if a.abs() < 1e-14 {
1417 if b.abs() < 1e-14 {
1418 return Vec::new();
1419 }
1420 return vec![-c / b];
1421 }
1422 let disc = b * b - 4.0 * a * c;
1423 if disc < 0.0 {
1424 Vec::new()
1425 } else {
1426 let sq = disc.sqrt();
1427 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1428 }
1429}
1430
1431#[derive(Clone, Copy)]
1433struct Complex {
1434 re: f64,
1435 im: f64,
1436}
1437
1438impl Complex {
1439 const fn new(re: f64, im: f64) -> Self {
1440 Self { re, im }
1441 }
1442 fn norm(self) -> f64 {
1443 self.re.hypot(self.im)
1444 }
1445}
1446
1447impl std::ops::Add for Complex {
1448 type Output = Self;
1449 fn add(self, o: Self) -> Self {
1450 Self::new(self.re + o.re, self.im + o.im)
1451 }
1452}
1453
1454impl std::ops::Sub for Complex {
1455 type Output = Self;
1456 fn sub(self, o: Self) -> Self {
1457 Self::new(self.re - o.re, self.im - o.im)
1458 }
1459}
1460
1461impl std::ops::Mul for Complex {
1462 type Output = Self;
1463 fn mul(self, o: Self) -> Self {
1464 Self::new(
1465 self.re.mul_add(o.re, -(self.im * o.im)),
1466 self.re.mul_add(o.im, self.im * o.re),
1467 )
1468 }
1469}
1470
1471impl std::ops::Div for Complex {
1472 type Output = Self;
1473 fn div(self, o: Self) -> Self {
1474 let den = o.re.mul_add(o.re, o.im * o.im);
1475 Self::new(
1476 self.re.mul_add(o.re, self.im * o.im) / den,
1477 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1478 )
1479 }
1480}
1481
1482fn build_curves_from_points(
1486 points_3d: &[Point3],
1487 ipoints: Vec<IntersectionPoint>,
1488) -> Result<Vec<IntersectionCurve>, MathError> {
1489 if points_3d.len() < 2 {
1490 return Ok(vec![]);
1491 }
1492
1493 let degree = 3.min(points_3d.len() - 1);
1494 let curve = interpolate(points_3d, degree)?;
1495 Ok(vec![IntersectionCurve {
1496 curve,
1497 points: ipoints,
1498 }])
1499}
1500
1501#[allow(
1513 clippy::cast_precision_loss,
1514 clippy::too_many_lines,
1515 clippy::similar_names,
1516 clippy::unnecessary_wraps,
1517 clippy::type_complexity
1518)]
1519pub fn intersect_analytic_analytic(
1520 a: AnalyticSurface<'_>,
1521 b: AnalyticSurface<'_>,
1522 grid_res: usize,
1523) -> Result<Vec<IntersectionCurve>, MathError> {
1524 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1525}
1526
1527pub fn intersect_analytic_analytic_bounded(
1538 a: AnalyticSurface<'_>,
1539 b: AnalyticSurface<'_>,
1540 grid_res: usize,
1541 v_range_hint_a: Option<(f64, f64)>,
1542 v_range_hint_b: Option<(f64, f64)>,
1543) -> Result<Vec<IntersectionCurve>, MathError> {
1544 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, None)
1545}
1546
1547pub fn intersect_analytic_analytic_in_region(
1560 a: AnalyticSurface<'_>,
1561 b: AnalyticSurface<'_>,
1562 grid_res: usize,
1563 v_range_hint_a: Option<(f64, f64)>,
1564 v_range_hint_b: Option<(f64, f64)>,
1565 region: Aabb3,
1566) -> Result<Vec<IntersectionCurve>, MathError> {
1567 intersect_analytic_analytic_impl(a, b, grid_res, v_range_hint_a, v_range_hint_b, Some(region))
1568}
1569
1570fn intersect_analytic_analytic_impl(
1571 a: AnalyticSurface<'_>,
1572 b: AnalyticSurface<'_>,
1573 grid_res: usize,
1574 v_range_hint_a: Option<(f64, f64)>,
1575 v_range_hint_b: Option<(f64, f64)>,
1576 region: Option<Aabb3>,
1577) -> Result<Vec<IntersectionCurve>, MathError> {
1578 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1581 return Ok(result);
1582 }
1583
1584 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1585 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1586 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1587 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1588
1589 let diag_a = {
1591 let p00 = surf_a(u_range_a.0, v_range_a.0);
1592 let p11 = surf_a(u_range_a.1, v_range_a.1);
1593 (p00 - p11).length()
1594 };
1595 let diag_b = {
1596 let p00 = surf_b(u_range_b.0, v_range_b.0);
1597 let p11 = surf_b(u_range_b.1, v_range_b.1);
1598 (p00 - p11).length()
1599 };
1600 let char_size = diag_a.min(diag_b).max(0.1);
1601
1602 #[allow(clippy::type_complexity)]
1606 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1607 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1611 let mut min_dist = f64::INFINITY;
1612
1613 #[allow(clippy::cast_precision_loss)]
1614 for ia in 0..grid_res {
1615 for ja in 0..grid_res {
1616 let ua =
1617 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1618 let va =
1619 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1620
1621 let pa = surf_a(ua, va);
1622
1623 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1625 let pb = surf_b(ub, vb);
1626 let dist = (pa - pb).length();
1627 min_dist = min_dist.min(dist);
1628
1629 if dist < seed_threshold {
1630 let mid = Point3::new(
1635 (pa.x() + pb.x()) * 0.5,
1636 (pa.y() + pb.y()) * 0.5,
1637 (pa.z() + pb.z()) * 0.5,
1638 );
1639 seeds.push((mid, (ua, va), (ub, vb)));
1640 }
1641 }
1642 }
1643
1644 let reject_dist = (char_size / grid_res as f64) * 3.0;
1653 if min_dist > reject_dist {
1654 return Ok(vec![]);
1655 }
1656
1657 if seeds.is_empty() {
1658 return Ok(vec![]);
1659 }
1660
1661 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1665 let dedup_radius = march_step * 10.0;
1666 let mut unique_seeds = Vec::new();
1667 for seed in &seeds {
1668 let dominated = unique_seeds
1669 .iter()
1670 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1671 if !dominated {
1672 unique_seeds.push(*seed);
1673 }
1674 }
1675
1676 let region = region.map(|r| r.expanded(2.0 * char_size / grid_res as f64));
1681 if let Some(r) = region {
1682 for seed in &mut unique_seeds {
1683 let mut p = seed.0;
1684 for _ in 0..8 {
1685 let (ua, va) = project_analytic(&a, p, u_range_a, v_range_a);
1686 let pa = surf_a(ua, va);
1687 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1688 let pb = surf_b(ub, vb);
1689 p = Point3::new(
1690 (pa.x() + pb.x()) * 0.5,
1691 (pa.y() + pb.y()) * 0.5,
1692 (pa.z() + pb.z()) * 0.5,
1693 );
1694 if (pa - pb).length() < 1e-9 {
1695 break;
1696 }
1697 }
1698 seed.0 = p;
1699 }
1700 unique_seeds.retain(|seed| r.contains_point(seed.0));
1701 }
1702
1703 let mut curves = Vec::new();
1705 let mut used_seeds = vec![false; unique_seeds.len()];
1706
1707 for si in 0..unique_seeds.len() {
1708 if used_seeds[si] {
1709 continue;
1710 }
1711 used_seeds[si] = true;
1712
1713 let march_result = march_analytic_intersection(
1714 &a,
1715 &b,
1716 surf_a.as_ref(),
1717 norm_a.as_ref(),
1718 surf_b.as_ref(),
1719 norm_b.as_ref(),
1720 unique_seeds[si].0,
1721 u_range_a,
1722 v_range_a,
1723 u_range_b,
1724 v_range_b,
1725 march_step,
1726 is_u_periodic(&a),
1727 is_u_periodic(&b),
1728 region,
1729 );
1730
1731 if march_result.len() >= 2 {
1732 for (sj, other) in unique_seeds.iter().enumerate() {
1733 if !used_seeds[sj]
1734 && march_result
1735 .iter()
1736 .any(|p| (*p - other.0).length() < dedup_radius)
1737 {
1738 used_seeds[sj] = true;
1739 }
1740 }
1741
1742 let ipts: Vec<IntersectionPoint> = march_result
1743 .iter()
1744 .map(|&pt| IntersectionPoint {
1745 point: pt,
1746 param1: (0.0, 0.0),
1747 param2: (0.0, 0.0),
1748 })
1749 .collect();
1750
1751 let degree = 3.min(march_result.len() - 1);
1752 if let Ok(curve) = interpolate(&march_result, degree) {
1753 curves.push(IntersectionCurve {
1754 curve,
1755 points: ipts,
1756 });
1757 }
1758 }
1759 }
1760
1761 Ok(curves)
1762}
1763
1764#[allow(clippy::too_many_lines)]
1778fn try_algebraic_intersection(
1779 a: &AnalyticSurface<'_>,
1780 b: &AnalyticSurface<'_>,
1781 v_range_a: Option<(f64, f64)>,
1782 v_range_b: Option<(f64, f64)>,
1783) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1784 match (a, b) {
1785 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => Ok(
1786 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)?
1787 .or_else(|| ruling_cone_cylinder(cone, cyl, true)),
1788 ),
1789 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => Ok(
1790 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)?
1791 .or_else(|| ruling_cone_cylinder(cone, cyl, false)),
1792 ),
1793 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1794 algebraic_sphere_sphere(s1, s2).map(Some)
1795 }
1796 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1797 let axis_dot = c1.axis().dot(c2.axis()).abs();
1798 if axis_dot > 1.0 - 1e-10 {
1799 let delta = c2.origin() - c1.origin();
1801 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1802 let along = delta_vec.dot(c1.axis());
1803 let perp = (delta_vec - c1.axis() * along).length();
1804 if perp < 1e-8 {
1805 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1808 return Ok(None); }
1810 return Ok(Some(vec![])); }
1812 }
1813 algebraic_cylinder_cylinder(c1, c2)
1815 }
1816 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1818 algebraic_sphere_cylinder(s, c, true)
1819 }
1820 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1821 algebraic_sphere_cylinder(s, c, false)
1822 }
1823 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1824 (AnalyticSurface::Cone(cone), AnalyticSurface::Sphere(sphere)) => {
1825 Ok(ruling_cone_sphere(cone, sphere, true))
1826 }
1827 (AnalyticSurface::Sphere(sphere), AnalyticSurface::Cone(cone)) => {
1828 Ok(ruling_cone_sphere(cone, sphere, false))
1829 }
1830 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1831 Ok(parallel_axis_torus_cylinder(t, c, true)
1832 .or_else(|| ruling_torus_cylinder(t, c, true)))
1833 }
1834 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1835 Ok(parallel_axis_torus_cylinder(t, c, false)
1836 .or_else(|| ruling_torus_cylinder(t, c, false)))
1837 }
1838 _ => Ok(None),
1839 }
1840}
1841
1842fn parallel_axis_torus_cylinder(
1849 torus: &ToroidalSurface,
1850 cyl: &CylindricalSurface,
1851 torus_first: bool,
1852) -> Option<Vec<IntersectionCurve>> {
1853 let axis = torus.z_axis();
1854 let along = cyl.axis().dot(axis);
1855 if along.abs() < 1.0 - 1e-10 {
1856 return None;
1857 }
1858 let offset = cyl.origin() - torus.center();
1859 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1860 return None;
1861 }
1862 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1863 let roots = |u: f64| {
1864 let q = cyl.evaluate(u, 0.0) - torus.center();
1865 let height = q.dot(axis);
1866 let rho = (q - axis * height).length();
1867 let reach = minor * minor - (rho - major) * (rho - major);
1868 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1869 };
1870 let samples = ruling_samples(cyl, &roots);
1871 let loops = if samples.iter().all(Option::is_some) {
1872 closed_ruling_loops(&samples)
1873 } else {
1874 partial_ruling_loops(cyl, &roots, &samples)
1875 };
1876 if loops.is_empty() {
1877 return None;
1878 }
1879 Some(fit_ruling_loops(&loops, |p| {
1880 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1881 }))
1882}
1883
1884fn meridian_crossings(
1890 first: (f64, f64, f64),
1891 second: (f64, f64, f64),
1892 scale: f64,
1893) -> Option<Vec<(f64, f64)>> {
1894 let ((x1, z1, r1), (x2, z2, r2)) = (first, second);
1895 let (dx, dz) = (x2 - x1, z2 - z1);
1896 let dist = dx.hypot(dz);
1897 let slack = 1e-9 * scale;
1898 if dist < slack || (dist - (r1 + r2)).abs() < slack || (dist - (r1 - r2).abs()).abs() < slack {
1899 return None;
1900 }
1901 if dist > r1 + r2 || dist < (r1 - r2).abs() {
1902 return Some(Vec::new());
1903 }
1904 let along = r2.mul_add(-r2, r1.mul_add(r1, dist * dist)) / (2.0 * dist);
1905 let across = r1.mul_add(r1, -(along * along)).max(0.0).sqrt();
1906 let (ux, uz) = (dx / dist, dz / dist);
1907 let mut crossings = Vec::with_capacity(2);
1908 for side in [1.0, -1.0] {
1909 let rho = x1 + along * ux - side * across * uz;
1910 if rho <= slack {
1911 return None;
1912 }
1913 crossings.push((rho, z1 + along * uz + side * across * ux));
1914 }
1915 Some(crossings)
1916}
1917
1918fn circles_about_axis(
1920 base: Point3,
1921 axis: Vec3,
1922 crossings: &[(f64, f64)],
1923) -> Result<Vec<ExactIntersectionCurve>, MathError> {
1924 crossings
1925 .iter()
1926 .map(|&(rho, z)| {
1927 Circle3D::new(base + axis * z, axis, rho).map(ExactIntersectionCurve::Circle)
1928 })
1929 .collect()
1930}
1931
1932pub fn exact_torus_torus(
1943 first: &ToroidalSurface,
1944 second: &ToroidalSurface,
1945) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1946 let axis = first.z_axis();
1947 let scale = first.major_radius() + second.major_radius();
1948 let offset = second.center() - first.center();
1949 if first.minor_radius() >= first.major_radius()
1951 || second.minor_radius() >= second.major_radius()
1952 || axis.cross(second.z_axis()).length() > 1e-9
1953 || offset.cross(axis).length() > 1e-9 * scale
1954 {
1955 return Ok(None);
1956 }
1957 let Some(crossings) = meridian_crossings(
1958 (first.major_radius(), 0.0, first.minor_radius()),
1959 (
1960 second.major_radius(),
1961 offset.dot(axis),
1962 second.minor_radius(),
1963 ),
1964 scale,
1965 ) else {
1966 return Ok(None);
1967 };
1968 circles_about_axis(first.center(), axis, &crossings).map(Some)
1969}
1970
1971pub fn exact_cylinder_torus(
1983 cylinder: &CylindricalSurface,
1984 torus: &ToroidalSurface,
1985) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1986 let axis = torus.z_axis();
1987 let scale = torus.major_radius() + cylinder.radius();
1988 let offset = cylinder.origin() - torus.center();
1989 if torus.minor_radius() >= torus.major_radius()
1991 || axis.cross(cylinder.axis()).length() > 1e-9
1992 || offset.cross(axis).length() > 1e-9 * scale
1993 {
1994 return Ok(None);
1995 }
1996 let gap = cylinder.radius() - torus.major_radius();
1997 let small = torus.minor_radius();
1998 if (gap.abs() - small).abs() < 1e-9 * scale {
1999 return Ok(None);
2000 }
2001 if gap.abs() > small {
2002 return Ok(Some(Vec::new()));
2003 }
2004 let height = small.mul_add(small, -(gap * gap)).sqrt();
2005 circles_about_axis(
2006 torus.center(),
2007 axis,
2008 &[(cylinder.radius(), height), (cylinder.radius(), -height)],
2009 )
2010 .map(Some)
2011}
2012
2013pub fn exact_sphere_torus(
2026 sphere: &SphericalSurface,
2027 torus: &ToroidalSurface,
2028) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2029 let axis = torus.z_axis();
2030 let scale = torus.major_radius() + sphere.radius();
2031 let offset = sphere.center() - torus.center();
2032 if torus.minor_radius() >= torus.major_radius() || offset.cross(axis).length() > 1e-9 * scale {
2034 return Ok(None);
2035 }
2036 let Some(crossings) = meridian_crossings(
2037 (0.0, offset.dot(axis), sphere.radius()),
2038 (torus.major_radius(), 0.0, torus.minor_radius()),
2039 scale,
2040 ) else {
2041 return Ok(None);
2042 };
2043 circles_about_axis(torus.center(), axis, &crossings).map(Some)
2044}
2045
2046pub fn exact_cone_cone(
2071 c1: &ConicalSurface,
2072 c2: &ConicalSurface,
2073) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2074 let axis = c1.axis();
2075 let axis2 = c2.axis();
2076
2077 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
2079 return Ok(None); }
2081 let apex1 = c1.apex();
2082 let apex2 = c2.apex();
2083 let delta = apex2 - apex1;
2084 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2085 let along = delta_v.dot(axis);
2086 if (delta_v - axis * along).length() > 1e-8 {
2087 return offset_parallel_cone_cone(c1, c2);
2088 }
2089
2090 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
2091 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
2092 return Ok(None); }
2094 let m1 = c1.half_angle().cos() / s1;
2095 let m2 = c2.half_angle().cos() / s2;
2096 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
2097 let d2 = along; let denom = m1 - m2 * sigma;
2100 if denom.abs() < 1e-12 {
2101 if sigma > 0.0 && d2.abs() < 1e-9 {
2104 return Ok(None);
2105 }
2106 return Ok(Some(vec![]));
2107 }
2108
2109 let t_star = (-m2 * sigma * d2) / denom;
2110 let radius = m1 * t_star;
2111 if radius < 1e-12 {
2112 return Ok(Some(vec![])); }
2114
2115 let center = Point3::new(
2116 apex1.x() + axis.x() * t_star,
2117 apex1.y() + axis.y() * t_star,
2118 apex1.z() + axis.z() * t_star,
2119 );
2120 let circle = Circle3D::new(center, axis, radius)?;
2121 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2122}
2123
2124fn offset_parallel_cone_cone(
2135 c1: &ConicalSurface,
2136 c2: &ConicalSurface,
2137) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2138 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
2139 return Ok(None); }
2141 let t1 = c1.half_angle().tan();
2142 let t2 = c2.half_angle().tan();
2143 if !t1.is_finite() || !t2.is_finite() {
2144 return Ok(None);
2145 }
2146 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
2147 return Ok(None);
2148 }
2149
2150 let w = c1.axis();
2151 let apex1 = c1.apex();
2152 let apex2 = c2.apex();
2153 let delta = apex2 - apex1;
2154 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2155 let s = delta_v.dot(w);
2156 let tm = 0.5 * (t1 + t2);
2157 let k = 1.0 + tm * tm;
2158
2159 let n = (delta_v - w * (k * s)) * 2.0;
2163 let n_len = n.length();
2164 if n_len < 1e-12 {
2165 return Ok(None);
2166 }
2167 let n_hat = n * (1.0 / n_len);
2168 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
2169
2170 let axis2 = c2.axis();
2176 let scale = 1.0 + delta_v.length();
2177 let mut out = Vec::new();
2178 for curve in exact_plane_cone(c1, n_hat, d, 0.0)? {
2179 let samples: Vec<Point3> = match &curve {
2180 ExactIntersectionCurve::Circle(c) => (0..4)
2181 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
2182 .collect(),
2183 ExactIntersectionCurve::Ellipse(e) => (0..4)
2184 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
2185 .collect(),
2186 ExactIntersectionCurve::Points(_) => return Ok(None),
2187 };
2188 let on_real_nappe = |p: &Point3| {
2189 let rel = *p - apex2;
2190 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
2191 };
2192 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
2193 match hits {
2194 0 => {}
2195 4 => out.push(curve),
2196 _ => return Ok(None),
2197 }
2198 }
2199 Ok(Some(out))
2200}
2201
2202pub fn exact_cone_cylinder(
2222 cone: &ConicalSurface,
2223 cyl: &CylindricalSurface,
2224) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2225 let axis = cone.axis();
2226 let cyl_axis = cyl.axis();
2227
2228 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
2230 return Ok(None);
2231 }
2232 let apex = cone.apex();
2233 let delta = apex - cyl.origin();
2234 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
2235 let along = delta_v.dot(cyl_axis);
2236 if (delta_v - cyl_axis * along).length() > 1e-8 {
2237 return Ok(None);
2238 }
2239
2240 let s = cone.half_angle().sin();
2241 if s.abs() < 1e-12 {
2242 return Ok(None); }
2244 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
2246 return Ok(None); }
2248
2249 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
2251 return Ok(Some(vec![])); }
2253 let center = Point3::new(
2254 apex.x() + axis.x() * t_star,
2255 apex.y() + axis.y() * t_star,
2256 apex.z() + axis.z() * t_star,
2257 );
2258 let circle = Circle3D::new(center, axis, cyl.radius())?;
2259 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
2260}
2261
2262fn algebraic_cone_cone(
2271 c1: &ConicalSurface,
2272 c2: &ConicalSurface,
2273) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2274 let Some(exacts) = exact_cone_cone(c1, c2)? else {
2275 return Ok(None);
2276 };
2277 let mut curves = Vec::new();
2278 for exact in exacts {
2279 let n_samples = 33;
2280 let mut positions = Vec::with_capacity(n_samples);
2281 let mut points = Vec::with_capacity(n_samples);
2282 #[allow(clippy::cast_precision_loss)]
2283 for i in 0..n_samples {
2284 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2285 let pt = match &exact {
2286 ExactIntersectionCurve::Circle(circle) => {
2287 crate::traits::ParametricCurve::evaluate(circle, theta)
2288 }
2289 ExactIntersectionCurve::Ellipse(ellipse) => {
2290 crate::traits::ParametricCurve::evaluate(ellipse, theta)
2291 }
2292 ExactIntersectionCurve::Points(_) => break,
2293 };
2294 positions.push(pt);
2295 points.push(IntersectionPoint {
2296 point: pt,
2297 param1: (0.0, 0.0),
2298 param2: (0.0, 0.0),
2299 });
2300 }
2301 if positions.is_empty() {
2302 continue;
2303 }
2304 let degree = 3.min(positions.len() - 1);
2305 let curve = interpolate(&positions, degree)?;
2306 curves.push(IntersectionCurve { curve, points });
2307 }
2308 Ok(Some(curves))
2309}
2310
2311pub fn exact_sphere_cylinder(
2331 sphere: &SphericalSurface,
2332 cyl: &CylindricalSurface,
2333) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2334 let sc = sphere.center();
2335 let r_sphere = sphere.radius();
2336 let co = cyl.origin();
2337 let axis = cyl.axis();
2338 let r_cyl = cyl.radius();
2339
2340 let delta = sc - co;
2342 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
2343 let along = delta_vec.dot(axis);
2344 let perp_vec = delta_vec - axis * along;
2345 let d_perp = perp_vec.length();
2346
2347 if d_perp > 1e-7 {
2350 return Ok(None);
2351 }
2352
2353 if r_cyl > r_sphere + 1e-10 {
2356 return Ok(Some(vec![]));
2357 }
2358 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
2359 if z_sq < 0.0 {
2360 return Ok(Some(vec![]));
2361 }
2362 let z = z_sq.sqrt();
2363
2364 let center_axis_pt = Point3::new(
2367 co.x() + axis.x() * along,
2368 co.y() + axis.y() * along,
2369 co.z() + axis.z() * along,
2370 );
2371
2372 let mut circles = Vec::new();
2373 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
2374 for &z_offset in offsets {
2375 let center = Point3::new(
2376 center_axis_pt.x() + axis.x() * z_offset,
2377 center_axis_pt.y() + axis.y() * z_offset,
2378 center_axis_pt.z() + axis.z() * z_offset,
2379 );
2380 let circle = Circle3D::new(center, axis, r_cyl)?;
2381 circles.push(ExactIntersectionCurve::Circle(circle));
2382 }
2383 Ok(Some(circles))
2384}
2385
2386pub fn exact_cone_sphere(
2404 cone: &ConicalSurface,
2405 sphere: &SphericalSurface,
2406) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
2407 let offset = cone.apex() - sphere.center();
2408 let along = offset.dot(cone.axis());
2409 if (offset - cone.axis() * along).length() > 1e-7 {
2410 return Ok(None);
2411 }
2412 let lin_tol = Tolerance::new().linear;
2413 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2414 let (far_sq, radius_sq) = (offset.dot(offset), sphere.radius() * sphere.radius());
2415 let b = 2.0 * sin_a * along;
2416 let (disc, far, near) = ruling_quadratic(1.0, b, far_sq - radius_sq);
2417 let noise = 16.0 * f64::EPSILON * 4.0f64.mul_add(far_sq + radius_sq, b * b);
2420 if disc < -noise {
2421 return Ok(Some(vec![]));
2422 }
2423 let roots: &[f64] = if far - near < lin_tol {
2424 &[far]
2425 } else {
2426 &[near, far]
2427 };
2428 let mut circles = Vec::new();
2429 for &v in roots {
2430 if v * cos_a > lin_tol {
2431 let centre = cone.apex() + cone.axis() * (v * sin_a);
2432 let circle = Circle3D::new(centre, cone.axis(), v * cos_a)?;
2433 circles.push(ExactIntersectionCurve::Circle(circle));
2434 }
2435 }
2436 Ok(Some(circles))
2437}
2438
2439fn algebraic_sphere_cylinder(
2448 sphere: &SphericalSurface,
2449 cyl: &CylindricalSurface,
2450 sphere_first: bool,
2451) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2452 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
2453 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
2454 };
2455
2456 let mut curves = Vec::new();
2457 for exact in exacts {
2458 let ExactIntersectionCurve::Circle(circle) = exact else {
2459 continue;
2460 };
2461 let n_samples = 33;
2462 let mut points = Vec::with_capacity(n_samples);
2463 let mut positions = Vec::with_capacity(n_samples);
2464 #[allow(clippy::cast_precision_loss)]
2465 for i in 0..n_samples {
2466 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2467 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
2468 positions.push(pt);
2469 let (param1, param2) = in_order(
2470 sphere.project_point(pt),
2471 cyl.project_point(pt),
2472 sphere_first,
2473 );
2474 points.push(IntersectionPoint {
2475 point: pt,
2476 param1,
2477 param2,
2478 });
2479 }
2480 let degree = 3.min(positions.len() - 1);
2481 let curve = interpolate(&positions, degree)?;
2482 curves.push(IntersectionCurve { curve, points });
2483 }
2484
2485 Ok(Some(curves))
2486}
2487
2488fn off_axis_sphere_cylinder(
2497 sphere: &SphericalSurface,
2498 cyl: &CylindricalSurface,
2499 sphere_first: bool,
2500) -> Option<Vec<IntersectionCurve>> {
2501 let (centre, radius) = (sphere.center(), sphere.radius());
2502 let axis = cyl.axis();
2503 let offset = centre - cyl.origin();
2504 let axis_distance = (offset - axis * offset.dot(axis)).length();
2505 let lin_tol = Tolerance::new().linear;
2506 if axis_distance > radius + cyl.radius() + lin_tol
2507 || axis_distance + radius < cyl.radius() - lin_tol
2508 {
2509 return Some(Vec::new());
2510 }
2511 let roots = |u: f64| {
2512 let q = cyl.evaluate(u, 0.0) - centre;
2513 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
2514 };
2515 let samples = ruling_samples(cyl, &roots);
2516 let loops = if samples.iter().all(Option::is_some) {
2517 closed_ruling_loops(&samples)
2518 } else {
2519 partial_ruling_loops(cyl, &roots, &samples)
2520 };
2521 if loops.is_empty() {
2522 return None;
2523 }
2524 Some(fit_ruling_loops(&loops, |p| {
2525 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
2526 }))
2527}
2528
2529const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
2532 if a_first { (a, b) } else { (b, a) }
2533}
2534
2535#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2549fn algebraic_cylinder_cylinder(
2550 c1: &CylindricalSurface,
2551 c2: &CylindricalSurface,
2552) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2553 let alpha = c1.axis().dot(c2.axis());
2554 let a_coeff = 1.0 - alpha * alpha;
2555
2556 if a_coeff.abs() < 1e-12 {
2558 return Ok(None);
2559 }
2560
2561 let r1 = c1.radius();
2562 let r2 = c2.radius();
2563 let o1 = c1.origin();
2564 let o2 = c2.origin();
2565 let a1 = c1.axis();
2566 let a2 = c2.axis();
2567
2568 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2571 let cross = a1.cross(a2);
2572 let cross_len = cross.length();
2573 if cross_len > 1e-12 {
2574 let axis_dist = delta.dot(cross).abs() / cross_len;
2575 if axis_dist > r1 + r2 + Tolerance::new().linear {
2576 return Ok(Some(vec![])); }
2578 }
2579
2580 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2586 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2587 let alpha = sweep.axis().dot(a);
2588 let quad = 1.0 - alpha * alpha;
2589 let (axis, sweep) = (sweep.axis(), sweep.clone());
2590 move |u: f64| {
2591 let q = sweep.evaluate(u, 0.0) - o;
2592 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2593 let b = 2.0 * (q_a1 - alpha * q_a2);
2594 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2595 ruling_quadratic(quad, b, c)
2596 }
2597 };
2598 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2599 let samples1 = ruling_samples(c1, &roots1);
2600 let loops = if samples1.iter().all(Option::is_some) {
2601 closed_ruling_loops(&samples1)
2602 } else {
2603 let samples2 = ruling_samples(c2, &roots2);
2604 if samples2.iter().all(Option::is_some) {
2605 closed_ruling_loops(&samples2)
2606 } else if samples1.iter().any(Option::is_some) {
2607 partial_ruling_loops(c1, &roots1, &samples1)
2608 } else {
2609 partial_ruling_loops(c2, &roots2, &samples2)
2610 }
2611 };
2612 if loops.is_empty() {
2613 return Ok(None);
2614 }
2615 Ok(Some(fit_ruling_loops(&loops, |p| {
2616 (c1.project_point(p), c2.project_point(p))
2617 })))
2618}
2619
2620fn ruling_cone_cylinder(
2627 cone: &ConicalSurface,
2628 cyl: &CylindricalSurface,
2629 cone_first: bool,
2630) -> Option<Vec<IntersectionCurve>> {
2631 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2632 if sin_t < 1e-12 || cos_t < 1e-12 {
2633 return None;
2634 }
2635 let (apex, d, w) = (cone.apex(), cone.axis(), cyl.axis());
2636 let s = 1.0 / (sin_t * sin_t);
2637 let alpha = w.dot(d);
2638 let quad = 1.0 - s * alpha * alpha;
2639 if quad.abs() < 1e-9 {
2640 return None;
2641 }
2642 let roots = |u: f64| {
2643 let delta = cyl.evaluate(u, 0.0) - apex;
2644 let (dd, dw) = (delta.dot(d), delta.dot(w));
2645 let b = 2.0 * (dw - s * dd * alpha);
2646 let c = delta.dot(delta) - s * dd * dd;
2647 ruling_quadratic(quad, b, c)
2648 };
2649 let lin_tol = Tolerance::new().linear;
2650 let far_nappe = (0..WINDOW_SCAN * RULING_SAMPLES).any(|k| {
2651 #[allow(clippy::cast_precision_loss)]
2652 let u = TAU * (k as f64 + 0.5) / (WINDOW_SCAN * RULING_SAMPLES) as f64;
2653 let (disc, vp, vm) = roots(u);
2654 disc >= -lin_tol
2655 && [vp, vm]
2656 .iter()
2657 .any(|&t| (cyl.evaluate(u, t) - apex).dot(d) < -lin_tol)
2658 });
2659 if far_nappe {
2660 return None;
2661 }
2662 let samples = ruling_samples(cyl, &roots);
2663 let scan = WINDOW_SCAN * RULING_SAMPLES;
2668 #[allow(clippy::cast_precision_loss)]
2671 let meets = |k: usize| roots(TAU * ((k % scan) as f64 + 0.5) / scan as f64).0 >= -lin_tol;
2672 if let Some(start) = (0..scan).find(|&k| !meets(k)) {
2673 let mut k = start;
2674 while k < start + scan {
2675 if !meets(k) {
2676 k += 1;
2677 continue;
2678 }
2679 let first = k;
2680 while k < start + scan && meets(k) {
2681 k += 1;
2682 }
2683 let covered = (first..k)
2684 .filter(|&j| j % WINDOW_SCAN == WINDOW_SCAN / 2 - 1 && meets(j + 1))
2685 .count();
2686 if covered < WINDOW_MIN_SAMPLES {
2687 return None;
2688 }
2689 }
2690 }
2691 let loops = if samples.iter().all(Option::is_some) {
2692 closed_ruling_loops(&samples)
2693 } else {
2694 partial_ruling_loops(cyl, &roots, &samples)
2695 };
2696 if loops.is_empty() {
2697 return None;
2698 }
2699 Some(fit_ruling_loops(&loops, |p| {
2700 in_order(cone.project_point(p), cyl.project_point(p), cone_first)
2701 }))
2702}
2703
2704fn ruling_torus_cylinder(
2714 torus: &ToroidalSurface,
2715 cyl: &CylindricalSurface,
2716 torus_first: bool,
2717) -> Option<Vec<IntersectionCurve>> {
2718 if cyl.axis().dot(torus.z_axis()).abs() > 1.0 - 1e-9
2719 || torus.minor_radius() >= torus.major_radius()
2720 {
2721 return None;
2722 }
2723 let roots = |u: f64| intersect_line_torus(torus, cyl.evaluate(u, 0.0), cyl.axis());
2724 let rows: Vec<Vec<f64>> = (0..RULING_SAMPLES).map(|i| roots(ruling_u(i))).collect();
2725 let count = rows[0].len();
2726 let scan = WINDOW_SCAN * RULING_SAMPLES;
2727 #[allow(clippy::cast_precision_loss)]
2728 if count == 0
2729 || count % 2 == 1
2730 || (0..scan).any(|k| roots(TAU * (k as f64 + 0.5) / scan as f64).len() != count)
2731 {
2732 return None;
2733 }
2734 let loops: Vec<Vec<Point3>> = (0..count)
2735 .map(|j| {
2736 let mut pts: Vec<Point3> = rows
2737 .iter()
2738 .enumerate()
2739 .map(|(i, r)| cyl.evaluate(ruling_u(i), r[j]))
2740 .collect();
2741 pts.push(pts[0]);
2742 pts
2743 })
2744 .collect();
2745 Some(fit_ruling_loops(&loops, |p| {
2746 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
2747 }))
2748}
2749
2750fn ruling_cone_sphere(
2759 cone: &ConicalSurface,
2760 sphere: &SphericalSurface,
2761 cone_first: bool,
2762) -> Option<Vec<IntersectionCurve>> {
2763 let (apex, centre, radius) = (cone.apex(), sphere.center(), sphere.radius());
2764 let offset = apex - centre;
2765 let lin_tol = Tolerance::new().linear;
2766 let along = offset.dot(cone.axis());
2767 let across = (offset - cone.axis() * along).length();
2768 if across < lin_tol {
2769 return None;
2770 }
2771 let k = offset.dot(offset) - radius * radius;
2777 if radius - offset.length() > lin_tol {
2778 let exit = |u: f64| {
2779 let h = (cone.evaluate(u, 1.0) - apex).dot(offset);
2780 let root = h.mul_add(h, -k).sqrt();
2781 cone.evaluate(u, if h > 0.0 { -k / (h + root) } else { root - h })
2782 };
2783 let mut samples: Vec<(f64, Point3)> = (0..=RULING_SAMPLES)
2788 .map(|i| (ruling_u(i), exit(ruling_u(i))))
2789 .collect();
2790 for _ in 0..10 {
2791 let mut refined = Vec::with_capacity(2 * samples.len());
2792 for pair in samples.windows(2) {
2793 let ((u0, p0), (u1, p1)) = (pair[0], pair[1]);
2794 refined.push(pair[0]);
2795 let um = 0.5 * (u0 + u1);
2796 let pm = exit(um);
2797 let chord = (p1 - p0).length();
2798 if chord > lin_tol && (pm - (p0 + (p1 - p0) * 0.5)).length() > 0.01 * chord {
2799 refined.push((um, pm));
2800 }
2801 }
2802 refined.extend(samples.last().copied());
2803 if refined.len() == samples.len() {
2804 break;
2805 }
2806 samples = refined;
2807 }
2808 let mut pts: Vec<Point3> = samples.iter().map(|&(_, p)| p).collect();
2809 if let Some(last) = pts.last_mut() {
2810 *last = samples[0].1;
2811 }
2812 return Some(fit_ruling_loops(&[pts], |p| {
2813 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2814 }));
2815 }
2816 let crossing = |h: f64| {
2820 let (disc, vp, vm) = ruling_quadratic(1.0, 2.0 * h, k);
2821 (disc > lin_tol && vm >= lin_tol).then_some((vm, vp))
2822 };
2823 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2828 if crossing(sin_a.mul_add(along, cos_a * across)).is_none()
2829 || crossing(sin_a.mul_add(along, -cos_a * across)).is_none()
2830 {
2831 return window_cone_sphere(cone, sphere, cone_first);
2832 }
2833 let rows: Vec<(f64, f64)> = (0..RULING_SAMPLES)
2834 .map(|i| crossing((cone.evaluate(ruling_u(i), 1.0) - apex).dot(offset)))
2835 .collect::<Option<_>>()?;
2836 let loops: Vec<Vec<Point3>> = [0, 1]
2837 .iter()
2838 .map(|&j| {
2839 let mut pts: Vec<Point3> = rows
2840 .iter()
2841 .enumerate()
2842 .map(|(i, &(near, far))| {
2843 cone.evaluate(ruling_u(i), if j == 0 { near } else { far })
2844 })
2845 .collect();
2846 pts.push(pts[0]);
2847 pts
2848 })
2849 .collect();
2850 Some(fit_ruling_loops(&loops, |p| {
2851 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2852 }))
2853}
2854
2855fn window_cone_sphere(
2870 cone: &ConicalSurface,
2871 sphere: &SphericalSurface,
2872 cone_first: bool,
2873) -> Option<Vec<IntersectionCurve>> {
2874 let offset = cone.apex() - sphere.center();
2875 let lin_tol = Tolerance::new().linear;
2876 if offset.length() - sphere.radius() <= lin_tol {
2877 return None;
2878 }
2879 let k = offset.dot(offset) - sphere.radius() * sphere.radius();
2880 let (sin_a, cos_a) = cone.half_angle().sin_cos();
2881 let (ox, oy) = (offset.dot(cone.x_axis()), offset.dot(cone.y_axis()));
2882 let (c, a) = (sin_a * offset.dot(cone.axis()), cos_a * ox.hypot(oy));
2883 if a < lin_tol {
2884 return None;
2885 }
2886 let reach = (-k.sqrt() - c) / a;
2887 if reach <= -1.0 {
2888 return Some(Vec::new());
2889 }
2890 if reach >= 1.0 {
2891 return None;
2892 }
2893 let (mid, half) = (oy.atan2(ox) + std::f64::consts::PI, reach.acos());
2894 let half = std::f64::consts::PI - half;
2895 let n = RULING_SAMPLES;
2896 let mut pts: Vec<Point3> = (0..n)
2897 .map(|i| {
2898 #[allow(clippy::cast_precision_loss)]
2899 let theta = TAU * i as f64 / n as f64;
2900 let u = half.mul_add(-theta.cos(), mid);
2901 let h = a.mul_add((u - mid + std::f64::consts::PI).cos(), c);
2902 let split = h.mul_add(h, -k).max(0.0).sqrt();
2903 cone.evaluate(u, -h - split.copysign(theta.sin()))
2904 })
2905 .collect();
2906 pts.push(pts[0]);
2907 Some(fit_ruling_loops(&[pts], |p| {
2908 in_order(cone.project_point(p), sphere.project_point(p), cone_first)
2909 }))
2910}
2911
2912const WINDOW_SCAN: usize = 16;
2915const WINDOW_MIN_SAMPLES: usize = 8;
2916
2917const RULING_SAMPLES: usize = 128;
2921
2922#[allow(clippy::cast_precision_loss)]
2923fn ruling_u(i: usize) -> f64 {
2924 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2925}
2926
2927fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2929 let disc = b * b - 4.0 * quad * c;
2930 let root = disc.max(0.0).sqrt();
2931 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2932}
2933
2934fn ruling_samples(
2938 sweep: &CylindricalSurface,
2939 roots: &impl Fn(f64) -> (f64, f64, f64),
2940) -> Vec<Option<(Point3, Point3)>> {
2941 let lin_tol = Tolerance::new().linear;
2942 (0..RULING_SAMPLES)
2943 .map(|i| {
2944 let u = ruling_u(i);
2945 let (disc, vp, vm) = roots(u);
2946 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2947 })
2948 .collect()
2949}
2950
2951fn closed_ruling_loops(samples: &[Option<(Point3, Point3)>]) -> Vec<Vec<Point3>> {
2953 let mut plus: Vec<Point3> = samples.iter().flatten().map(|s| s.0).collect();
2954 let mut minus: Vec<Point3> = samples.iter().flatten().map(|s| s.1).collect();
2955 plus.push(plus[0]);
2956 minus.push(minus[0]);
2957 vec![plus, minus]
2958}
2959
2960fn partial_ruling_loops(
2965 sweep: &CylindricalSurface,
2966 roots: &impl Fn(f64) -> (f64, f64, f64),
2967 samples: &[Option<(Point3, Point3)>],
2968) -> Vec<Vec<Point3>> {
2969 let branch_point = |inside: usize, outside: usize| -> Point3 {
2970 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
2971 if (hi - lo).abs() > std::f64::consts::PI {
2972 hi += if hi < lo { TAU } else { -TAU };
2973 }
2974 for _ in 0..60 {
2975 let mid = 0.5 * (lo + hi);
2976 if roots(mid).0 >= 0.0 {
2977 lo = mid;
2978 } else {
2979 hi = mid;
2980 }
2981 }
2982 let (_, vp, vm) = roots(lo);
2983 sweep.evaluate(lo, 0.5 * (vp + vm))
2984 };
2985 let Some(first_gap) = samples.iter().position(Option::is_none) else {
2986 return Vec::new();
2987 };
2988 let mut loops = Vec::new();
2989 let mut k = 0;
2990 while k < RULING_SAMPLES {
2991 let i = (first_gap + k) % RULING_SAMPLES;
2992 if samples[i].is_none() {
2993 k += 1;
2994 continue;
2995 }
2996 let start = i;
2997 let mut run = Vec::new();
2998 while k < RULING_SAMPLES {
2999 let j = (first_gap + k) % RULING_SAMPLES;
3000 let Some(pair) = samples[j] else { break };
3001 run.push(pair);
3002 k += 1;
3003 }
3004 let end = (start + run.len() - 1) % RULING_SAMPLES;
3005 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
3006 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
3007 let mut pts = vec![head];
3008 pts.extend(run.iter().map(|p| p.0));
3009 pts.push(tail);
3010 pts.extend(run.iter().rev().map(|p| p.1));
3011 pts.push(head);
3012 loops.push(pts);
3013 }
3014 loops
3015}
3016
3017fn fit_ruling_loops(
3020 loops: &[Vec<Point3>],
3021 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
3022) -> Vec<IntersectionCurve> {
3023 let mut curves = Vec::new();
3024 for pts in loops {
3025 if pts.len() < 4 {
3026 continue;
3027 }
3028 let ipts: Vec<IntersectionPoint> = pts
3029 .iter()
3030 .map(|&p| {
3031 let (param1, param2) = params(p);
3032 IntersectionPoint {
3033 point: p,
3034 param1,
3035 param2,
3036 }
3037 })
3038 .collect();
3039 let degree = 3.min(pts.len() - 1);
3040 if let Ok(curve) = interpolate(pts, degree) {
3041 curves.push(IntersectionCurve {
3042 curve,
3043 points: ipts,
3044 });
3045 }
3046 }
3047 curves
3048}
3049
3050#[allow(clippy::unnecessary_wraps)]
3076fn algebraic_parallel_cone_cylinder(
3077 cone: &ConicalSurface,
3078 cyl: &CylindricalSurface,
3079 v_range_cone: Option<(f64, f64)>,
3080 v_range_cyl: Option<(f64, f64)>,
3081) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
3082 let axis = cone.axis();
3083 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
3084 return Ok(None); }
3086
3087 let apex = cone.apex();
3088 let delta = cyl.origin() - apex;
3089 let along = delta.dot(axis);
3090 let perp = delta - axis * along;
3091 let d = perp.length();
3092 if d < 1e-9 {
3093 return Ok(None); }
3095
3096 let (e1, e2) = (cone.x_axis(), cone.y_axis());
3097 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
3098
3099 let (sin_t, cos_t) = cone.half_angle().sin_cos();
3100 if cos_t < 1e-12 || sin_t < 1e-12 {
3101 return Ok(None);
3102 }
3103 let r = cyl.radius();
3104
3105 let mut v_min = (d - r).abs() / cos_t;
3107 let mut v_max = (d + r) / cos_t;
3108 if v_max <= v_min {
3109 return Ok(Some(vec![]));
3110 }
3111
3112 let mut lo = v_min;
3118 let mut hi = v_max;
3119 if let Some((a, b)) = v_range_cone {
3124 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3125 lo = lo.max(a);
3126 hi = hi.min(b);
3127 }
3128 if let Some((a, b)) = v_range_cyl {
3129 let flip = cyl.axis().dot(axis);
3132 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
3133 let (a, b) = (to_cone_v(a), to_cone_v(b));
3134 let (a, b) = if a <= b { (a, b) } else { (b, a) };
3135 lo = lo.max(a);
3136 hi = hi.min(b);
3137 }
3138 let (turn_lo, turn_hi) = (v_min, v_max);
3139 v_min = lo.max(v_min);
3140 v_max = hi.min(v_max);
3141 if v_max - v_min <= 1e-12 {
3142 return Ok(Some(vec![]));
3143 }
3144 let slack = Tolerance::new().linear;
3152 #[allow(clippy::cast_precision_loss)]
3153 let resolved = d - r > 3.0 * (d * r).sqrt() * TAU / RULING_SAMPLES as f64;
3154 if v_min <= turn_lo + slack && v_max >= turn_hi - slack && resolved {
3155 let mut pts: Vec<Point3> = (0..RULING_SAMPLES)
3156 .map(|i| {
3157 let (sin_u, cos_u) = ruling_u(i).sin_cos();
3158 let foot = cyl.origin() + (cyl.x_axis() * cos_u + cyl.y_axis() * sin_u) * r;
3159 let off = foot - apex;
3160 let across = off - axis * off.dot(axis);
3161 apex + across + axis * (across.length() * sin_t / cos_t)
3162 })
3163 .collect();
3164 pts.push(pts[0]);
3165 return Ok(Some(fit_ruling_loops(&[pts], |p| {
3166 (cone.project_point(p), cyl.project_point(p))
3167 })));
3168 }
3169
3170 let n_samples = 128;
3171 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3172 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
3173 #[allow(clippy::cast_precision_loss)]
3174 for i in 0..=n_samples {
3175 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
3176 let rho = v * cos_t;
3177 if rho < 1e-12 {
3178 if (d - r).abs() < 1e-12 {
3186 let apex = cone.evaluate(phi0, v);
3187 plus.push(apex);
3188 minus.push(apex);
3189 }
3190 continue;
3191 }
3192 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
3193 let alpha = cos_alpha.acos();
3194 plus.push(cone.evaluate(phi0 + alpha, v));
3195 minus.push(cone.evaluate(phi0 - alpha, v));
3196 }
3197
3198 let mut curves = Vec::new();
3199 for pts in [&plus, &minus] {
3200 if pts.len() < 4 {
3203 continue;
3204 }
3205 let ipts: Vec<IntersectionPoint> = pts
3206 .iter()
3207 .map(|&p| IntersectionPoint {
3208 point: p,
3209 param1: cone.project_point(p),
3210 param2: cyl.project_point(p),
3211 })
3212 .collect();
3213 let degree = 3.min(pts.len() - 1);
3214 match interpolate(pts, degree) {
3215 Ok(curve) => curves.push(IntersectionCurve {
3216 curve,
3217 points: ipts,
3218 }),
3219 Err(_) => return Ok(None),
3224 }
3225 }
3226
3227 Ok(Some(curves))
3228}
3229
3230fn algebraic_sphere_sphere(
3238 s1: &SphericalSurface,
3239 s2: &SphericalSurface,
3240) -> Result<Vec<IntersectionCurve>, MathError> {
3241 let c1 = s1.center();
3242 let c2 = s2.center();
3243 let r1 = s1.radius();
3244 let r2 = s2.radius();
3245
3246 let delta = c2 - c1;
3247 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
3248 let d = d_sq.sqrt();
3249
3250 if d < 1e-12 {
3251 return Ok(vec![]);
3253 }
3254
3255 if d > r1 + r2 + 1e-10 {
3257 return Ok(vec![]); }
3259 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
3260 return Ok(vec![]); }
3262
3263 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
3265
3266 let r_circle_sq = r1 * r1 - d1 * d1;
3268 if r_circle_sq < 0.0 {
3269 if r_circle_sq > -1e-10 {
3271 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3273 let tangent_pt = Point3::new(
3274 c1.x() + axis.x() * d1,
3275 c1.y() + axis.y() * d1,
3276 c1.z() + axis.z() * d1,
3277 );
3278 let ipt = IntersectionPoint {
3279 point: tangent_pt,
3280 param1: (0.0, 0.0),
3281 param2: (0.0, 0.0),
3282 };
3283 return Ok(vec![IntersectionCurve {
3285 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
3286 points: vec![ipt],
3287 }]);
3288 }
3289 return Ok(vec![]);
3290 }
3291
3292 let r_circle = r_circle_sq.sqrt();
3293 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
3294 let center = Point3::new(
3295 c1.x() + axis.x() * d1,
3296 c1.y() + axis.y() * d1,
3297 c1.z() + axis.z() * d1,
3298 );
3299
3300 let basis = Frame3::from_normal(center, axis)?;
3302 let u_dir = basis.x;
3303 let v_dir = basis.y;
3304
3305 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
3308 let mut positions = Vec::with_capacity(n_samples);
3309 #[allow(clippy::cast_precision_loss)]
3310 for i in 0..n_samples {
3311 let theta = TAU * i as f64 / (n_samples - 1) as f64;
3312 let (sin_t, cos_t) = theta.sin_cos();
3313 let pt = Point3::new(
3314 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
3315 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
3316 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
3317 );
3318 positions.push(pt);
3319 points.push(IntersectionPoint {
3320 point: pt,
3321 param1: (0.0, 0.0),
3322 param2: (0.0, 0.0),
3323 });
3324 }
3325
3326 let degree = 3.min(positions.len() - 1);
3327 let curve = interpolate(&positions, degree)?;
3328
3329 Ok(vec![IntersectionCurve { curve, points }])
3330}
3331
3332#[allow(clippy::too_many_arguments)]
3338fn correct_to_intersection(
3339 a: &AnalyticSurface<'_>,
3340 b: &AnalyticSurface<'_>,
3341 surf_a: &dyn Fn(f64, f64) -> Point3,
3342 norm_a: &dyn Fn(f64, f64) -> Vec3,
3343 surf_b: &dyn Fn(f64, f64) -> Point3,
3344 norm_b: &dyn Fn(f64, f64) -> Vec3,
3345 point: Point3,
3346 u_range_a: (f64, f64),
3347 v_range_a: (f64, f64),
3348 u_range_b: (f64, f64),
3349 v_range_b: (f64, f64),
3350 max_iters: usize,
3351) -> Point3 {
3352 let mut p = point;
3353 for _ in 0..max_iters {
3354 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
3355 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
3356 let pa = surf_a(ua, va);
3357 let pb = surf_b(ub, vb);
3358 let na = norm_a(ua, va);
3359 let nb = norm_b(ub, vb);
3360 let pv = Vec3::new(p.x(), p.y(), p.z());
3361
3362 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
3363 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
3364
3365 if da.abs() < 1e-7 && db.abs() < 1e-7 {
3366 break;
3367 }
3368
3369 let t = na.cross(nb);
3370 let t_len = t.length();
3371 if t_len < 1e-10 {
3372 return Point3::new(
3374 (pa.x() + pb.x()) * 0.5,
3375 (pa.y() + pb.y()) * 0.5,
3376 (pa.z() + pb.z()) * 0.5,
3377 );
3378 }
3379 let t_hat = t * (1.0 / t_len);
3380
3381 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3383 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3384 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
3385 if det.abs() < 1e-15 {
3386 return Point3::new(
3387 (pa.x() + pb.x()) * 0.5,
3388 (pa.y() + pb.y()) * 0.5,
3389 (pa.z() + pb.z()) * 0.5,
3390 );
3391 }
3392 let inv = 1.0 / det;
3393 let dx = inv
3395 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
3396 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
3397 let dy = inv
3398 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
3399 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
3400 let dz = inv
3401 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
3402 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
3403 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
3404
3405 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
3408 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
3409 let pc_a = surf_a(uc, vc);
3410 let pc_b = surf_b(ud, vd);
3411 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
3412 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
3413 .dot(norm_a(uc, vc))
3414 .abs();
3415 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
3416 .dot(norm_b(ud, vd))
3417 .abs();
3418 if da_new > da.abs() && db_new > db.abs() {
3419 return p;
3420 }
3421
3422 p = candidate;
3423 }
3424 p
3425}
3426
3427#[allow(clippy::too_many_arguments)]
3433fn march_analytic_intersection(
3434 a: &AnalyticSurface<'_>,
3435 b: &AnalyticSurface<'_>,
3436 surf_a: &dyn Fn(f64, f64) -> Point3,
3437 norm_a: &dyn Fn(f64, f64) -> Vec3,
3438 surf_b: &dyn Fn(f64, f64) -> Point3,
3439 norm_b: &dyn Fn(f64, f64) -> Vec3,
3440 seed: Point3,
3441 u_range_a: (f64, f64),
3442 v_range_a: (f64, f64),
3443 u_range_b: (f64, f64),
3444 v_range_b: (f64, f64),
3445 initial_step: f64,
3446 u_periodic_a: bool,
3447 u_periodic_b: bool,
3448 region: Option<Aabb3>,
3449) -> Vec<Point3> {
3450 let max_steps = 500;
3451 let h_min = 1e-6;
3452 let h_max = initial_step * 4.0;
3453 let closure_dist = initial_step * 5.0;
3457 let max_angle = 10.0_f64.to_radians();
3459 let min_angle = 2.0_f64.to_radians();
3460
3461 let mut forward = Vec::new();
3463 let mut backward = Vec::new();
3465
3466 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
3467 let mut current = seed;
3468 let mut h = initial_step;
3469 let mut prev_tangent: Option<Vec3> = None;
3470
3471 for _ in 0..max_steps {
3472 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
3473 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
3474
3475 let na = norm_a(ua, va);
3476 let nb = norm_b(ub, vb);
3477
3478 let tangent = na.cross(nb);
3479 let t_len = tangent.length();
3480 if t_len < 1e-10 {
3481 break;
3482 }
3483 let t_dir = tangent * (direction / t_len);
3484
3485 if let Some(prev_t) = prev_tangent {
3487 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
3488 let angle = cos_angle.acos();
3489 if angle > max_angle && h > h_min {
3490 h = (h * 0.5).max(h_min);
3491 } else if angle < min_angle {
3492 h = (h * 2.0).min(h_max);
3493 }
3494 }
3495 prev_tangent = Some(t_dir);
3496
3497 let next = Point3::new(
3498 h.mul_add(t_dir.x(), current.x()),
3499 h.mul_add(t_dir.y(), current.y()),
3500 h.mul_add(t_dir.z(), current.z()),
3501 );
3502
3503 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
3504 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
3505
3506 let pa = surf_a(ua2, va2);
3507 let pb = surf_b(ub2, vb2);
3508 let mid = Point3::new(
3509 (pa.x() + pb.x()) * 0.5,
3510 (pa.y() + pb.y()) * 0.5,
3511 (pa.z() + pb.z()) * 0.5,
3512 );
3513 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
3514 || va2 <= v_range_a.0
3515 || va2 >= v_range_a.1;
3516 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
3517 || vb2 <= v_range_b.0
3518 || vb2 >= v_range_b.1;
3519
3520 if out_a || out_b {
3521 break;
3522 }
3523 if region.is_some_and(|r| !r.contains_point(mid)) {
3524 points.push(mid);
3525 break;
3526 }
3527
3528 let dist_to_seed = (mid - seed).length();
3532 if points.len() > 10 && dist_to_seed < closure_dist {
3533 points.push(seed);
3534 break;
3535 }
3536
3537 points.push(mid);
3538 current = mid;
3539 }
3540 }
3541
3542 backward.reverse();
3544 let mut result = backward;
3545 result.push(seed);
3546 result.append(&mut forward);
3547
3548 for pt in &mut result {
3550 *pt = correct_to_intersection(
3551 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
3552 5,
3553 );
3554 }
3555
3556 result
3557}
3558
3559fn project_analytic(
3563 surface: &AnalyticSurface<'_>,
3564 point: Point3,
3565 u_range: (f64, f64),
3566 v_range: (f64, f64),
3567) -> (f64, f64) {
3568 match surface {
3569 AnalyticSurface::Cylinder(cyl) => {
3570 let (u, v) = cyl.project_point(point);
3571 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3572 }
3573 AnalyticSurface::Sphere(sphere) => {
3574 let (u, v) = sphere.project_point(point);
3575 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3576 }
3577 AnalyticSurface::Cone(cone) => {
3578 let (u, v) = cone.project_point(point);
3579 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3580 }
3581 AnalyticSurface::Torus(torus) => {
3582 let (u, v) = torus.project_point(point);
3583 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
3584 }
3585 }
3586}
3587
3588fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
3592 matches!(
3593 surface,
3594 AnalyticSurface::Cylinder(_)
3595 | AnalyticSurface::Cone(_)
3596 | AnalyticSurface::Sphere(_)
3597 | AnalyticSurface::Torus(_)
3598 )
3599}
3600
3601#[allow(clippy::type_complexity)]
3603fn surface_closures<'a>(
3604 surface: &'a AnalyticSurface<'a>,
3605) -> (
3606 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
3607 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
3608 (f64, f64),
3609 (f64, f64),
3610) {
3611 match surface {
3612 AnalyticSurface::Cylinder(cyl) => (
3613 Box::new(|u, v| cyl.evaluate(u, v)),
3614 Box::new(|u, v| cyl.normal(u, v)),
3615 (0.0, TAU),
3616 (-1.0, 1.0),
3617 ),
3618 AnalyticSurface::Cone(cone) => (
3619 Box::new(|u, v| cone.evaluate(u, v)),
3620 Box::new(|u, v| cone.normal(u, v)),
3621 (0.0, TAU),
3622 (0.01, 2.0),
3623 ),
3624 AnalyticSurface::Sphere(sphere) => (
3625 Box::new(|u, v| sphere.evaluate(u, v)),
3626 Box::new(|u, v| sphere.normal(u, v)),
3627 (0.0, TAU),
3628 (-FRAC_PI_2, FRAC_PI_2),
3629 ),
3630 AnalyticSurface::Torus(torus) => (
3631 Box::new(|u, v| torus.evaluate(u, v)),
3632 Box::new(|u, v| torus.normal(u, v)),
3633 (0.0, TAU),
3634 (0.0, TAU),
3635 ),
3636 }
3637}
3638
3639#[cfg(test)]
3640#[allow(clippy::unwrap_used, clippy::expect_used)]
3641mod tests {
3642 use super::*;
3643 use crate::tolerance::Tolerance;
3644
3645 #[test]
3649 fn plane_cone_conic_arcs_lie_on_both_surfaces() {
3650 let half_angle = 1.1_f64;
3651 let cone = ConicalSurface::new(
3652 Point3::new(0.0, 0.0, 0.0),
3653 Vec3::new(0.0, 0.0, 1.0),
3654 half_angle,
3655 )
3656 .unwrap();
3657 let ruling = Vec3::new(half_angle.sin(), 0.0, half_angle.cos());
3658 for (normal, d) in [(Vec3::new(1.0, 0.0, 0.0), 0.5), (ruling, 1.0)] {
3659 let chains =
3660 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, d, 10.0)
3661 .unwrap();
3662 let chain = chains
3663 .iter()
3664 .find_map(|c| match c {
3665 ExactIntersectionCurve::Points(chain) => Some(chain),
3666 _ => None,
3667 })
3668 .expect("a parabola or hyperbola section is sampled");
3669 let (from, to) = (chain[2], chain[chain.len() - 3]);
3670 let arc = plane_cone_conic_arc(&cone, normal, d, from, to)
3671 .unwrap()
3672 .expect("an exact arc");
3673 let (t0, t1) = arc.domain();
3674 assert!((arc.evaluate(t0) - from).length() < 1e-12);
3675 assert!((arc.evaluate(t1) - to).length() < 1e-12);
3676 for i in 0..=200 {
3677 let q = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0);
3678 let w = q - Point3::new(0.0, 0.0, 0.0);
3679 let off_plane = (normal.dot(w) - d).abs();
3680 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3681 assert!(off_plane < 1e-9, "off the plane by {off_plane}");
3682 assert!(off_cone < 1e-9, "off the cone by {off_cone}");
3683 }
3684 }
3685 }
3686
3687 #[test]
3691 fn plane_cone_conic_arc_declines_a_near_parabolic_ellipse() {
3692 let half_angle = 1.1_f64;
3693 let cone = ConicalSurface::new(
3694 Point3::new(0.0, 0.0, 0.0),
3695 Vec3::new(0.0, 0.0, 1.0),
3696 half_angle,
3697 )
3698 .unwrap();
3699 for shortfall in [1e-10, 3e-10, 8e-10] {
3700 let tilt = half_angle - shortfall / (2.0 * half_angle).sin();
3701 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
3702 let chains =
3703 exact_plane_analytic_reaching(AnalyticSurface::Cone(&cone), normal, 1.0, 10.0)
3704 .unwrap();
3705 let Some(chain) = chains.iter().find_map(|c| match c {
3706 ExactIntersectionCurve::Points(chain) => Some(chain),
3707 _ => None,
3708 }) else {
3709 continue;
3710 };
3711 let (from, to) = (chain[2], chain[chain.len() - 3]);
3712 assert!(
3713 plane_cone_conic_arc(&cone, normal, 1.0, from, from)
3714 .unwrap()
3715 .is_none(),
3716 "coincident ends"
3717 );
3718 let Some(arc) = plane_cone_conic_arc(&cone, normal, 1.0, from, to).unwrap() else {
3719 continue;
3720 };
3721 let (t0, t1) = arc.domain();
3722 for i in 0..=200 {
3723 let w = arc.evaluate(t0 + (t1 - t0) * f64::from(i) / 200.0)
3724 - Point3::new(0.0, 0.0, 0.0);
3725 let off_cone = (w.z() - w.length() * half_angle.sin()).abs();
3726 assert!(off_cone < 1e-8, "{shortfall}: off the cone by {off_cone}");
3727 }
3728 }
3729 }
3730
3731 #[test]
3732 fn plane_cylinder_perpendicular() {
3733 let cyl =
3734 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3735 .unwrap();
3736
3737 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
3739 assert!(!curves.is_empty(), "should find intersection curve");
3740 assert!(
3741 curves[0].points.len() > 10,
3742 "should have many sample points"
3743 );
3744
3745 let tol = Tolerance::loose();
3746 for pt in &curves[0].points {
3747 assert!(
3748 tol.approx_eq(pt.point.z(), 3.0),
3749 "z should be ~3.0, got {}",
3750 pt.point.z()
3751 );
3752 let r = pt.point.x().hypot(pt.point.y());
3753 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
3754 }
3755 }
3756
3757 #[test]
3758 fn plane_sphere_equator() {
3759 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
3760
3761 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
3762 assert!(!curves.is_empty());
3763
3764 let tol = Tolerance::loose();
3765 for pt in &curves[0].points {
3766 assert!(
3767 tol.approx_eq(pt.point.z(), 0.0),
3768 "z should be ~0, got {}",
3769 pt.point.z()
3770 );
3771 let r = pt.point.x().hypot(pt.point.y());
3772 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
3773 }
3774 }
3775
3776 #[test]
3777 fn plane_sphere_no_intersection() {
3778 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
3779
3780 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
3781 assert!(curves.is_empty());
3782 }
3783
3784 #[test]
3785 fn plane_cone_cross_section() {
3786 let cone = ConicalSurface::new(
3787 Point3::new(0.0, 0.0, 0.0),
3788 Vec3::new(0.0, 0.0, 1.0),
3789 std::f64::consts::FRAC_PI_4,
3790 )
3791 .unwrap();
3792
3793 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
3794 assert!(!curves.is_empty(), "should find intersection with cone");
3795 }
3796
3797 #[test]
3804 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
3805 let c1 = ConicalSurface::new(
3806 Point3::new(
3807 -16.999_999_999_999_975,
3808 -16.999_999_999_999_975,
3809 5.849_999_999_999_951,
3810 ),
3811 Vec3::new(0.0, 0.0, -1.0),
3812 0.785_398_163_397_433_5,
3813 )
3814 .unwrap();
3815 let c2 = ConicalSurface::new(
3816 Point3::new(
3817 -16.750_000_000_000_036,
3818 -16.750_000_000_000_018,
3819 0.749_999_999_999_881,
3820 ),
3821 Vec3::new(0.0, 0.0, 1.0),
3822 0.785_398_163_397_467_6,
3823 )
3824 .unwrap();
3825
3826 let curves = exact_cone_cone(&c1, &c2)
3827 .unwrap()
3828 .expect("offset parallel equal-angle cones must take the radical-plane path");
3829 assert_eq!(curves.len(), 1, "expected exactly one section conic");
3830 assert!(
3831 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
3832 "expected an ellipse section, got {:?}",
3833 curves[0]
3834 );
3835 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
3836 return;
3837 };
3838
3839 for i in 0..16 {
3843 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
3844 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
3845 let rel = p - cone.apex();
3846 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
3847 let axial = rel_v.dot(cone.axis());
3848 let radial = (rel_v - cone.axis() * axial).length();
3849 assert!(
3850 axial > 0.0,
3851 "{label}: sample on phantom nappe (axial {axial})"
3852 );
3853 let expect = cone.half_angle().tan() * axial;
3854 assert!(
3855 (radial - expect).abs() < 1e-9,
3856 "{label}: sample off surface by {}",
3857 (radial - expect).abs()
3858 );
3859 }
3860 }
3861 }
3862
3863 #[test]
3867 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
3868 let c1 = ConicalSurface::new(
3869 Point3::new(0.0, 0.0, 5.0),
3870 Vec3::new(0.0, 0.0, -1.0),
3871 std::f64::consts::FRAC_PI_4,
3872 )
3873 .unwrap();
3874 let c2 = ConicalSurface::new(
3875 Point3::new(0.25, 0.25, 20.0),
3876 Vec3::new(0.0, 0.0, 1.0),
3877 std::f64::consts::FRAC_PI_4,
3878 )
3879 .unwrap();
3880 let curves = exact_cone_cone(&c1, &c2)
3881 .unwrap()
3882 .expect("radical-plane path");
3883 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
3884 }
3885
3886 #[test]
3889 fn offset_parallel_cones_with_unequal_angles_defer() {
3890 let c1 = ConicalSurface::new(
3891 Point3::new(0.0, 0.0, 5.0),
3892 Vec3::new(0.0, 0.0, -1.0),
3893 std::f64::consts::FRAC_PI_4,
3894 )
3895 .unwrap();
3896 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
3897 .unwrap();
3898 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
3899 }
3900
3901 fn cone_and_tilted_tube() -> (ConicalSurface, CylindricalSurface) {
3905 let cone = ConicalSurface::new(
3906 Point3::new(0.0, 0.0, 0.0),
3907 Vec3::new(0.0, 0.0, 1.0),
3908 std::f64::consts::FRAC_PI_4,
3909 )
3910 .unwrap();
3911 let (s, c) = 40.0_f64.to_radians().sin_cos();
3912 let tube =
3913 CylindricalSurface::new(Point3::new(0.1, 0.0, 3.0), Vec3::new(0.0, s, c), 0.1).unwrap();
3914 (cone, tube)
3915 }
3916
3917 #[test]
3918 fn marcher_keeps_to_its_region() {
3919 let (cone, tube) = cone_and_tilted_tube();
3920 let run = |region: Option<Aabb3>| {
3921 let (a, b) = (
3922 AnalyticSurface::Cone(&cone),
3923 AnalyticSurface::Cylinder(&tube),
3924 );
3925 let (va, vb) = (Some((0.5, 4.0)), Some((-5.0, 5.0)));
3926 match region {
3927 Some(r) => intersect_analytic_analytic_in_region(a, b, 32, va, vb, r),
3928 None => intersect_analytic_analytic_bounded(a, b, 32, va, vb),
3929 }
3930 .unwrap()
3931 };
3932 assert!(!run(None).is_empty());
3933 let near = Aabb3 {
3934 min: Point3::new(-0.5, -2.0, 0.8),
3935 max: Point3::new(0.7, -0.7, 2.0),
3936 };
3937 let curves = run(Some(near));
3938 assert!(!curves.is_empty(), "the loop through the region is kept");
3939 let reach = near.expanded(0.6);
3941 for curve in &curves {
3942 assert!(curve.points.iter().all(|p| reach.contains_point(p.point)));
3943 }
3944 let away = Aabb3 {
3945 min: Point3::new(5.0, 5.0, 5.0),
3946 max: Point3::new(6.0, 6.0, 6.0),
3947 };
3948 assert!(run(Some(away)).is_empty(), "nothing is marched outside it");
3949 }
3950
3951 #[test]
3952 fn coaxial_cones_cross_at_single_circle() {
3953 let outer = ConicalSurface::new(
3958 Point3::new(0.0, 0.0, 50.0),
3959 Vec3::new(0.0, 0.0, -1.0),
3960 5.0_f64.atan(),
3961 )
3962 .unwrap();
3963 let inner = ConicalSurface::new(
3964 Point3::new(0.0, 0.0, 90.0),
3965 Vec3::new(0.0, 0.0, -1.0),
3966 10.0_f64.atan(),
3967 )
3968 .unwrap();
3969
3970 let curves = intersect_analytic_analytic_bounded(
3971 AnalyticSurface::Cone(&outer),
3972 AnalyticSurface::Cone(&inner),
3973 32,
3974 None,
3975 None,
3976 )
3977 .unwrap();
3978
3979 assert_eq!(
3980 curves.len(),
3981 1,
3982 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
3983 curves.len()
3984 );
3985 for p in &curves[0].points {
3986 let r = p.point.x().hypot(p.point.y());
3987 assert!(
3988 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
3989 "intersection point off the expected z=10,r=8 circle: {:?}",
3990 p.point
3991 );
3992 }
3993 }
3994
3995 #[test]
3996 fn plane_torus_cross_section() {
3997 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
3998
3999 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
4000 assert!(
4001 !curves.is_empty(),
4002 "should find intersection curves with torus"
4003 );
4004 }
4005
4006 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
4009 let rho = p.x().hypot(p.y());
4010 ((rho - major).hypot(p.z())) - minor
4011 }
4012
4013 #[test]
4019 fn oblique_cone_cylinder_traces_curves_on_both() {
4020 use crate::traits::ParametricCurve;
4021 let cone = ConicalSurface::new(
4025 Point3::new(0.0, 0.0, 3.0),
4026 Vec3::new(0.0, 0.0, -1.0),
4027 2.0_f64.atan(),
4028 )
4029 .unwrap();
4030 for (x0, loops) in [(0.5, 1), (0.0, 2)] {
4031 let cyl =
4032 CylindricalSurface::new(Point3::new(x0, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4033 .unwrap();
4034 for cone_first in [true, false] {
4035 let (a, b) = if cone_first {
4036 (
4037 AnalyticSurface::Cone(&cone),
4038 AnalyticSurface::Cylinder(&cyl),
4039 )
4040 } else {
4041 (
4042 AnalyticSurface::Cylinder(&cyl),
4043 AnalyticSurface::Cone(&cone),
4044 )
4045 };
4046 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4047 assert_eq!(curves.len(), loops, "x0 {x0}: loops");
4048 for c in &curves {
4049 let (t0, t1) = c.curve.domain();
4050 for k in 0..=64 {
4051 let t = (t1 - t0).mul_add(f64::from(k) / 64.0, t0);
4052 let p = ParametricCurve::evaluate(&c.curve, t);
4053 let rod = (p.x() - x0).hypot(p.z() - 1.0);
4056 assert!(
4057 (rod - 0.6).abs() < 1e-4,
4058 "x0 {x0}: off the rod by {}",
4059 rod - 0.6
4060 );
4061 let cone_r = p.x().hypot(p.y());
4062 assert!(
4063 (cone_r - 0.5 * (3.0 - p.z())).abs() < 1e-4,
4064 "x0 {x0}: off the cone at {p:?}"
4065 );
4066 }
4067 }
4068 }
4069 }
4070 }
4071
4072 #[test]
4073 fn a_rod_through_a_rings_tube_traces_four_loops() {
4074 use crate::traits::ParametricCurve;
4075 let ring = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4076 let rod =
4079 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.3), Vec3::new(0.0, 1.0, 0.0), 0.6)
4080 .unwrap();
4081 let curves = ruling_torus_cylinder(&ring, &rod, true).unwrap();
4082 assert_eq!(curves.len(), 4);
4083 for c in &curves {
4084 let (t0, t1) = c.curve.domain();
4085 for k in 0..=64 {
4086 let p =
4087 ParametricCurve::evaluate(&c.curve, (t1 - t0).mul_add(f64::from(k) / 64.0, t0));
4088 let on_rod = (p.x() - 0.5).hypot(p.z() - 0.3) - 0.6;
4089 let on_ring = (p.x().hypot(p.y()) - 4.0).hypot(p.z()) - 1.5;
4090 assert!(
4091 on_rod.abs() < 1e-4 && on_ring.abs() < 1e-4,
4092 "off by {on_rod}, {on_ring}"
4093 );
4094 }
4095 }
4096 let high =
4098 CylindricalSurface::new(Point3::new(0.5, 0.0, 1.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4099 .unwrap();
4100 assert!(ruling_torus_cylinder(&ring, &high, true).is_none());
4101 let grazing =
4103 CylindricalSurface::new(Point3::new(0.5, 0.0, 0.9001), Vec3::new(0.0, 1.0, 0.0), 0.6)
4104 .unwrap();
4105 assert!(ruling_torus_cylinder(&ring, &grazing, true).is_none());
4106 let spindle = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0, 2.0).unwrap();
4108 let thin =
4109 CylindricalSurface::new(Point3::new(0.3, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 0.2)
4110 .unwrap();
4111 assert!(ruling_torus_cylinder(&spindle, &thin, true).is_none());
4112 }
4113
4114 #[test]
4115 fn a_pin_through_a_ball_traces_two_loops() {
4116 use crate::traits::ParametricCurve;
4117 let ball = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
4118 let half = 0.08_f64.atan();
4121 let apex = Point3::new(1.0, 0.5, -5.0 + 1.2 / 0.08);
4122 let pin = ConicalSurface::new(apex, Vec3::new(0.0, 0.0, -1.0), FRAC_PI_2 - half).unwrap();
4123 for cone_first in [true, false] {
4124 let (a, b) = if cone_first {
4125 (AnalyticSurface::Cone(&pin), AnalyticSurface::Sphere(&ball))
4126 } else {
4127 (AnalyticSurface::Sphere(&ball), AnalyticSurface::Cone(&pin))
4128 };
4129 let curves = intersect_analytic_analytic(a, b, 32).unwrap();
4130 assert_eq!(curves.len(), 2, "entry and exit loops");
4131 for c in &curves {
4132 let (t0, t1) = c.curve.domain();
4133 for k in 0..=64 {
4134 let p = ParametricCurve::evaluate(
4135 &c.curve,
4136 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4137 );
4138 let on_ball = (p - Point3::new(0.0, 0.0, 0.0)).length() - 3.0;
4139 let axial = apex.z() - p.z();
4140 let on_pin = (p.x() - 1.0).hypot(p.y() - 0.5) - axial * half.tan();
4141 assert!(
4142 on_ball.abs() < 1e-4 && on_pin.abs() < 1e-4,
4143 "off by {on_ball}, {on_pin}"
4144 );
4145 }
4146 }
4147 }
4148 let coaxial =
4153 ConicalSurface::new(Point3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0), 1.4)
4154 .unwrap();
4155 assert!(ruling_cone_sphere(&coaxial, &ball, true).is_none());
4156 let aside = ConicalSurface::new(
4157 Point3::new(2.8, 0.0, 10.0),
4158 Vec3::new(0.0, 0.0, -1.0),
4159 FRAC_PI_2 - half,
4160 )
4161 .unwrap();
4162 assert_eq!(ruling_cone_sphere(&aside, &ball, true).unwrap().len(), 1);
4163 let holding = ConicalSurface::new(
4164 Point3::new(1.0, 0.5, 1.0),
4165 Vec3::new(0.0, 0.0, -1.0),
4166 FRAC_PI_2 - half,
4167 )
4168 .unwrap();
4169 assert_eq!(ruling_cone_sphere(&holding, &ball, true).unwrap().len(), 1);
4170 let away = ConicalSurface::new(
4171 Point3::new(1.0, 0.5, 10.0),
4172 Vec3::new(0.0, 0.0, 1.0),
4173 FRAC_PI_2 - half,
4174 )
4175 .unwrap();
4176 assert!(ruling_cone_sphere(&away, &ball, true).unwrap().is_empty());
4177 let step = TAU / 2048.0;
4181 let grazed =
4182 SphericalSurface::new(Point3::new(step.cos(), step.sin(), 10.0), 9.255_250_971_8)
4183 .unwrap();
4184 let wide =
4185 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5).unwrap();
4186 assert_eq!(ruling_cone_sphere(&wide, &grazed, true).unwrap().len(), 1);
4187 }
4188
4189 #[test]
4190 fn a_ball_beside_a_cone_meets_it_in_one_loop() {
4191 use crate::traits::ParametricCurve;
4192 let cone = ConicalSurface::new(
4194 Point3::new(0.0, 0.0, 3.0),
4195 Vec3::new(0.0, 0.0, -1.0),
4196 2.0_f64.atan(),
4197 )
4198 .unwrap();
4199 for (centre, radius) in [
4200 (Point3::new(1.0, 0.8, 1.2), 1.1),
4201 (Point3::new(1.5, 0.0, 0.0), 0.8),
4202 ] {
4203 let ball = SphericalSurface::new(centre, radius).unwrap();
4204 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4205 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4206 let (t0, t1) = curves[0].curve.domain();
4207 for k in 0..=64 {
4208 let p = ParametricCurve::evaluate(
4209 &curves[0].curve,
4210 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4211 );
4212 let on_ball = (p - centre).length() - radius;
4213 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4214 assert!(
4215 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5,
4216 "ball at {centre:?}: off by {on_ball}, {on_cone}"
4217 );
4218 }
4219 }
4220 let clear = SphericalSurface::new(Point3::new(4.0, 0.0, 0.0), 0.5).unwrap();
4222 assert!(ruling_cone_sphere(&cone, &clear, true).unwrap().is_empty());
4223 let on_apex = SphericalSurface::new(Point3::new(0.6, 0.0, 3.8), 1.0).unwrap();
4224 assert!(ruling_cone_sphere(&cone, &on_apex, true).is_none());
4225 }
4226
4227 #[test]
4228 fn a_ball_holding_a_cones_apex_meets_it_in_one_loop() {
4229 use crate::traits::ParametricCurve;
4230 let cone = ConicalSurface::new(
4231 Point3::new(0.0, 0.0, 3.0),
4232 Vec3::new(0.0, 0.0, -1.0),
4233 2.0_f64.atan(),
4234 )
4235 .unwrap();
4236 for (centre, radius) in [
4239 (Point3::new(0.5, 0.0, 2.5), 2.0),
4240 (Point3::new(-0.4, 0.3, 2.0), 1.5),
4241 (Point3::new(0.0, 0.8, 3.0), 0.8001),
4242 (Point3::new(0.0, 0.8, 3.0), 0.800_001),
4243 ] {
4244 let ball = SphericalSurface::new(centre, radius).unwrap();
4245 let curves = ruling_cone_sphere(&cone, &ball, true).unwrap();
4246 assert_eq!(curves.len(), 1, "one loop for the ball at {centre:?}");
4247 let (t0, t1) = curves[0].curve.domain();
4248 for k in 0..=4096 {
4249 let p = ParametricCurve::evaluate(
4250 &curves[0].curve,
4251 (t1 - t0).mul_add(f64::from(k) / 4096.0, t0),
4252 );
4253 let on_ball = (p - centre).length() - radius;
4254 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4255 assert!(
4256 on_ball.abs() < 1e-5 && on_cone.abs() < 1e-5 && p.z() < 3.0,
4257 "ball at {centre:?}: off by {on_ball}, {on_cone} at {p:?}"
4258 );
4259 }
4260 }
4261 }
4262
4263 #[test]
4264 fn oblique_cone_cylinder_defers_where_rulings_cannot_trace_it() {
4265 let t = 2.0_f64.atan();
4266 let cone =
4267 ConicalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), t).unwrap();
4268 let through_apex =
4270 CylindricalSurface::new(Point3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 1.0, 0.0), 0.6)
4271 .unwrap();
4272 assert!(ruling_cone_cylinder(&cone, &through_apex, true).is_none());
4273 let generator = Vec3::new(t.cos(), 0.0, -t.sin());
4275 let along = CylindricalSurface::new(Point3::new(0.0, 0.3, 0.0), generator, 0.2).unwrap();
4276 assert!(ruling_cone_cylinder(&cone, &along, true).is_none());
4277 let pin =
4280 ConicalSurface::new(Point3::new(20.5, 0.0, 0.0), Vec3::new(-1.0, 0.0, 0.0), t).unwrap();
4281 let tube =
4282 CylindricalSurface::new(Point3::new(0.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 1.0), 20.0)
4283 .unwrap();
4284 assert!(ruling_cone_cylinder(&pin, &tube, true).is_none());
4285 }
4286
4287 #[test]
4288 fn parallel_cone_cylinder_gives_two_exact_branches() {
4289 use crate::traits::ParametricCurve;
4290 let cone = ConicalSurface::new(
4291 Point3::new(-5.45, -36.55, -4.85),
4292 Vec3::new(0.0, 0.0, 1.0),
4293 std::f64::consts::FRAC_PI_4,
4294 )
4295 .unwrap();
4296 let cyl = CylindricalSurface::new(
4297 Point3::new(-8.0, -34.0, -5.0),
4298 Vec3::new(0.0, 0.0, 1.0),
4299 4.45,
4300 )
4301 .unwrap();
4302 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
4304 let curves = intersect_analytic_analytic_bounded(
4305 AnalyticSurface::Cone(&cone),
4306 AnalyticSurface::Cylinder(&cyl),
4307 32,
4308 Some(v_hint),
4309 Some((0.0, 2.5)),
4310 )
4311 .unwrap();
4312
4313 assert_eq!(curves.len(), 2, "expected exactly the two branches");
4314 for c in &curves {
4315 let (t0, t1) = c.curve.domain();
4316 for k in 0..=32 {
4317 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
4318 let p = ParametricCurve::evaluate(&c.curve, t);
4319 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
4321 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
4322 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
4324 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
4325 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
4327 }
4328 }
4329 }
4330
4331 #[test]
4332 fn parallel_rod_through_a_cones_wall_closes_one_loop() {
4333 use crate::traits::ParametricCurve;
4334 let cone = ConicalSurface::new(
4336 Point3::new(0.0, 0.0, 3.0),
4337 Vec3::new(0.0, 0.0, -1.0),
4338 2.0_f64.atan(),
4339 )
4340 .unwrap();
4341 for (x, y) in [(0.0, 1.3), (1.2, 0.5)] {
4343 let rod =
4344 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4345 .unwrap();
4346 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4347 .unwrap()
4348 .unwrap();
4349 assert_eq!(curves.len(), 1, "one closed loop at ({x}, {y})");
4350 let (t0, t1) = curves[0].curve.domain();
4351 let (first, last) = (
4352 ParametricCurve::evaluate(&curves[0].curve, t0),
4353 ParametricCurve::evaluate(&curves[0].curve, t1),
4354 );
4355 assert!((first - last).length() < 1e-9, "open at ({x}, {y})");
4356 for k in 0..=64 {
4357 let p = ParametricCurve::evaluate(
4358 &curves[0].curve,
4359 (t1 - t0).mul_add(f64::from(k) / 64.0, t0),
4360 );
4361 let on_rod = (p.x() - x).hypot(p.y() - y) - 0.6;
4362 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4363 assert!(
4364 on_rod.abs() < 1e-5 && on_cone.abs() < 1e-5,
4365 "({x}, {y}): off by {on_rod}, {on_cone}"
4366 );
4367 }
4368 }
4369 for (x, y) in [(0.3, 0.2), (0.65, 0.0)] {
4372 let rod =
4373 CylindricalSurface::new(Point3::new(x, y, -10.0), Vec3::new(0.0, 0.0, 1.0), 0.6)
4374 .unwrap();
4375 let curves = algebraic_parallel_cone_cylinder(&cone, &rod, None, None)
4376 .unwrap()
4377 .unwrap();
4378 assert_eq!(curves.len(), 2, "two branches at ({x}, {y})");
4379 }
4380 }
4381
4382 #[test]
4385 fn coaxial_cone_cylinder_defers_to_other_paths() {
4386 let cone = ConicalSurface::new(
4387 Point3::new(0.0, 0.0, 0.0),
4388 Vec3::new(0.0, 0.0, 1.0),
4389 std::f64::consts::FRAC_PI_4,
4390 )
4391 .unwrap();
4392 let cyl =
4393 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
4394 .unwrap();
4395 assert!(
4396 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4397 .unwrap()
4398 .is_none()
4399 );
4400 }
4401
4402 #[test]
4403 fn oblique_cone_cylinder_defers_to_other_paths() {
4404 let cone = ConicalSurface::new(
4405 Point3::new(0.0, 0.0, 0.0),
4406 Vec3::new(0.0, 0.0, 1.0),
4407 std::f64::consts::FRAC_PI_4,
4408 )
4409 .unwrap();
4410 let cyl =
4411 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4412 .unwrap();
4413 assert!(
4414 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
4415 .unwrap()
4416 .is_none()
4417 );
4418 }
4419
4420 #[test]
4421 fn plane_torus_lobe_closes_and_stays_on_surface() {
4422 use crate::traits::ParametricCurve;
4423 let (major, minor) = (10.0, 3.0);
4424 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4425
4426 for (n, d) in [
4430 (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), ] {
4434 let curves = intersect_plane_torus(&torus, n, d).unwrap();
4435 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
4436 for c in &curves {
4437 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4438 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4439 assert!(
4440 (p0 - p1).length() < 1e-7,
4441 "lobe not closed: gap={} (n={n:?} d={d})",
4442 (p0 - p1).length()
4443 );
4444 for k in 0..=64 {
4446 let t = f64::from(k) / 64.0;
4447 let p = ParametricCurve::evaluate(&c.curve, t);
4448 assert!(
4449 torus_implicit(p, major, minor).abs() < 1e-2,
4450 "off-surface point {p:?} implicit={}",
4451 torus_implicit(p, major, minor)
4452 );
4453 }
4454 }
4455 }
4456 }
4457
4458 #[test]
4459 fn plane_torus_inner_tangent_figure_eight_stays_open() {
4460 use crate::traits::ParametricCurve;
4461 let (major, minor) = (10.0, 3.0);
4462 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4463
4464 let curves =
4469 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
4470 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
4471 let max_gap = curves
4472 .iter()
4473 .map(|c| {
4474 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
4475 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
4476 (p0 - p1).length()
4477 })
4478 .fold(0.0_f64, f64::max);
4479 assert!(
4480 max_gap > 1e-2,
4481 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
4482 );
4483 }
4484
4485 #[test]
4490 fn plane_torus_wall_sections_close_into_their_loops() {
4491 for (major, minor) in [(4.0, 1.5), (100.0, 30.0), (0.05, 0.01)] {
4492 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4493 for k in 1..200 {
4494 let (d, want) = match k.cmp(&100) {
4495 std::cmp::Ordering::Less => ((major - minor) * f64::from(k) / 100.0, 2),
4497 std::cmp::Ordering::Greater => (
4499 2.0f64.mul_add(minor * f64::from(k - 100) / 100.0, major - minor),
4500 1,
4501 ),
4502 std::cmp::Ordering::Equal => continue,
4503 };
4504 let loops = plane_torus_loops(&torus, Vec3::new(1.0, 0.0, 0.0), d, 128);
4505 let closed = loops
4506 .iter()
4507 .filter(|l| (l[0].point - l[l.len() - 1].point).length() < 1e-12)
4508 .count();
4509 assert_eq!(
4510 (loops.len(), closed),
4511 (want, want),
4512 "R {major} r {minor}, wall at {d}"
4513 );
4514 }
4515 }
4516 }
4517
4518 #[test]
4522 fn plane_torus_sections_round_the_axis_stay_on_the_torus() {
4523 let (major, minor) = (4.0, 1.5);
4524 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
4525 for tilt in [0.03_f64, 0.08, 0.2] {
4526 let normal = Vec3::new(tilt.sin(), 0.0, tilt.cos());
4527 let curves = intersect_plane_torus(&torus, normal, 0.0).unwrap();
4528 assert_eq!(curves.len(), 2, "tilt {tilt}");
4529 for c in &curves {
4530 let (t0, t1) = c.curve.domain();
4531 let off = (0..=400)
4532 .map(|k| {
4533 let p = c
4534 .curve
4535 .evaluate((t1 - t0).mul_add(f64::from(k) / 400.0, t0));
4536 (p.x().hypot(p.y()) - major).hypot(p.z()) - minor
4537 })
4538 .fold(0.0_f64, |m, e| m.max(e.abs()));
4539 assert!(
4540 off < 1e-6,
4541 "tilt {tilt}: fitted section {off} off the torus"
4542 );
4543 }
4544 }
4545 }
4546
4547 #[test]
4548 fn line_torus_box_edge_crossing_is_exact() {
4549 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4552 let ts = intersect_line_torus(
4553 &torus,
4554 Point3::new(6.0, -4.0, -5.0),
4555 Vec3::new(0.0, 0.0, 1.0),
4556 );
4557 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
4559 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
4560 let rho = 6.0_f64.hypot(4.0);
4561 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
4562 assert!(
4563 (zs[0] - (-z_exp)).abs() < 1e-9,
4564 "z0={} exp={}",
4565 zs[0],
4566 -z_exp
4567 );
4568 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
4569 for &t in &ts {
4571 let p = Point3::new(6.0, -4.0, -5.0 + t);
4572 let rho = p.x().hypot(p.y());
4573 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
4574 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
4575 }
4576 }
4577
4578 #[test]
4579 fn line_torus_miss_and_tangent() {
4580 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
4581 let miss = intersect_line_torus(
4583 &torus,
4584 Point3::new(20.0, 0.0, 0.0),
4585 Vec3::new(0.0, 0.0, 1.0),
4586 );
4587 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
4588 let axis =
4590 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
4591 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
4592 }
4593
4594 #[test]
4595 fn dispatch_via_analytic_surface() {
4596 let cyl =
4597 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4598 .unwrap();
4599 let curves = intersect_plane_analytic(
4600 AnalyticSurface::Cylinder(&cyl),
4601 Vec3::new(0.0, 0.0, 1.0),
4602 0.0,
4603 )
4604 .unwrap();
4605 assert!(!curves.is_empty());
4606 }
4607
4608 #[test]
4609 fn perpendicular_cylinders_intersect() {
4610 let cyl_z =
4611 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4612 .unwrap();
4613 let cyl_x =
4614 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4615 .unwrap();
4616
4617 let curves = intersect_analytic_analytic(
4618 AnalyticSurface::Cylinder(&cyl_z),
4619 AnalyticSurface::Cylinder(&cyl_x),
4620 16,
4621 )
4622 .unwrap();
4623
4624 assert!(
4625 !curves.is_empty(),
4626 "perpendicular cylinders should intersect"
4627 );
4628
4629 for c in &curves {
4630 assert!(
4631 c.points.len() >= 2,
4632 "intersection curve should have >= 2 points, got {}",
4633 c.points.len()
4634 );
4635 }
4636 }
4637
4638 #[test]
4641 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
4642 let cyl_z =
4643 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4644 .unwrap();
4645 let cyl_x =
4646 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
4647 .unwrap();
4648 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4649 .unwrap()
4650 .unwrap();
4651 assert_eq!(curves.len(), 1);
4652 let curve = &curves[0].curve;
4653 let (t0, t1) = curve.domain();
4654 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
4655 let off = |p: Point3| {
4656 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
4657 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
4658 on_z.max(on_x)
4659 };
4660 let worst = (0..=400)
4661 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
4662 .fold(0.0, f64::max);
4663 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
4664 }
4665
4666 #[test]
4670 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
4671 let cyl_z =
4672 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4673 .unwrap();
4674 let cyl_x =
4675 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
4676 .unwrap();
4677 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
4678 .unwrap()
4679 .expect("the thin cylinder's sweep finds the loop");
4680 assert_eq!(curves.len(), 1);
4681 }
4682
4683 #[test]
4684 fn sphere_cylinder_intersect() {
4685 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4686 let cyl =
4687 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
4688 .unwrap();
4689
4690 let curves = intersect_analytic_analytic(
4691 AnalyticSurface::Sphere(&sphere),
4692 AnalyticSurface::Cylinder(&cyl),
4693 16,
4694 )
4695 .unwrap();
4696
4697 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
4701 }
4702
4703 #[test]
4704 fn exact_sphere_cylinder_coaxial_two_circles() {
4705 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4708 let cyl =
4709 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4710 .unwrap();
4711 let circles = exact_sphere_cylinder(&sphere, &cyl)
4712 .unwrap()
4713 .expect("coaxial case returns Some");
4714 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
4715 let mut zs: Vec<f64> = circles
4716 .iter()
4717 .filter_map(|c| match c {
4718 ExactIntersectionCurve::Circle(circle) => {
4719 assert!(
4720 (circle.radius() - 3.0).abs() < 1e-9,
4721 "rim radius == cyl radius"
4722 );
4723 Some(circle.center().z())
4724 }
4725 _ => None,
4726 })
4727 .collect();
4728 assert_eq!(zs.len(), 2, "both sections must be exact circles");
4729 zs.sort_by(f64::total_cmp);
4730 let z = 27.0_f64.sqrt();
4731 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
4732 }
4733
4734 #[test]
4735 fn exact_sphere_cylinder_non_coaxial_defers() {
4736 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
4738 let cyl =
4739 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
4740 .unwrap();
4741 assert!(
4742 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
4743 "non-coaxial sphere/cylinder defers to the marcher"
4744 );
4745 }
4746
4747 #[test]
4748 fn a_ball_on_a_cones_axis_meets_it_in_circles() {
4749 let cone = ConicalSurface::new(
4751 Point3::new(0.0, 0.0, 3.0),
4752 Vec3::new(0.0, 0.0, -1.0),
4753 2.0_f64.atan(),
4754 )
4755 .unwrap();
4756 for (height, radius, count) in [
4757 (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), ] {
4763 let centre = Point3::new(0.0, 0.0, height);
4764 let ball = SphericalSurface::new(centre, radius).unwrap();
4765 let curves = exact_cone_sphere(&cone, &ball).unwrap().unwrap();
4766 let circles = circles_of(&curves);
4767 assert_eq!(circles.len(), count, "ball at {height}, radius {radius}");
4768 for circle in circles {
4769 for k in 0..16 {
4770 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4771 let on_ball = (p - centre).length() - radius;
4772 let on_cone = p.x().hypot(p.y()) - (3.0 - p.z()) / 2.0;
4773 assert!(
4774 on_ball.abs() < 1e-9 && on_cone.abs() < 1e-9,
4775 "ball at {height}: off by {on_ball}, {on_cone}"
4776 );
4777 }
4778 }
4779 }
4780 let aside = SphericalSurface::new(Point3::new(0.5, 0.0, 0.0), 2.0).unwrap();
4781 assert!(exact_cone_sphere(&cone, &aside).unwrap().is_none());
4782 let wide =
4785 ConicalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.3).unwrap();
4786 let far = SphericalSurface::new(Point3::new(0.0, 0.0, 1e6), 1e6 * 0.3_f64.cos()).unwrap();
4787 let curves = exact_cone_sphere(&wide, &far).unwrap().unwrap();
4788 let circles = circles_of(&curves);
4789 assert_eq!(circles.len(), 1, "the touch");
4790 let touch = 1e6 * 0.3_f64.sin() * 0.3_f64.cos();
4791 assert!(
4792 (circles[0].radius() - touch).abs() < 1e-3,
4793 "{}",
4794 circles[0].radius()
4795 );
4796 }
4797
4798 fn circles_of(curves: &[ExactIntersectionCurve]) -> Vec<&Circle3D> {
4800 curves
4801 .iter()
4802 .filter_map(|c| match c {
4803 ExactIntersectionCurve::Circle(circle) => Some(circle),
4804 _ => None,
4805 })
4806 .collect()
4807 }
4808
4809 fn worst_off(
4812 circles: &[&Circle3D],
4813 torus: &ToroidalSurface,
4814 other: impl Fn(Point3) -> f64,
4815 ) -> f64 {
4816 let mut worst = 0.0_f64;
4817 for circle in circles {
4818 for k in 0..16 {
4819 let p = circle.evaluate(TAU * f64::from(k) / 16.0);
4820 let q = p - torus.center();
4821 let along = q.dot(torus.z_axis());
4822 let rho = (q - torus.z_axis() * along).length();
4823 let off = ((rho - torus.major_radius()).hypot(along) - torus.minor_radius()).abs();
4824 worst = worst.max(off).max(other(p).abs());
4825 }
4826 }
4827 worst
4828 }
4829
4830 #[test]
4831 fn exact_sphere_torus_meets_a_ball_on_the_axis_in_circles() {
4832 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4833 for height in [0.0, 1.0] {
4834 let centre = Point3::new(0.0, 0.0, height);
4835 let sphere = SphericalSurface::new(centre, 3.0).unwrap();
4836 let curves = exact_sphere_torus(&sphere, &torus).unwrap().unwrap();
4837 let circles = circles_of(&curves);
4838 assert_eq!((curves.len(), circles.len()), (2, 2), "height {height}");
4839 let worst = worst_off(&circles, &torus, |p| (p - centre).length() - 3.0);
4840 assert!(worst < 1e-9, "height {height}: {worst}");
4841 }
4842 }
4843
4844 #[test]
4845 fn exact_sphere_torus_misses_touches_and_defers() {
4846 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4847 let ball = |x: f64, r: f64| SphericalSurface::new(Point3::new(x, 0.0, 0.0), r).unwrap();
4848 assert!(
4849 exact_sphere_torus(&ball(0.0, 1.0), &torus)
4850 .unwrap()
4851 .unwrap()
4852 .is_empty(),
4853 "a small ball in the hole misses"
4854 );
4855 assert!(
4856 exact_sphere_torus(&ball(0.0, 2.5), &torus)
4857 .unwrap()
4858 .is_none(),
4859 "a ball touching the inner equator defers"
4860 );
4861 assert!(
4862 exact_sphere_torus(&ball(1.0, 3.0), &torus)
4863 .unwrap()
4864 .is_none(),
4865 "a ball off the axis defers"
4866 );
4867 let spindle = ToroidalSurface::with_axis_and_ref_dir(
4868 Point3::new(0.0, 0.0, 0.0),
4869 1.0,
4870 2.0,
4871 Vec3::new(0.0, 0.0, 1.0),
4872 Vec3::new(1.0, 0.0, 0.0),
4873 )
4874 .unwrap();
4875 assert!(
4876 exact_sphere_torus(&ball(0.0, 2.5), &spindle)
4877 .unwrap()
4878 .is_none()
4879 );
4880 }
4881
4882 #[test]
4883 fn exact_cylinder_torus_meets_a_coaxial_rod_in_circles() {
4884 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 4.0, 1.5).unwrap();
4885 let z = Vec3::new(0.0, 0.0, 1.0);
4886 let rod = |r: f64| CylindricalSurface::new(Point3::new(0.0, 0.0, -5.0), z, r).unwrap();
4887 let curves = exact_cylinder_torus(&rod(4.2), &torus).unwrap().unwrap();
4888 let circles = circles_of(&curves);
4889 assert_eq!((curves.len(), circles.len()), (2, 2));
4890 let worst = worst_off(&circles, &torus, |p| p.x().hypot(p.y()) - 4.2);
4891 assert!(worst < 1e-9, "{worst}");
4892 assert!(
4893 exact_cylinder_torus(&rod(2.0), &torus)
4894 .unwrap()
4895 .unwrap()
4896 .is_empty(),
4897 "a rod clear in the hole misses"
4898 );
4899 assert!(
4900 exact_cylinder_torus(&rod(5.5), &torus).unwrap().is_none(),
4901 "a wall touching the outer equator defers"
4902 );
4903 let tilted =
4904 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.1, 1.0), 4.2)
4905 .unwrap();
4906 let offset = CylindricalSurface::new(Point3::new(0.5, 0.0, 0.0), z, 4.2).unwrap();
4907 assert!(exact_cylinder_torus(&tilted, &torus).unwrap().is_none());
4908 assert!(exact_cylinder_torus(&offset, &torus).unwrap().is_none());
4909 let spindle = ToroidalSurface::with_axis_and_ref_dir(
4910 Point3::new(0.0, 0.0, 0.0),
4911 1.0,
4912 2.0,
4913 z,
4914 Vec3::new(1.0, 0.0, 0.0),
4915 )
4916 .unwrap();
4917 assert!(
4918 exact_cylinder_torus(&rod(0.5), &spindle).unwrap().is_none(),
4919 "a spindle torus's inner lemon also meets the rod"
4920 );
4921 }
4922
4923 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
4926 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
4927 let cyl =
4928 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
4929 .unwrap();
4930 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
4931 .unwrap()
4932 .unwrap();
4933 let mut worst: f64 = 0.0;
4934 for c in &curves {
4935 for ip in &c.points {
4936 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
4937 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
4938 worst = worst
4939 .max((on_sphere - ip.point).length())
4940 .max((on_cylinder - ip.point).length());
4941 }
4942 let (t0, t1) = c.curve.domain();
4943 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
4944 for k in 0..=400 {
4945 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
4946 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
4947 let on_cylinder = ((p.x() - cylinder_origin.x())
4948 .hypot(p.y() - cylinder_origin.y())
4949 - cylinder_radius)
4950 .abs();
4951 worst = worst.max(on_sphere).max(on_cylinder);
4952 }
4953 }
4954 (curves.len(), worst)
4955 }
4956
4957 #[test]
4960 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
4961 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
4962 assert_eq!(count, 2);
4963 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
4964 }
4965
4966 #[test]
4968 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
4969 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
4970 assert_eq!(count, 1);
4971 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
4972 }
4973
4974 #[test]
4975 fn disjoint_cylinders_no_intersection() {
4976 let cyl_a =
4977 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4978 .unwrap();
4979 let cyl_b =
4980 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
4981 .unwrap();
4982
4983 let curves = intersect_analytic_analytic(
4984 AnalyticSurface::Cylinder(&cyl_a),
4985 AnalyticSurface::Cylinder(&cyl_b),
4986 16,
4987 )
4988 .unwrap();
4989
4990 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
4991 }
4992
4993 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
4997 use crate::traits::ParametricCurve;
4998 match curve {
4999 ExactIntersectionCurve::Circle(c) => (0..=64)
5000 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
5001 .collect(),
5002 ExactIntersectionCurve::Ellipse(e) => (0..=64)
5003 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
5004 .collect(),
5005 ExactIntersectionCurve::Points(pts) => pts.clone(),
5006 }
5007 }
5008
5009 fn assert_on_plane_and_cone(
5012 curves: &[ExactIntersectionCurve],
5013 cone: &ConicalSurface,
5014 n: Vec3,
5015 d: f64,
5016 z_bound: (f64, f64),
5017 ) {
5018 assert!(!curves.is_empty(), "expected at least one section curve");
5019 let mut total = 0;
5020 for curve in curves {
5021 for p in collect_points(curve) {
5022 total += 1;
5023 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
5024 assert!(
5025 plane_err < 1e-9,
5026 "point off plane by {plane_err:.2e}: {p:?}"
5027 );
5028 let (u, v) = cone.project_point(p);
5029 let q = cone.evaluate(u, v);
5030 let cone_err =
5031 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
5032 .sqrt();
5033 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
5034 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
5035 assert!(
5036 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
5037 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
5038 p.z()
5039 );
5040 }
5041 }
5042 assert!(total >= 8, "too few section points ({total})");
5043 }
5044
5045 #[test]
5046 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
5047 let cone = ConicalSurface::new(
5051 Point3::new(0.0, 0.0, 0.0),
5052 Vec3::new(0.0, 0.0, 1.0),
5053 std::f64::consts::FRAC_PI_4,
5054 )
5055 .unwrap();
5056 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5057 let d = n.z() * 5.0;
5059 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5060 assert!(
5061 curves
5062 .iter()
5063 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
5064 "oblique steep plane × cone must yield an exact Ellipse"
5065 );
5066 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
5068 }
5069
5070 #[test]
5071 fn oblique_plane_cone_wrong_nappe_is_empty() {
5072 let cone = ConicalSurface::new(
5076 Point3::new(0.0, 0.0, 0.0),
5077 Vec3::new(0.0, 0.0, 1.0),
5078 std::f64::consts::FRAC_PI_4,
5079 )
5080 .unwrap();
5081 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
5082 let d = n.z() * -5.0;
5083 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5084 assert!(
5085 curves.is_empty(),
5086 "plane on the phantom-nappe side must yield no real curve, got {}",
5087 curves.len()
5088 );
5089 }
5090
5091 #[test]
5092 fn oblique_plane_cone_parabola_on_both_single_branch() {
5093 let cone = ConicalSurface::new(
5096 Point3::new(0.0, 0.0, 0.0),
5097 Vec3::new(0.0, 0.0, 1.0),
5098 std::f64::consts::FRAC_PI_4,
5099 )
5100 .unwrap();
5101 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
5102 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5104 assert_eq!(
5105 curves.len(),
5106 1,
5107 "a parabola is a single branch, got {}",
5108 curves.len()
5109 );
5110 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
5112 }
5113
5114 #[test]
5115 fn oblique_plane_cone_hyperbola_real_nappe_only() {
5116 let cone = ConicalSurface::new(
5124 Point3::new(-59.0, -59.0, 15.85),
5125 Vec3::new(0.0, 0.0, -1.0),
5126 std::f64::consts::FRAC_PI_4,
5127 )
5128 .unwrap();
5129 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
5130 let d = -58.360_56;
5131 let cos_theta = n.dot(cone.axis()).abs();
5132 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
5133 let curves = exact_plane_cone(&cone, n, d, 0.0).unwrap();
5134 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
5137 for c in &curves {
5139 assert!(
5140 matches!(c, ExactIntersectionCurve::Points(_)),
5141 "hyperbola must be sampled Points, not a closed conic"
5142 );
5143 }
5144 }
5145}