1use std::f64::consts::{FRAC_PI_2, TAU};
8
9use crate::MathError;
10use crate::curves::{Circle3D, Ellipse3D};
11use crate::frame::Frame3;
12use crate::nurbs::fitting::interpolate;
13use crate::nurbs::intersection::{IntersectionCurve, IntersectionPoint};
14use crate::surfaces::{ConicalSurface, CylindricalSurface, SphericalSurface, ToroidalSurface};
15use crate::tolerance::Tolerance;
16use crate::vec::{Point3, Vec3};
17
18#[derive(Debug, Clone)]
20pub enum ExactIntersectionCurve {
21 Circle(Circle3D),
23 Ellipse(Ellipse3D),
25 Points(Vec<Point3>),
27}
28
29pub fn exact_plane_analytic(
40 surface: AnalyticSurface<'_>,
41 plane_normal: Vec3,
42 plane_d: f64,
43) -> Result<Vec<ExactIntersectionCurve>, MathError> {
44 match surface {
45 AnalyticSurface::Cylinder(cyl) => exact_plane_cylinder(cyl, plane_normal, plane_d),
46 AnalyticSurface::Sphere(sphere) => exact_plane_sphere(sphere, plane_normal, plane_d),
47 AnalyticSurface::Cone(cone) => exact_plane_cone(cone, plane_normal, plane_d),
48 AnalyticSurface::Torus(torus) => {
49 let chains = sample_plane_torus(torus, plane_normal, plane_d)?;
51 Ok(chains
52 .into_iter()
53 .map(ExactIntersectionCurve::Points)
54 .collect())
55 }
56 }
57}
58
59fn exact_plane_cylinder(
65 cyl: &CylindricalSurface,
66 normal: Vec3,
67 d: f64,
68) -> Result<Vec<ExactIntersectionCurve>, MathError> {
69 let axis = cyl.axis();
70 let cos_theta = normal.dot(axis).abs();
71 let r = cyl.radius();
72
73 if cos_theta < 1e-10 {
74 let chains = sample_plane_cylinder(cyl, normal, d)?;
77 return Ok(chains
78 .into_iter()
79 .map(ExactIntersectionCurve::Points)
80 .collect());
81 }
82
83 let n_dot_axis = normal.dot(axis);
86 let n_dot_origin = dot_np(normal, cyl.origin());
87 let t = (d - n_dot_origin) / n_dot_axis;
88 let center_on_axis = Point3::new(
89 cyl.origin().x() + t * axis.x(),
90 cyl.origin().y() + t * axis.y(),
91 cyl.origin().z() + t * axis.z(),
92 );
93
94 if cos_theta > 1.0 - 1e-10 {
95 let circle = Circle3D::new(center_on_axis, normal, r)?;
97 Ok(vec![ExactIntersectionCurve::Circle(circle)])
98 } else {
99 let semi_minor = r;
103 let semi_major = r / cos_theta;
104
105 let axis_proj = Vec3::new(
109 axis.x() - n_dot_axis * normal.x(),
110 axis.y() - n_dot_axis * normal.y(),
111 axis.z() - n_dot_axis * normal.z(),
112 );
113 let u_axis = axis_proj.normalize()?;
114 let v_axis = normal.cross(u_axis);
115
116 let ellipse = Ellipse3D::with_axes(
117 center_on_axis,
118 normal,
119 semi_major,
120 semi_minor,
121 u_axis,
122 v_axis,
123 )?;
124 Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)])
125 }
126}
127
128fn exact_plane_sphere(
132 sphere: &SphericalSurface,
133 normal: Vec3,
134 d: f64,
135) -> Result<Vec<ExactIntersectionCurve>, MathError> {
136 let h = dot_np(normal, sphere.center()) - d;
137 let r = sphere.radius();
138
139 if h.abs() > r - 1e-10 {
140 return Ok(vec![]);
141 }
142
143 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
144 let circle_center = Point3::new(
145 h.mul_add(-normal.x(), sphere.center().x()),
146 h.mul_add(-normal.y(), sphere.center().y()),
147 h.mul_add(-normal.z(), sphere.center().z()),
148 );
149
150 let circle = Circle3D::new(circle_center, normal, circle_r)?;
151 Ok(vec![ExactIntersectionCurve::Circle(circle)])
152}
153
154fn exact_plane_cone(
163 cone: &ConicalSurface,
164 normal: Vec3,
165 d: f64,
166) -> Result<Vec<ExactIntersectionCurve>, MathError> {
167 let axis = cone.axis();
168 let cos_theta = normal.dot(axis).abs();
169 let half_angle = cone.half_angle();
170
171 if cos_theta > 1.0 - 1e-10 {
172 let n_dot_axis = normal.dot(axis);
175 let n_dot_apex = dot_np(normal, cone.apex());
176 let t = (d - n_dot_apex) / n_dot_axis;
177
178 if t.abs() < 1e-10 {
183 return Ok(vec![]);
184 }
185
186 let center = Point3::new(
187 cone.apex().x() + t * axis.x(),
188 cone.apex().y() + t * axis.y(),
189 cone.apex().z() + t * axis.z(),
190 );
191 let circle_r = t.abs() * half_angle.cos() / half_angle.sin();
195 if circle_r < 1e-15 {
196 return Ok(vec![]);
197 }
198
199 let circle = Circle3D::new(center, normal, circle_r)?;
200 return Ok(vec![ExactIntersectionCurve::Circle(circle)]);
201 }
202
203 let c = normal.dot(axis);
215 let p2 = (1.0 - c * c).max(0.0);
216 let p = p2.sqrt();
217 let k = half_angle.sin().powi(2);
218 let a_coeff = p2 - k;
219
220 let m = Vec3::new(
222 axis.x() - c * normal.x(),
223 axis.y() - c * normal.y(),
224 axis.z() - c * normal.z(),
225 );
226 let m_len = m.length();
227 if m_len < 1e-12 {
228 let chains = sample_plane_cone(cone, normal, d)?;
231 return Ok(chains
232 .into_iter()
233 .map(ExactIntersectionCurve::Points)
234 .collect());
235 }
236 let e1 = m * (1.0 / m_len);
237 let e2 = normal.cross(e1);
238 let apex = cone.apex();
239 let e = d - dot_np(normal, apex);
240
241 if a_coeff < -1e-9 {
244 let abs_a = -a_coeff; if e * c < 0.0 {
251 return Ok(vec![]);
252 }
253 let s_c = e * c * p / abs_a;
256 let rhs = e * e * k * (1.0 - k) / abs_a;
257 if rhs <= 0.0 {
258 return Ok(vec![]);
259 }
260 let semi_s = (rhs / abs_a).sqrt(); let semi_t = (rhs / k).sqrt(); if semi_s < 1e-12 || semi_t < 1e-12 {
263 return Ok(vec![]);
264 }
265 let center = apex + normal * e + e1 * s_c;
266 let (semi_major, semi_minor, u_axis, v_axis) = if semi_s >= semi_t {
267 (semi_s, semi_t, e1, e2)
268 } else {
269 (semi_t, semi_s, e2, e1)
270 };
271 let ellipse = Ellipse3D::with_axes(center, normal, semi_major, semi_minor, u_axis, v_axis)?;
272 return Ok(vec![ExactIntersectionCurve::Ellipse(ellipse)]);
273 }
274
275 let chains = sample_plane_cone(cone, normal, d)?;
278 Ok(chains
279 .into_iter()
280 .map(ExactIntersectionCurve::Points)
281 .collect())
282}
283
284#[derive(Clone, Copy)]
286pub enum AnalyticSurface<'a> {
287 Cylinder(&'a CylindricalSurface),
289 Cone(&'a ConicalSurface),
291 Sphere(&'a SphericalSurface),
293 Torus(&'a ToroidalSurface),
295}
296
297fn dot_np(n: Vec3, p: Point3) -> f64 {
299 n.dot(Vec3::new(p.x(), p.y(), p.z()))
300}
301
302pub fn intersect_plane_analytic(
310 surface: AnalyticSurface<'_>,
311 normal: Vec3,
312 d: f64,
313) -> Result<Vec<IntersectionCurve>, MathError> {
314 match surface {
315 AnalyticSurface::Cylinder(cyl) => intersect_plane_cylinder(cyl, normal, d),
316 AnalyticSurface::Cone(cone) => intersect_plane_cone(cone, normal, d),
317 AnalyticSurface::Sphere(sphere) => intersect_plane_sphere(sphere, normal, d),
318 AnalyticSurface::Torus(torus) => intersect_plane_torus(torus, normal, d),
319 }
320}
321
322pub fn sample_plane_analytic(
333 surface: AnalyticSurface<'_>,
334 normal: Vec3,
335 d: f64,
336) -> Result<Vec<Vec<Point3>>, MathError> {
337 match surface {
338 AnalyticSurface::Cylinder(cyl) => sample_plane_cylinder(cyl, normal, d),
339 AnalyticSurface::Cone(cone) => sample_plane_cone(cone, normal, d),
340 AnalyticSurface::Sphere(sphere) => sample_plane_sphere(sphere, normal, d),
341 AnalyticSurface::Torus(torus) => sample_plane_torus(torus, normal, d),
342 }
343}
344
345#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
347fn sample_plane_cylinder(
348 cyl: &CylindricalSurface,
349 normal: Vec3,
350 d: f64,
351) -> Result<Vec<Vec<Point3>>, MathError> {
352 let n_samples = 64_usize;
353 let mut points = Vec::with_capacity(n_samples + 1);
354
355 for i in 0..=n_samples {
356 let u = TAU * (i as f64) / (n_samples as f64);
357 let base = cyl.evaluate(u, 0.0);
358 let n_dot_axis = normal.dot(cyl.axis());
359 let n_dot_base = dot_np(normal, base);
360
361 if n_dot_axis.abs() < 1e-12 {
362 if (n_dot_base - d).abs() < 1e-6 {
363 points.push(base);
364 }
365 } else {
366 let v = (d - n_dot_base) / n_dot_axis;
367 if v.abs() <= 100.0 {
368 points.push(cyl.evaluate(u, v));
369 }
370 }
371 }
372
373 if points.len() < 2 {
374 Ok(vec![])
375 } else {
376 Ok(vec![points])
377 }
378}
379
380#[allow(clippy::cast_precision_loss)]
382fn sample_plane_sphere(
383 sphere: &SphericalSurface,
384 normal: Vec3,
385 d: f64,
386) -> Result<Vec<Vec<Point3>>, MathError> {
387 let h = dot_np(normal, sphere.center()) - d;
388 let r = sphere.radius();
389
390 if h.abs() > r - 1e-10 {
391 return Ok(vec![]);
392 }
393
394 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
395 let circle_center = Point3::new(
396 h.mul_add(-normal.x(), sphere.center().x()),
397 h.mul_add(-normal.y(), sphere.center().y()),
398 h.mul_add(-normal.z(), sphere.center().z()),
399 );
400
401 let basis = Frame3::from_normal(circle_center, normal)?;
402 let u_dir = basis.x;
403 let v_dir = basis.y;
404
405 let n_samples = 64_usize;
406 let mut points = Vec::with_capacity(n_samples + 1);
407
408 for i in 0..=n_samples {
409 let theta = TAU * (i as f64) / (n_samples as f64);
410 let (sin_t, cos_t) = theta.sin_cos();
411 points.push(circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t));
412 }
413
414 Ok(vec![points])
415}
416
417#[allow(clippy::cast_precision_loss, clippy::unnecessary_wraps)]
429fn sample_plane_cone(
430 cone: &ConicalSurface,
431 normal: Vec3,
432 d: f64,
433) -> Result<Vec<Vec<Point3>>, MathError> {
434 let apex = cone.apex();
435 let n_dot_apex = dot_np(normal, apex);
436 let e = d - n_dot_apex;
437
438 let n_samples = 512_usize;
442 let mut vs: Vec<Option<f64>> = Vec::with_capacity(n_samples);
443 let mut v_min = f64::INFINITY;
444 for i in 0..n_samples {
445 let u = TAU * (i as f64) / (n_samples as f64);
446 let g = cone.evaluate(u, 1.0) - apex;
447 let n_dot_g = normal.dot(Vec3::new(g.x(), g.y(), g.z()));
448 if n_dot_g.abs() < 1e-12 {
449 vs.push(None);
450 continue;
451 }
452 let v = e / n_dot_g;
453 if v >= -1e-12 {
454 let v = v.max(0.0);
455 v_min = v_min.min(v);
456 vs.push(Some(v));
457 } else {
458 vs.push(None);
459 }
460 }
461
462 if !v_min.is_finite() {
463 return Ok(Vec::new());
464 }
465
466 let v_max = (8.0 * v_min).max(v_min + 4.0);
473
474 let kept: Vec<Option<f64>> = vs.iter().map(|v| v.filter(|&v| v <= v_max)).collect();
477
478 let point_at = |u: f64, v: f64| -> Point3 {
479 let g = cone.evaluate(u, 1.0) - apex;
480 apex + g * v
481 };
482 #[allow(clippy::cast_precision_loss)]
483 let u_of = |i: usize| TAU * (i as f64) / (n_samples as f64);
484 let n_dot_g_at = |u: f64| -> f64 {
485 let g = cone.evaluate(u, 1.0) - apex;
486 normal.dot(Vec3::new(g.x(), g.y(), g.z()))
487 };
488
489 if kept.iter().all(Option::is_some) {
490 let mut pts: Vec<Point3> = kept
492 .iter()
493 .enumerate()
494 .filter_map(|(i, v)| v.map(|v| point_at(u_of(i), v)))
495 .collect();
496 if let Some(&first) = pts.first() {
497 pts.push(first);
498 }
499 return Ok(vec![pts]);
500 }
501
502 let tail = |i_end: usize, forward: bool, kept: &[Option<f64>]| -> Vec<Point3> {
511 let Some(v_end) = kept[i_end] else {
512 return Vec::new();
513 };
514 let u_end = u_of(i_end);
515 #[allow(clippy::cast_precision_loss)]
516 let pitch = TAU / (n_samples as f64);
517 let u_next = if forward {
518 u_end + pitch
519 } else {
520 u_end - pitch
521 };
522 let target = e / v_max;
523 let h_end = n_dot_g_at(u_end) - target;
524 let h_next = n_dot_g_at(u_next) - target;
525 if v_end >= v_max || h_end == 0.0 || h_end.signum() == h_next.signum() {
526 return Vec::new();
527 }
528 let (mut lo, mut hi) = (u_end, u_next);
529 for _ in 0..60 {
530 let mid = f64::midpoint(lo, hi);
531 if (n_dot_g_at(mid) - target).signum() == h_end.signum() {
532 lo = mid;
533 } else {
534 hi = mid;
535 }
536 }
537 let u_star = f64::midpoint(lo, hi);
538 let tail_n = 8_usize;
539 (1..=tail_n)
540 .filter_map(|k| {
541 #[allow(clippy::cast_precision_loss)]
542 let u = u_end + (u_star - u_end) * (k as f64) / (tail_n as f64);
543 let ng = n_dot_g_at(u);
544 if ng.abs() < 1e-12 {
545 return None;
546 }
547 let v = e / ng;
548 (v >= -1e-12 && v <= v_max * (1.0 + 1e-9)).then(|| point_at(u, v.max(0.0)))
549 })
550 .collect()
551 };
552
553 let gap = kept.iter().position(Option::is_none).unwrap_or(0);
556 let mut chains: Vec<Vec<Point3>> = Vec::new();
557 let mut run: Vec<usize> = Vec::new();
558 let flush = |run: &mut Vec<usize>, chains: &mut Vec<Vec<Point3>>| {
559 if run.len() >= 2 {
560 let first = run[0];
561 let last = run[run.len() - 1];
562 let mut pts: Vec<Point3> = tail(first, false, &kept);
563 pts.reverse();
564 pts.extend(
565 run.iter()
566 .filter_map(|&i| kept[i].map(|v| point_at(u_of(i), v))),
567 );
568 pts.extend(tail(last, true, &kept));
569 chains.push(pts);
570 }
571 run.clear();
572 };
573 for k in 0..n_samples {
574 let idx = (gap + k) % n_samples;
575 if kept[idx].is_some() {
576 run.push(idx);
577 } else {
578 flush(&mut run, &mut chains);
579 }
580 }
581 flush(&mut run, &mut chains);
582 Ok(chains.into_iter().filter(|c| c.len() >= 2).collect())
583}
584
585#[allow(clippy::unnecessary_wraps)] fn sample_plane_torus(
591 torus: &ToroidalSurface,
592 normal: Vec3,
593 d: f64,
594) -> Result<Vec<Vec<Point3>>, MathError> {
595 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
596 Ok(chain_torus_crossings(&crossing_pts)
597 .into_iter()
598 .map(|run| run.into_iter().map(|p| p.point).collect())
599 .collect())
600}
601
602#[allow(clippy::cast_precision_loss)]
612pub fn intersect_plane_cylinder(
613 cyl: &CylindricalSurface,
614 normal: Vec3,
615 d: f64,
616) -> Result<Vec<IntersectionCurve>, MathError> {
617 let n_samples = 64_usize;
618 let mut points_3d = Vec::new();
619 let mut ipoints = Vec::new();
620
621 for i in 0..=n_samples {
622 let u = TAU * (i as f64) / (n_samples as f64);
623 let base = cyl.evaluate(u, 0.0);
626 let n_dot_axis = normal.dot(cyl.axis());
627 let n_dot_base = dot_np(normal, base);
628
629 if n_dot_axis.abs() < 1e-12 {
630 if (n_dot_base - d).abs() < 1e-6 {
632 let pt = base;
633 points_3d.push(pt);
634 ipoints.push(IntersectionPoint {
635 point: pt,
636 param1: (u, 0.0),
637 param2: (0.0, 0.0),
638 });
639 }
640 } else {
641 let v = (d - n_dot_base) / n_dot_axis;
642 if v.abs() <= 100.0 {
644 let pt = cyl.evaluate(u, v);
645 points_3d.push(pt);
646 ipoints.push(IntersectionPoint {
647 point: pt,
648 param1: (u, v),
649 param2: (0.0, 0.0),
650 });
651 }
652 }
653 }
654
655 build_curves_from_points(&points_3d, ipoints)
656}
657
658#[allow(clippy::cast_precision_loss)]
667pub fn intersect_plane_sphere(
668 sphere: &SphericalSurface,
669 normal: Vec3,
670 d: f64,
671) -> Result<Vec<IntersectionCurve>, MathError> {
672 let h = dot_np(normal, sphere.center()) - d;
673 let r = sphere.radius();
674
675 if h.abs() > r - 1e-10 {
677 return Ok(vec![]);
678 }
679
680 let circle_r = (r.mul_add(r, -(h * h))).sqrt();
681 let circle_center = Point3::new(
682 h.mul_add(-normal.x(), sphere.center().x()),
683 h.mul_add(-normal.y(), sphere.center().y()),
684 h.mul_add(-normal.z(), sphere.center().z()),
685 );
686
687 let basis = Frame3::from_normal(circle_center, normal)?;
689 let u_dir = basis.x;
690 let v_dir = basis.y;
691
692 let n_samples = 64_usize;
693 let mut points_3d = Vec::new();
694 let mut ipoints = Vec::new();
695
696 for i in 0..=n_samples {
697 let theta = TAU * (i as f64) / (n_samples as f64);
698 let (sin_t, cos_t) = theta.sin_cos();
699 let pt = circle_center + u_dir * (circle_r * cos_t) + v_dir * (circle_r * sin_t);
700 points_3d.push(pt);
701 ipoints.push(IntersectionPoint {
702 point: pt,
703 param1: (theta, 0.0),
704 param2: (0.0, 0.0),
705 });
706 }
707
708 build_curves_from_points(&points_3d, ipoints)
709}
710
711#[allow(clippy::cast_precision_loss)]
720pub fn intersect_plane_cone(
721 cone: &ConicalSurface,
722 normal: Vec3,
723 d: f64,
724) -> Result<Vec<IntersectionCurve>, MathError> {
725 let n_samples = 64_usize;
726 let mut points_3d = Vec::new();
727 let mut ipoints = Vec::new();
728
729 for i in 0..n_samples {
730 let u = TAU * (i as f64) / (n_samples as f64);
731 let apex = cone.apex();
734 let n_dot_apex = dot_np(normal, apex);
735 let p1 = cone.evaluate(u, 1.0);
737 let dir = p1 - apex;
738 let n_dot_dir = normal.dot(dir);
739
740 if n_dot_dir.abs() < 1e-12 {
741 continue;
742 }
743
744 let v = (d - n_dot_apex) / n_dot_dir;
745 if v.abs() > 1e-10 && v.abs() < 100.0 {
747 let pt = cone.evaluate(u, v);
748 points_3d.push(pt);
749 ipoints.push(IntersectionPoint {
750 point: pt,
751 param1: (u, v),
752 param2: (0.0, 0.0),
753 });
754 }
755 }
756
757 build_curves_from_points(&points_3d, ipoints)
758}
759
760#[allow(clippy::unnecessary_wraps)]
772pub fn intersect_plane_torus(
773 torus: &ToroidalSurface,
774 normal: Vec3,
775 d: f64,
776) -> Result<Vec<IntersectionCurve>, MathError> {
777 let crossing_pts = plane_torus_crossings(torus, normal, d, 128);
781
782 let mut curves = Vec::new();
783 for ipts in chain_torus_crossings(&crossing_pts) {
784 let pts: Vec<Point3> = ipts.iter().map(|p| p.point).collect();
785 if let Ok(curve) = interpolate(&pts, 3.min(pts.len() - 1)) {
786 curves.push(IntersectionCurve {
787 curve,
788 points: ipts,
789 });
790 }
791 }
792
793 Ok(curves)
794}
795
796fn chain_torus_crossings(crossing_pts: &[(f64, f64, Point3)]) -> Vec<Vec<IntersectionPoint>> {
808 let mut used = vec![false; crossing_pts.len()];
809 let mut runs = Vec::new();
810
811 for start in 0..crossing_pts.len() {
812 if used[start] {
813 continue;
814 }
815 used[start] = true;
816 let mut chain = vec![start];
817
818 loop {
819 let last = chain[chain.len() - 1];
820 let last_pt = crossing_pts[last].2;
821 let mut best_idx = None;
822 let mut best_dist = 1.0_f64;
823
824 for (j, &is_used) in used.iter().enumerate() {
825 if is_used {
826 continue;
827 }
828 let dist = (crossing_pts[j].2 - last_pt).length();
829 if dist < best_dist {
830 best_dist = dist;
831 best_idx = Some(j);
832 }
833 }
834
835 if let Some(j) = best_idx {
836 used[j] = true;
837 chain.push(j);
838 } else {
839 break;
840 }
841 }
842
843 if chain.len() < 4 {
844 continue;
845 }
846 let mut ipts: Vec<IntersectionPoint> = chain
847 .iter()
848 .map(|&i| IntersectionPoint {
849 point: crossing_pts[i].2,
850 param1: (crossing_pts[i].0, crossing_pts[i].1),
851 param2: (0.0, 0.0),
852 })
853 .collect();
854
855 let closing_gap = (ipts[ipts.len() - 1].point - ipts[0].point).length();
856 let median_spacing = {
857 let mut spac: Vec<f64> = ipts
858 .windows(2)
859 .map(|w| (w[1].point - w[0].point).length())
860 .collect();
861 spac.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
862 spac.get(spac.len() / 2).copied().unwrap_or(0.0)
863 };
864 if closing_gap > 1e-9
871 && median_spacing > 1e-12
872 && closing_gap <= 2.0 * median_spacing
873 && !chain_self_touches(&ipts, median_spacing)
874 {
875 ipts.push(ipts[0]);
876 }
877 runs.push(ipts);
878 }
879
880 runs
881}
882
883fn chain_self_touches(ipts: &[IntersectionPoint], median_spacing: f64) -> bool {
893 let m = ipts.len();
894 let k = (m / 4).clamp(1, 6);
895 if m < 3 * k || median_spacing <= 0.0 {
896 return false;
897 }
898 let thresh = median_spacing * 1.5;
899 for i in k..(m - k) {
900 for j in (i + k)..(m - k) {
901 if (ipts[i].point - ipts[j].point).length() < thresh {
902 return true;
903 }
904 }
905 }
906 false
907}
908
909#[allow(clippy::cast_precision_loss)]
926fn plane_torus_crossings(
927 torus: &ToroidalSurface,
928 normal: Vec3,
929 d: f64,
930 n_v: usize,
931) -> Vec<(f64, f64, Point3)> {
932 let big_r = torus.major_radius();
933 let small_r = torus.minor_radius();
934 let a = normal.dot(torus.x_axis());
935 let b = normal.dot(torus.y_axis());
936 let c = normal.dot(torus.z_axis());
937 let s = a.hypot(b);
938 let phi = b.atan2(a);
939 let d_local = d - dot_np(normal, torus.center());
940
941 let mut pts: Vec<(f64, f64, Point3)> = Vec::new();
942
943 if s < 1e-12 {
945 if c.abs() < 1e-12 {
946 return pts;
947 }
948 let sin_v = d_local / (small_r * c);
949 if sin_v.abs() > 1.0 + 1e-9 {
950 return pts;
951 }
952 let v0 = sin_v.clamp(-1.0, 1.0).asin();
953 let v1 = std::f64::consts::PI - v0;
954 let mut vs = vec![v0];
955 if (v1 - v0).abs() > 1e-9 {
957 vs.push(v1);
958 }
959 for v in vs {
960 for i in 0..n_v {
961 let u = TAU * (i as f64) / (n_v as f64);
962 pts.push((u, v, torus.evaluate(u, v)));
963 }
964 }
965 return pts;
966 }
967
968 let v_off = TAU / (n_v as f64) * 0.5;
974 for i in 0..n_v {
975 let v = (i as f64).mul_add(TAU / (n_v as f64), v_off);
976 let tube_r = small_r.mul_add(v.cos(), big_r); let rhs = (d_local - small_r * c * v.sin()) / (s * tube_r);
978 if rhs.abs() > 1.0 {
979 continue;
980 }
981 let delta = rhs.clamp(-1.0, 1.0).acos();
982 for u in [phi + delta, phi - delta] {
983 pts.push((u, v, torus.evaluate(u, v)));
984 }
985 }
986 pts
987}
988
989#[must_use]
1002pub fn intersect_line_torus(torus: &ToroidalSurface, origin: Point3, dir: Vec3) -> Vec<f64> {
1003 let c = torus.center();
1004 let (xa, ya, za) = (torus.x_axis(), torus.y_axis(), torus.z_axis());
1005 let big_r = torus.major_radius();
1006 let small_r = torus.minor_radius();
1007
1008 let o = Vec3::new(origin.x() - c.x(), origin.y() - c.y(), origin.z() - c.z());
1010 let (a0, a1) = (xa.dot(o), xa.dot(dir));
1011 let (b0, b1) = (ya.dot(o), ya.dot(dir));
1012 let (c0, c1) = (za.dot(o), za.dot(dir));
1013
1014 let g2 = a1.mul_add(a1, b1.mul_add(b1, c1 * c1));
1016 let g1 = 2.0 * a1.mul_add(a0, b1.mul_add(b0, c1 * c0));
1017 let g0 = a0.mul_add(
1018 a0,
1019 b0.mul_add(b0, c0.mul_add(c0, big_r.mul_add(big_r, -small_r * small_r))),
1020 );
1021
1022 let four_rr = 4.0 * big_r * big_r;
1024 let h2 = four_rr * a1.mul_add(a1, b1 * b1);
1025 let h1 = four_rr * (2.0 * a1.mul_add(a0, b1 * b0));
1026 let h0 = four_rr * a0.mul_add(a0, b0 * b0);
1027
1028 let e4 = g2 * g2;
1030 let e3 = 2.0 * g2 * g1;
1031 let e2 = g1.mul_add(g1, 2.0 * g2 * g0) - h2;
1032 let e1 = 2.0f64.mul_add(g1 * g0, -h1);
1033 let e0 = g0.mul_add(g0, -h0);
1034
1035 let mut roots = real_roots_quartic(e4, e3, e2, e1, e0);
1036 let impl_f = |t: f64| -> f64 {
1038 let p = origin + dir * t;
1039 let q = Vec3::new(p.x() - c.x(), p.y() - c.y(), p.z() - c.z());
1040 let (a, b, cc) = (xa.dot(q), ya.dot(q), za.dot(q));
1041 (a.hypot(b) - big_r).hypot(cc) - small_r
1042 };
1043 for t in &mut roots {
1044 let eps = 1e-7;
1045 let f = impl_f(*t);
1046 let df = (impl_f(*t + eps) - impl_f(*t - eps)) / (2.0 * eps);
1047 if df.abs() > 1e-12 {
1048 *t -= f / df;
1049 }
1050 }
1051 roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1052 roots
1053}
1054
1055fn real_roots_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> Vec<f64> {
1058 if c4.abs() < 1e-14 {
1060 return real_roots_cubic(c3, c2, c1, c0);
1061 }
1062 let (a, b, c, d) = (c3 / c4, c2 / c4, c1 / c4, c0 / c4);
1064 let eval = |z: Complex| -> Complex {
1065 let mut acc = Complex::new(1.0, 0.0);
1067 acc = acc * z + Complex::new(a, 0.0);
1068 acc = acc * z + Complex::new(b, 0.0);
1069 acc = acc * z + Complex::new(c, 0.0);
1070 acc * z + Complex::new(d, 0.0)
1071 };
1072 let seed = Complex::new(0.4, 0.9);
1074 let mut r = [
1075 Complex::new(1.0, 0.0),
1076 seed,
1077 seed * seed,
1078 seed * seed * seed,
1079 ];
1080 for _ in 0..100 {
1081 let mut max_step = 0.0_f64;
1082 for i in 0..4 {
1083 let mut denom = Complex::new(1.0, 0.0);
1084 for j in 0..4 {
1085 if i != j {
1086 denom = denom * (r[i] - r[j]);
1087 }
1088 }
1089 if denom.norm() < 1e-300 {
1090 continue;
1091 }
1092 let step = eval(r[i]) / denom;
1093 r[i] = r[i] - step;
1094 max_step = max_step.max(step.norm());
1095 }
1096 if max_step < 1e-14 {
1097 break;
1098 }
1099 }
1100 let p_real = |x: f64| -> f64 { (((x + a) * x + b) * x + c) * x + d };
1107 let mut out: Vec<f64> = Vec::new();
1108 for z in r {
1109 if z.im.abs() >= 1e-7 {
1110 continue;
1111 }
1112 let x = z.re;
1113 let scale = 1.0 + a.abs() + b.abs() + c.abs() + d.abs() + x.abs().powi(4);
1116 if p_real(x).abs() > 1e-6 * scale {
1117 continue;
1118 }
1119 if out.iter().any(|&y| (y - x).abs() < 1e-9 * (1.0 + x.abs())) {
1120 continue;
1121 }
1122 out.push(x);
1123 }
1124 out
1125}
1126
1127fn real_roots_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
1129 if a.abs() < 1e-14 {
1130 return real_roots_quadratic(b, c, d);
1131 }
1132 let (b, c, d) = (b / a, c / a, d / a);
1134 let p = c - b * b / 3.0;
1135 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
1136 let shift = -b / 3.0;
1137 let disc = q * q / 4.0 + p * p * p / 27.0;
1138 if disc > 1e-14 {
1139 let sq = disc.sqrt();
1140 let u = (-q / 2.0 + sq).cbrt();
1141 let v = (-q / 2.0 - sq).cbrt();
1142 vec![u + v + shift]
1143 } else if disc < -1e-14 {
1144 let m = 2.0 * (-p / 3.0).sqrt();
1146 let theta = (3.0 * q / (p * m)).clamp(-1.0, 1.0).acos() / 3.0;
1147 (0..3)
1148 .map(|k| {
1149 m.mul_add(
1150 (theta - 2.0 * std::f64::consts::PI * f64::from(k) / 3.0).cos(),
1151 shift,
1152 )
1153 })
1154 .collect()
1155 } else {
1156 let u = (-q / 2.0).cbrt();
1158 vec![2.0 * u + shift, -u + shift]
1159 }
1160}
1161
1162fn real_roots_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
1164 if a.abs() < 1e-14 {
1165 if b.abs() < 1e-14 {
1166 return Vec::new();
1167 }
1168 return vec![-c / b];
1169 }
1170 let disc = b * b - 4.0 * a * c;
1171 if disc < 0.0 {
1172 Vec::new()
1173 } else {
1174 let sq = disc.sqrt();
1175 vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
1176 }
1177}
1178
1179#[derive(Clone, Copy)]
1181struct Complex {
1182 re: f64,
1183 im: f64,
1184}
1185
1186impl Complex {
1187 const fn new(re: f64, im: f64) -> Self {
1188 Self { re, im }
1189 }
1190 fn norm(self) -> f64 {
1191 self.re.hypot(self.im)
1192 }
1193}
1194
1195impl std::ops::Add for Complex {
1196 type Output = Self;
1197 fn add(self, o: Self) -> Self {
1198 Self::new(self.re + o.re, self.im + o.im)
1199 }
1200}
1201
1202impl std::ops::Sub for Complex {
1203 type Output = Self;
1204 fn sub(self, o: Self) -> Self {
1205 Self::new(self.re - o.re, self.im - o.im)
1206 }
1207}
1208
1209impl std::ops::Mul for Complex {
1210 type Output = Self;
1211 fn mul(self, o: Self) -> Self {
1212 Self::new(
1213 self.re.mul_add(o.re, -(self.im * o.im)),
1214 self.re.mul_add(o.im, self.im * o.re),
1215 )
1216 }
1217}
1218
1219impl std::ops::Div for Complex {
1220 type Output = Self;
1221 fn div(self, o: Self) -> Self {
1222 let den = o.re.mul_add(o.re, o.im * o.im);
1223 Self::new(
1224 self.re.mul_add(o.re, self.im * o.im) / den,
1225 self.im.mul_add(o.re, -(self.re * o.im)) / den,
1226 )
1227 }
1228}
1229
1230fn build_curves_from_points(
1234 points_3d: &[Point3],
1235 ipoints: Vec<IntersectionPoint>,
1236) -> Result<Vec<IntersectionCurve>, MathError> {
1237 if points_3d.len() < 2 {
1238 return Ok(vec![]);
1239 }
1240
1241 let degree = 3.min(points_3d.len() - 1);
1242 let curve = interpolate(points_3d, degree)?;
1243 Ok(vec![IntersectionCurve {
1244 curve,
1245 points: ipoints,
1246 }])
1247}
1248
1249#[allow(
1261 clippy::cast_precision_loss,
1262 clippy::too_many_lines,
1263 clippy::similar_names,
1264 clippy::unnecessary_wraps,
1265 clippy::type_complexity
1266)]
1267pub fn intersect_analytic_analytic(
1268 a: AnalyticSurface<'_>,
1269 b: AnalyticSurface<'_>,
1270 grid_res: usize,
1271) -> Result<Vec<IntersectionCurve>, MathError> {
1272 intersect_analytic_analytic_bounded(a, b, grid_res, None, None)
1273}
1274
1275pub fn intersect_analytic_analytic_bounded(
1286 a: AnalyticSurface<'_>,
1287 b: AnalyticSurface<'_>,
1288 grid_res: usize,
1289 v_range_hint_a: Option<(f64, f64)>,
1290 v_range_hint_b: Option<(f64, f64)>,
1291) -> Result<Vec<IntersectionCurve>, MathError> {
1292 if let Some(result) = try_algebraic_intersection(&a, &b, v_range_hint_a, v_range_hint_b)? {
1295 return Ok(result);
1296 }
1297
1298 let (surf_a, norm_a, u_range_a, default_v_a) = surface_closures(&a);
1299 let (surf_b, norm_b, u_range_b, default_v_b) = surface_closures(&b);
1300 let v_range_a = v_range_hint_a.unwrap_or(default_v_a);
1301 let v_range_b = v_range_hint_b.unwrap_or(default_v_b);
1302
1303 let diag_a = {
1305 let p00 = surf_a(u_range_a.0, v_range_a.0);
1306 let p11 = surf_a(u_range_a.1, v_range_a.1);
1307 (p00 - p11).length()
1308 };
1309 let diag_b = {
1310 let p00 = surf_b(u_range_b.0, v_range_b.0);
1311 let p11 = surf_b(u_range_b.1, v_range_b.1);
1312 (p00 - p11).length()
1313 };
1314 let char_size = diag_a.min(diag_b).max(0.1);
1315
1316 #[allow(clippy::type_complexity)]
1320 let mut seeds: Vec<(Point3, (f64, f64), (f64, f64))> = Vec::new();
1321 let seed_threshold = diag_a.max(diag_b).max(1.0) * 0.5;
1325 let mut min_dist = f64::INFINITY;
1326
1327 #[allow(clippy::cast_precision_loss)]
1328 for ia in 0..grid_res {
1329 for ja in 0..grid_res {
1330 let ua =
1331 u_range_a.0 + (u_range_a.1 - u_range_a.0) * (ia as f64 + 0.5) / (grid_res as f64);
1332 let va =
1333 v_range_a.0 + (v_range_a.1 - v_range_a.0) * (ja as f64 + 0.5) / (grid_res as f64);
1334
1335 let pa = surf_a(ua, va);
1336
1337 let (ub, vb) = project_analytic(&b, pa, u_range_b, v_range_b);
1339 let pb = surf_b(ub, vb);
1340 let dist = (pa - pb).length();
1341 min_dist = min_dist.min(dist);
1342
1343 if dist < seed_threshold {
1344 let mid = Point3::new(
1349 (pa.x() + pb.x()) * 0.5,
1350 (pa.y() + pb.y()) * 0.5,
1351 (pa.z() + pb.z()) * 0.5,
1352 );
1353 seeds.push((mid, (ua, va), (ub, vb)));
1354 }
1355 }
1356 }
1357
1358 let reject_dist = (char_size / grid_res as f64) * 3.0;
1367 if min_dist > reject_dist {
1368 return Ok(vec![]);
1369 }
1370
1371 if seeds.is_empty() {
1372 return Ok(vec![]);
1373 }
1374
1375 let march_step = (char_size * 0.02).clamp(0.005, 0.5);
1379 let dedup_radius = march_step * 10.0;
1380 let mut unique_seeds = Vec::new();
1381 for seed in &seeds {
1382 let dominated = unique_seeds
1383 .iter()
1384 .any(|s: &(Point3, (f64, f64), (f64, f64))| (s.0 - seed.0).length() < dedup_radius);
1385 if !dominated {
1386 unique_seeds.push(*seed);
1387 }
1388 }
1389
1390 let mut curves = Vec::new();
1392 let mut used_seeds = vec![false; unique_seeds.len()];
1393
1394 for si in 0..unique_seeds.len() {
1395 if used_seeds[si] {
1396 continue;
1397 }
1398 used_seeds[si] = true;
1399
1400 let march_result = march_analytic_intersection(
1401 &a,
1402 &b,
1403 surf_a.as_ref(),
1404 norm_a.as_ref(),
1405 surf_b.as_ref(),
1406 norm_b.as_ref(),
1407 unique_seeds[si].0,
1408 u_range_a,
1409 v_range_a,
1410 u_range_b,
1411 v_range_b,
1412 march_step,
1413 is_u_periodic(&a),
1414 is_u_periodic(&b),
1415 );
1416
1417 if march_result.len() >= 2 {
1418 for (sj, other) in unique_seeds.iter().enumerate() {
1419 if !used_seeds[sj]
1420 && march_result
1421 .iter()
1422 .any(|p| (*p - other.0).length() < dedup_radius)
1423 {
1424 used_seeds[sj] = true;
1425 }
1426 }
1427
1428 let ipts: Vec<IntersectionPoint> = march_result
1429 .iter()
1430 .map(|&pt| IntersectionPoint {
1431 point: pt,
1432 param1: (0.0, 0.0),
1433 param2: (0.0, 0.0),
1434 })
1435 .collect();
1436
1437 let degree = 3.min(march_result.len() - 1);
1438 if let Ok(curve) = interpolate(&march_result, degree) {
1439 curves.push(IntersectionCurve {
1440 curve,
1441 points: ipts,
1442 });
1443 }
1444 }
1445 }
1446
1447 Ok(curves)
1448}
1449
1450#[allow(clippy::too_many_lines)]
1460fn try_algebraic_intersection(
1461 a: &AnalyticSurface<'_>,
1462 b: &AnalyticSurface<'_>,
1463 v_range_a: Option<(f64, f64)>,
1464 v_range_b: Option<(f64, f64)>,
1465) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1466 match (a, b) {
1467 (AnalyticSurface::Cone(cone), AnalyticSurface::Cylinder(cyl)) => {
1468 algebraic_parallel_cone_cylinder(cone, cyl, v_range_a, v_range_b)
1469 }
1470 (AnalyticSurface::Cylinder(cyl), AnalyticSurface::Cone(cone)) => {
1471 algebraic_parallel_cone_cylinder(cone, cyl, v_range_b, v_range_a)
1472 }
1473 (AnalyticSurface::Sphere(s1), AnalyticSurface::Sphere(s2)) => {
1474 algebraic_sphere_sphere(s1, s2).map(Some)
1475 }
1476 (AnalyticSurface::Cylinder(c1), AnalyticSurface::Cylinder(c2)) => {
1477 let axis_dot = c1.axis().dot(c2.axis()).abs();
1478 if axis_dot > 1.0 - 1e-10 {
1479 let delta = c2.origin() - c1.origin();
1481 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1482 let along = delta_vec.dot(c1.axis());
1483 let perp = (delta_vec - c1.axis() * along).length();
1484 if perp < 1e-8 {
1485 if (c1.radius() - c2.radius()).abs() < 1e-8 {
1488 return Ok(None); }
1490 return Ok(Some(vec![])); }
1492 }
1493 algebraic_cylinder_cylinder(c1, c2)
1495 }
1496 (AnalyticSurface::Sphere(s), AnalyticSurface::Cylinder(c)) => {
1498 algebraic_sphere_cylinder(s, c, true)
1499 }
1500 (AnalyticSurface::Cylinder(c), AnalyticSurface::Sphere(s)) => {
1501 algebraic_sphere_cylinder(s, c, false)
1502 }
1503 (AnalyticSurface::Cone(c1), AnalyticSurface::Cone(c2)) => algebraic_cone_cone(c1, c2),
1504 (AnalyticSurface::Torus(t), AnalyticSurface::Cylinder(c)) => {
1505 Ok(parallel_axis_torus_cylinder(t, c, true))
1506 }
1507 (AnalyticSurface::Cylinder(c), AnalyticSurface::Torus(t)) => {
1508 Ok(parallel_axis_torus_cylinder(t, c, false))
1509 }
1510 _ => Ok(None),
1511 }
1512}
1513
1514fn parallel_axis_torus_cylinder(
1521 torus: &ToroidalSurface,
1522 cyl: &CylindricalSurface,
1523 torus_first: bool,
1524) -> Option<Vec<IntersectionCurve>> {
1525 let axis = torus.z_axis();
1526 let along = cyl.axis().dot(axis);
1527 if along.abs() < 1.0 - 1e-10 {
1528 return None;
1529 }
1530 let offset = cyl.origin() - torus.center();
1531 if (offset - axis * offset.dot(axis)).length() < Tolerance::new().linear {
1532 return None;
1533 }
1534 let (major, minor) = (torus.major_radius(), torus.minor_radius());
1535 let roots = |u: f64| {
1536 let q = cyl.evaluate(u, 0.0) - torus.center();
1537 let height = q.dot(axis);
1538 let rho = (q - axis * height).length();
1539 let reach = minor * minor - (rho - major) * (rho - major);
1540 ruling_quadratic(1.0, 2.0 * along.signum() * height, height * height - reach)
1541 };
1542 let samples = ruling_samples(cyl, &roots);
1543 let loops = if samples.iter().all(Option::is_some) {
1544 closed_ruling_loops(&samples)
1545 } else {
1546 partial_ruling_loops(cyl, &roots, &samples)
1547 };
1548 if loops.is_empty() {
1549 return None;
1550 }
1551 Some(fit_ruling_loops(&loops, |p| {
1552 in_order(torus.project_point(p), cyl.project_point(p), torus_first)
1553 }))
1554}
1555
1556pub fn exact_cone_cone(
1581 c1: &ConicalSurface,
1582 c2: &ConicalSurface,
1583) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1584 let axis = c1.axis();
1585 let axis2 = c2.axis();
1586
1587 if axis.dot(axis2).abs() < 1.0 - 1e-10 {
1589 return Ok(None); }
1591 let apex1 = c1.apex();
1592 let apex2 = c2.apex();
1593 let delta = apex2 - apex1;
1594 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1595 let along = delta_v.dot(axis);
1596 if (delta_v - axis * along).length() > 1e-8 {
1597 return offset_parallel_cone_cone(c1, c2);
1598 }
1599
1600 let (s1, s2) = (c1.half_angle().sin(), c2.half_angle().sin());
1601 if s1.abs() < 1e-12 || s2.abs() < 1e-12 {
1602 return Ok(None); }
1604 let m1 = c1.half_angle().cos() / s1;
1605 let m2 = c2.half_angle().cos() / s2;
1606 let sigma = if axis.dot(axis2) >= 0.0 { 1.0 } else { -1.0 };
1607 let d2 = along; let denom = m1 - m2 * sigma;
1610 if denom.abs() < 1e-12 {
1611 if sigma > 0.0 && d2.abs() < 1e-9 {
1614 return Ok(None);
1615 }
1616 return Ok(Some(vec![]));
1617 }
1618
1619 let t_star = (-m2 * sigma * d2) / denom;
1620 let radius = m1 * t_star;
1621 if radius < 1e-12 {
1622 return Ok(Some(vec![])); }
1624
1625 let center = Point3::new(
1626 apex1.x() + axis.x() * t_star,
1627 apex1.y() + axis.y() * t_star,
1628 apex1.z() + axis.z() * t_star,
1629 );
1630 let circle = Circle3D::new(center, axis, radius)?;
1631 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
1632}
1633
1634fn offset_parallel_cone_cone(
1645 c1: &ConicalSurface,
1646 c2: &ConicalSurface,
1647) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1648 if c1.half_angle().sin().abs() < 1e-12 || c2.half_angle().sin().abs() < 1e-12 {
1649 return Ok(None); }
1651 let t1 = c1.half_angle().tan();
1652 let t2 = c2.half_angle().tan();
1653 if !t1.is_finite() || !t2.is_finite() {
1654 return Ok(None);
1655 }
1656 if (t1 - t2).abs() > 1e-9 * (1.0 + t1.abs().max(t2.abs())) {
1657 return Ok(None);
1658 }
1659
1660 let w = c1.axis();
1661 let apex1 = c1.apex();
1662 let apex2 = c2.apex();
1663 let delta = apex2 - apex1;
1664 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1665 let s = delta_v.dot(w);
1666 let tm = 0.5 * (t1 + t2);
1667 let k = 1.0 + tm * tm;
1668
1669 let n = (delta_v - w * (k * s)) * 2.0;
1673 let n_len = n.length();
1674 if n_len < 1e-12 {
1675 return Ok(None);
1676 }
1677 let n_hat = n * (1.0 / n_len);
1678 let d = (dot_np(n, apex1) + delta_v.dot(delta_v) - k * s * s) / n_len;
1679
1680 let axis2 = c2.axis();
1686 let scale = 1.0 + delta_v.length();
1687 let mut out = Vec::new();
1688 for curve in exact_plane_cone(c1, n_hat, d)? {
1689 let samples: Vec<Point3> = match &curve {
1690 ExactIntersectionCurve::Circle(c) => (0..4)
1691 .map(|i| crate::traits::ParametricCurve::evaluate(c, TAU * f64::from(i) / 4.0))
1692 .collect(),
1693 ExactIntersectionCurve::Ellipse(e) => (0..4)
1694 .map(|i| crate::traits::ParametricCurve::evaluate(e, TAU * f64::from(i) / 4.0))
1695 .collect(),
1696 ExactIntersectionCurve::Points(_) => return Ok(None),
1697 };
1698 let on_real_nappe = |p: &Point3| {
1699 let rel = *p - apex2;
1700 Vec3::new(rel.x(), rel.y(), rel.z()).dot(axis2) >= -1e-9 * scale
1701 };
1702 let hits = samples.iter().filter(|p| on_real_nappe(p)).count();
1703 match hits {
1704 0 => {}
1705 4 => out.push(curve),
1706 _ => return Ok(None),
1707 }
1708 }
1709 Ok(Some(out))
1710}
1711
1712pub fn exact_cone_cylinder(
1732 cone: &ConicalSurface,
1733 cyl: &CylindricalSurface,
1734) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1735 let axis = cone.axis();
1736 let cyl_axis = cyl.axis();
1737
1738 if axis.dot(cyl_axis).abs() < 1.0 - 1e-10 {
1740 return Ok(None);
1741 }
1742 let apex = cone.apex();
1743 let delta = apex - cyl.origin();
1744 let delta_v = Vec3::new(delta.x(), delta.y(), delta.z());
1745 let along = delta_v.dot(cyl_axis);
1746 if (delta_v - cyl_axis * along).length() > 1e-8 {
1747 return Ok(None);
1748 }
1749
1750 let s = cone.half_angle().sin();
1751 if s.abs() < 1e-12 {
1752 return Ok(None); }
1754 let m = cone.half_angle().cos() / s; if m.abs() < 1e-12 {
1756 return Ok(None); }
1758
1759 let t_star = cyl.radius() / m; if t_star.abs() < 1e-12 {
1761 return Ok(Some(vec![])); }
1763 let center = Point3::new(
1764 apex.x() + axis.x() * t_star,
1765 apex.y() + axis.y() * t_star,
1766 apex.z() + axis.z() * t_star,
1767 );
1768 let circle = Circle3D::new(center, axis, cyl.radius())?;
1769 Ok(Some(vec![ExactIntersectionCurve::Circle(circle)]))
1770}
1771
1772fn algebraic_cone_cone(
1781 c1: &ConicalSurface,
1782 c2: &ConicalSurface,
1783) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1784 let Some(exacts) = exact_cone_cone(c1, c2)? else {
1785 return Ok(None);
1786 };
1787 let mut curves = Vec::new();
1788 for exact in exacts {
1789 let n_samples = 33;
1790 let mut positions = Vec::with_capacity(n_samples);
1791 let mut points = Vec::with_capacity(n_samples);
1792 #[allow(clippy::cast_precision_loss)]
1793 for i in 0..n_samples {
1794 let theta = TAU * i as f64 / (n_samples - 1) as f64;
1795 let pt = match &exact {
1796 ExactIntersectionCurve::Circle(circle) => {
1797 crate::traits::ParametricCurve::evaluate(circle, theta)
1798 }
1799 ExactIntersectionCurve::Ellipse(ellipse) => {
1800 crate::traits::ParametricCurve::evaluate(ellipse, theta)
1801 }
1802 ExactIntersectionCurve::Points(_) => break,
1803 };
1804 positions.push(pt);
1805 points.push(IntersectionPoint {
1806 point: pt,
1807 param1: (0.0, 0.0),
1808 param2: (0.0, 0.0),
1809 });
1810 }
1811 if positions.is_empty() {
1812 continue;
1813 }
1814 let degree = 3.min(positions.len() - 1);
1815 let curve = interpolate(&positions, degree)?;
1816 curves.push(IntersectionCurve { curve, points });
1817 }
1818 Ok(Some(curves))
1819}
1820
1821pub fn exact_sphere_cylinder(
1841 sphere: &SphericalSurface,
1842 cyl: &CylindricalSurface,
1843) -> Result<Option<Vec<ExactIntersectionCurve>>, MathError> {
1844 let sc = sphere.center();
1845 let r_sphere = sphere.radius();
1846 let co = cyl.origin();
1847 let axis = cyl.axis();
1848 let r_cyl = cyl.radius();
1849
1850 let delta = sc - co;
1852 let delta_vec = Vec3::new(delta.x(), delta.y(), delta.z());
1853 let along = delta_vec.dot(axis);
1854 let perp_vec = delta_vec - axis * along;
1855 let d_perp = perp_vec.length();
1856
1857 if d_perp > 1e-7 {
1860 return Ok(None);
1861 }
1862
1863 if r_cyl > r_sphere + 1e-10 {
1866 return Ok(Some(vec![]));
1867 }
1868 let z_sq = r_sphere * r_sphere - r_cyl * r_cyl;
1869 if z_sq < 0.0 {
1870 return Ok(Some(vec![]));
1871 }
1872 let z = z_sq.sqrt();
1873
1874 let center_axis_pt = Point3::new(
1877 co.x() + axis.x() * along,
1878 co.y() + axis.y() * along,
1879 co.z() + axis.z() * along,
1880 );
1881
1882 let mut circles = Vec::new();
1883 let offsets: &[f64] = if z < 1e-10 { &[0.0] } else { &[z, -z] };
1884 for &z_offset in offsets {
1885 let center = Point3::new(
1886 center_axis_pt.x() + axis.x() * z_offset,
1887 center_axis_pt.y() + axis.y() * z_offset,
1888 center_axis_pt.z() + axis.z() * z_offset,
1889 );
1890 let circle = Circle3D::new(center, axis, r_cyl)?;
1891 circles.push(ExactIntersectionCurve::Circle(circle));
1892 }
1893 Ok(Some(circles))
1894}
1895
1896fn algebraic_sphere_cylinder(
1905 sphere: &SphericalSurface,
1906 cyl: &CylindricalSurface,
1907 sphere_first: bool,
1908) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
1909 let Some(exacts) = exact_sphere_cylinder(sphere, cyl)? else {
1910 return Ok(off_axis_sphere_cylinder(sphere, cyl, sphere_first));
1911 };
1912
1913 let mut curves = Vec::new();
1914 for exact in exacts {
1915 let ExactIntersectionCurve::Circle(circle) = exact else {
1916 continue;
1917 };
1918 let n_samples = 33;
1919 let mut points = Vec::with_capacity(n_samples);
1920 let mut positions = Vec::with_capacity(n_samples);
1921 #[allow(clippy::cast_precision_loss)]
1922 for i in 0..n_samples {
1923 let theta = TAU * i as f64 / (n_samples - 1) as f64;
1924 let pt = crate::traits::ParametricCurve::evaluate(&circle, theta);
1925 positions.push(pt);
1926 let (param1, param2) = in_order(
1927 sphere.project_point(pt),
1928 cyl.project_point(pt),
1929 sphere_first,
1930 );
1931 points.push(IntersectionPoint {
1932 point: pt,
1933 param1,
1934 param2,
1935 });
1936 }
1937 let degree = 3.min(positions.len() - 1);
1938 let curve = interpolate(&positions, degree)?;
1939 curves.push(IntersectionCurve { curve, points });
1940 }
1941
1942 Ok(Some(curves))
1943}
1944
1945fn off_axis_sphere_cylinder(
1954 sphere: &SphericalSurface,
1955 cyl: &CylindricalSurface,
1956 sphere_first: bool,
1957) -> Option<Vec<IntersectionCurve>> {
1958 let (centre, radius) = (sphere.center(), sphere.radius());
1959 let axis = cyl.axis();
1960 let offset = centre - cyl.origin();
1961 let axis_distance = (offset - axis * offset.dot(axis)).length();
1962 let lin_tol = Tolerance::new().linear;
1963 if axis_distance > radius + cyl.radius() + lin_tol
1964 || axis_distance + radius < cyl.radius() - lin_tol
1965 {
1966 return Some(Vec::new());
1967 }
1968 let roots = |u: f64| {
1969 let q = cyl.evaluate(u, 0.0) - centre;
1970 ruling_quadratic(1.0, 2.0 * q.dot(axis), q.dot(q) - radius * radius)
1971 };
1972 let samples = ruling_samples(cyl, &roots);
1973 let loops = if samples.iter().all(Option::is_some) {
1974 closed_ruling_loops(&samples)
1975 } else {
1976 partial_ruling_loops(cyl, &roots, &samples)
1977 };
1978 if loops.is_empty() {
1979 return None;
1980 }
1981 Some(fit_ruling_loops(&loops, |p| {
1982 in_order(sphere.project_point(p), cyl.project_point(p), sphere_first)
1983 }))
1984}
1985
1986const fn in_order(a: (f64, f64), b: (f64, f64), a_first: bool) -> ((f64, f64), (f64, f64)) {
1989 if a_first { (a, b) } else { (b, a) }
1990}
1991
1992#[allow(clippy::too_many_lines, clippy::unnecessary_wraps)]
2006fn algebraic_cylinder_cylinder(
2007 c1: &CylindricalSurface,
2008 c2: &CylindricalSurface,
2009) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2010 let alpha = c1.axis().dot(c2.axis());
2011 let a_coeff = 1.0 - alpha * alpha;
2012
2013 if a_coeff.abs() < 1e-12 {
2015 return Ok(None);
2016 }
2017
2018 let r1 = c1.radius();
2019 let r2 = c2.radius();
2020 let o1 = c1.origin();
2021 let o2 = c2.origin();
2022 let a1 = c1.axis();
2023 let a2 = c2.axis();
2024
2025 let delta = Vec3::new(o1.x() - o2.x(), o1.y() - o2.y(), o1.z() - o2.z());
2028 let cross = a1.cross(a2);
2029 let cross_len = cross.length();
2030 if cross_len > 1e-12 {
2031 let axis_dist = delta.dot(cross).abs() / cross_len;
2032 if axis_dist > r1 + r2 + Tolerance::new().linear {
2033 return Ok(Some(vec![])); }
2035 }
2036
2037 let roots = |sweep: &CylindricalSurface, other: &CylindricalSurface| {
2043 let (o, a, radius) = (other.origin(), other.axis(), other.radius());
2044 let alpha = sweep.axis().dot(a);
2045 let quad = 1.0 - alpha * alpha;
2046 let (axis, sweep) = (sweep.axis(), sweep.clone());
2047 move |u: f64| {
2048 let q = sweep.evaluate(u, 0.0) - o;
2049 let (q_a1, q_a2) = (q.dot(axis), q.dot(a));
2050 let b = 2.0 * (q_a1 - alpha * q_a2);
2051 let c = q.dot(q) - q_a2 * q_a2 - radius * radius;
2052 ruling_quadratic(quad, b, c)
2053 }
2054 };
2055 let (roots1, roots2) = (roots(c1, c2), roots(c2, c1));
2056 let samples1 = ruling_samples(c1, &roots1);
2057 let loops = if samples1.iter().all(Option::is_some) {
2058 closed_ruling_loops(&samples1)
2059 } else {
2060 let samples2 = ruling_samples(c2, &roots2);
2061 if samples2.iter().all(Option::is_some) {
2062 closed_ruling_loops(&samples2)
2063 } else if samples1.iter().any(Option::is_some) {
2064 partial_ruling_loops(c1, &roots1, &samples1)
2065 } else {
2066 partial_ruling_loops(c2, &roots2, &samples2)
2067 }
2068 };
2069 if loops.is_empty() {
2070 return Ok(None);
2071 }
2072 Ok(Some(fit_ruling_loops(&loops, |p| {
2073 (c1.project_point(p), c2.project_point(p))
2074 })))
2075}
2076
2077const RULING_SAMPLES: usize = 128;
2081
2082#[allow(clippy::cast_precision_loss)]
2083fn ruling_u(i: usize) -> f64 {
2084 TAU * (i as f64 + 0.5) / RULING_SAMPLES as f64
2085}
2086
2087fn ruling_quadratic(quad: f64, b: f64, c: f64) -> (f64, f64, f64) {
2089 let disc = b * b - 4.0 * quad * c;
2090 let root = disc.max(0.0).sqrt();
2091 (disc, (-b + root) / (2.0 * quad), (-b - root) / (2.0 * quad))
2092}
2093
2094fn ruling_samples(
2098 sweep: &CylindricalSurface,
2099 roots: &impl Fn(f64) -> (f64, f64, f64),
2100) -> Vec<Option<(Point3, Point3)>> {
2101 let lin_tol = Tolerance::new().linear;
2102 (0..RULING_SAMPLES)
2103 .map(|i| {
2104 let u = ruling_u(i);
2105 let (disc, vp, vm) = roots(u);
2106 (disc >= -lin_tol).then(|| (sweep.evaluate(u, vp), sweep.evaluate(u, vm)))
2107 })
2108 .collect()
2109}
2110
2111fn closed_ruling_loops(samples: &[Option<(Point3, Point3)>]) -> Vec<Vec<Point3>> {
2113 let mut plus: Vec<Point3> = samples.iter().flatten().map(|s| s.0).collect();
2114 let mut minus: Vec<Point3> = samples.iter().flatten().map(|s| s.1).collect();
2115 plus.push(plus[0]);
2116 minus.push(minus[0]);
2117 vec![plus, minus]
2118}
2119
2120fn partial_ruling_loops(
2125 sweep: &CylindricalSurface,
2126 roots: &impl Fn(f64) -> (f64, f64, f64),
2127 samples: &[Option<(Point3, Point3)>],
2128) -> Vec<Vec<Point3>> {
2129 let branch_point = |inside: usize, outside: usize| -> Point3 {
2130 let (mut lo, mut hi) = (ruling_u(inside), ruling_u(outside));
2131 if (hi - lo).abs() > std::f64::consts::PI {
2132 hi += if hi < lo { TAU } else { -TAU };
2133 }
2134 for _ in 0..60 {
2135 let mid = 0.5 * (lo + hi);
2136 if roots(mid).0 >= 0.0 {
2137 lo = mid;
2138 } else {
2139 hi = mid;
2140 }
2141 }
2142 let (_, vp, vm) = roots(lo);
2143 sweep.evaluate(lo, 0.5 * (vp + vm))
2144 };
2145 let Some(first_gap) = samples.iter().position(Option::is_none) else {
2146 return Vec::new();
2147 };
2148 let mut loops = Vec::new();
2149 let mut k = 0;
2150 while k < RULING_SAMPLES {
2151 let i = (first_gap + k) % RULING_SAMPLES;
2152 if samples[i].is_none() {
2153 k += 1;
2154 continue;
2155 }
2156 let start = i;
2157 let mut run = Vec::new();
2158 while k < RULING_SAMPLES {
2159 let j = (first_gap + k) % RULING_SAMPLES;
2160 let Some(pair) = samples[j] else { break };
2161 run.push(pair);
2162 k += 1;
2163 }
2164 let end = (start + run.len() - 1) % RULING_SAMPLES;
2165 let head = branch_point(start, (start + RULING_SAMPLES - 1) % RULING_SAMPLES);
2166 let tail = branch_point(end, (end + 1) % RULING_SAMPLES);
2167 let mut pts = vec![head];
2168 pts.extend(run.iter().map(|p| p.0));
2169 pts.push(tail);
2170 pts.extend(run.iter().rev().map(|p| p.1));
2171 pts.push(head);
2172 loops.push(pts);
2173 }
2174 loops
2175}
2176
2177fn fit_ruling_loops(
2180 loops: &[Vec<Point3>],
2181 params: impl Fn(Point3) -> ((f64, f64), (f64, f64)),
2182) -> Vec<IntersectionCurve> {
2183 let mut curves = Vec::new();
2184 for pts in loops {
2185 if pts.len() < 4 {
2186 continue;
2187 }
2188 let ipts: Vec<IntersectionPoint> = pts
2189 .iter()
2190 .map(|&p| {
2191 let (param1, param2) = params(p);
2192 IntersectionPoint {
2193 point: p,
2194 param1,
2195 param2,
2196 }
2197 })
2198 .collect();
2199 let degree = 3.min(pts.len() - 1);
2200 if let Ok(curve) = interpolate(pts, degree) {
2201 curves.push(IntersectionCurve {
2202 curve,
2203 points: ipts,
2204 });
2205 }
2206 }
2207 curves
2208}
2209
2210#[allow(clippy::unnecessary_wraps)]
2236fn algebraic_parallel_cone_cylinder(
2237 cone: &ConicalSurface,
2238 cyl: &CylindricalSurface,
2239 v_range_cone: Option<(f64, f64)>,
2240 v_range_cyl: Option<(f64, f64)>,
2241) -> Result<Option<Vec<IntersectionCurve>>, MathError> {
2242 let axis = cone.axis();
2243 if axis.dot(cyl.axis()).abs() < 1.0 - 1e-10 {
2244 return Ok(None); }
2246
2247 let apex = cone.apex();
2248 let delta = cyl.origin() - apex;
2249 let along = delta.dot(axis);
2250 let perp = delta - axis * along;
2251 let d = perp.length();
2252 if d < 1e-9 {
2253 return Ok(None); }
2255
2256 let (e1, e2) = (cone.x_axis(), cone.y_axis());
2257 let phi0 = perp.dot(e2).atan2(perp.dot(e1));
2258
2259 let (sin_t, cos_t) = cone.half_angle().sin_cos();
2260 if cos_t < 1e-12 || sin_t < 1e-12 {
2261 return Ok(None);
2262 }
2263 let r = cyl.radius();
2264
2265 let mut v_min = (d - r).abs() / cos_t;
2267 let mut v_max = (d + r) / cos_t;
2268 if v_max <= v_min {
2269 return Ok(Some(vec![]));
2270 }
2271
2272 let mut lo = v_min;
2278 let mut hi = v_max;
2279 if let Some((a, b)) = v_range_cone {
2284 let (a, b) = if a <= b { (a, b) } else { (b, a) };
2285 lo = lo.max(a);
2286 hi = hi.min(b);
2287 }
2288 if let Some((a, b)) = v_range_cyl {
2289 let flip = cyl.axis().dot(axis);
2292 let to_cone_v = |cv: f64| (along + cv * flip) / sin_t;
2293 let (a, b) = (to_cone_v(a), to_cone_v(b));
2294 let (a, b) = if a <= b { (a, b) } else { (b, a) };
2295 lo = lo.max(a);
2296 hi = hi.min(b);
2297 }
2298 v_min = lo.max(v_min);
2299 v_max = hi.min(v_max);
2300 if v_max - v_min <= 1e-12 {
2301 return Ok(Some(vec![]));
2302 }
2303
2304 let n_samples = 128;
2305 let mut plus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
2306 let mut minus: Vec<Point3> = Vec::with_capacity(n_samples + 1);
2307 #[allow(clippy::cast_precision_loss)]
2308 for i in 0..=n_samples {
2309 let v = v_min + (v_max - v_min) * (i as f64) / (n_samples as f64);
2310 let rho = v * cos_t;
2311 if rho < 1e-12 {
2312 if (d - r).abs() < 1e-12 {
2320 let apex = cone.evaluate(phi0, v);
2321 plus.push(apex);
2322 minus.push(apex);
2323 }
2324 continue;
2325 }
2326 let cos_alpha = ((d * d + rho * rho - r * r) / (2.0 * d * rho)).clamp(-1.0, 1.0);
2327 let alpha = cos_alpha.acos();
2328 plus.push(cone.evaluate(phi0 + alpha, v));
2329 minus.push(cone.evaluate(phi0 - alpha, v));
2330 }
2331
2332 let mut curves = Vec::new();
2333 for pts in [&plus, &minus] {
2334 if pts.len() < 4 {
2337 continue;
2338 }
2339 let ipts: Vec<IntersectionPoint> = pts
2340 .iter()
2341 .map(|&p| IntersectionPoint {
2342 point: p,
2343 param1: cone.project_point(p),
2344 param2: cyl.project_point(p),
2345 })
2346 .collect();
2347 let degree = 3.min(pts.len() - 1);
2348 match interpolate(pts, degree) {
2349 Ok(curve) => curves.push(IntersectionCurve {
2350 curve,
2351 points: ipts,
2352 }),
2353 Err(_) => return Ok(None),
2358 }
2359 }
2360
2361 Ok(Some(curves))
2362}
2363
2364fn algebraic_sphere_sphere(
2372 s1: &SphericalSurface,
2373 s2: &SphericalSurface,
2374) -> Result<Vec<IntersectionCurve>, MathError> {
2375 let c1 = s1.center();
2376 let c2 = s2.center();
2377 let r1 = s1.radius();
2378 let r2 = s2.radius();
2379
2380 let delta = c2 - c1;
2381 let d_sq = delta.x() * delta.x() + delta.y() * delta.y() + delta.z() * delta.z();
2382 let d = d_sq.sqrt();
2383
2384 if d < 1e-12 {
2385 return Ok(vec![]);
2387 }
2388
2389 if d > r1 + r2 + 1e-10 {
2391 return Ok(vec![]); }
2393 if d + r2.min(r1) + 1e-10 < r1.max(r2) {
2394 return Ok(vec![]); }
2396
2397 let d1 = (d_sq + r1 * r1 - r2 * r2) / (2.0 * d);
2399
2400 let r_circle_sq = r1 * r1 - d1 * d1;
2402 if r_circle_sq < 0.0 {
2403 if r_circle_sq > -1e-10 {
2405 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2407 let tangent_pt = Point3::new(
2408 c1.x() + axis.x() * d1,
2409 c1.y() + axis.y() * d1,
2410 c1.z() + axis.z() * d1,
2411 );
2412 let ipt = IntersectionPoint {
2413 point: tangent_pt,
2414 param1: (0.0, 0.0),
2415 param2: (0.0, 0.0),
2416 };
2417 return Ok(vec![IntersectionCurve {
2419 curve: interpolate(&[tangent_pt, tangent_pt], 1)?,
2420 points: vec![ipt],
2421 }]);
2422 }
2423 return Ok(vec![]);
2424 }
2425
2426 let r_circle = r_circle_sq.sqrt();
2427 let axis = Vec3::new(delta.x() / d, delta.y() / d, delta.z() / d);
2428 let center = Point3::new(
2429 c1.x() + axis.x() * d1,
2430 c1.y() + axis.y() * d1,
2431 c1.z() + axis.z() * d1,
2432 );
2433
2434 let basis = Frame3::from_normal(center, axis)?;
2436 let u_dir = basis.x;
2437 let v_dir = basis.y;
2438
2439 let n_samples = 33; let mut points = Vec::with_capacity(n_samples);
2442 let mut positions = Vec::with_capacity(n_samples);
2443 #[allow(clippy::cast_precision_loss)]
2444 for i in 0..n_samples {
2445 let theta = TAU * i as f64 / (n_samples - 1) as f64;
2446 let (sin_t, cos_t) = theta.sin_cos();
2447 let pt = Point3::new(
2448 center.x() + (u_dir.x() * cos_t + v_dir.x() * sin_t) * r_circle,
2449 center.y() + (u_dir.y() * cos_t + v_dir.y() * sin_t) * r_circle,
2450 center.z() + (u_dir.z() * cos_t + v_dir.z() * sin_t) * r_circle,
2451 );
2452 positions.push(pt);
2453 points.push(IntersectionPoint {
2454 point: pt,
2455 param1: (0.0, 0.0),
2456 param2: (0.0, 0.0),
2457 });
2458 }
2459
2460 let degree = 3.min(positions.len() - 1);
2461 let curve = interpolate(&positions, degree)?;
2462
2463 Ok(vec![IntersectionCurve { curve, points }])
2464}
2465
2466#[allow(clippy::too_many_arguments)]
2472fn correct_to_intersection(
2473 a: &AnalyticSurface<'_>,
2474 b: &AnalyticSurface<'_>,
2475 surf_a: &dyn Fn(f64, f64) -> Point3,
2476 norm_a: &dyn Fn(f64, f64) -> Vec3,
2477 surf_b: &dyn Fn(f64, f64) -> Point3,
2478 norm_b: &dyn Fn(f64, f64) -> Vec3,
2479 point: Point3,
2480 u_range_a: (f64, f64),
2481 v_range_a: (f64, f64),
2482 u_range_b: (f64, f64),
2483 v_range_b: (f64, f64),
2484 max_iters: usize,
2485) -> Point3 {
2486 let mut p = point;
2487 for _ in 0..max_iters {
2488 let (ua, va) = project_analytic(a, p, u_range_a, v_range_a);
2489 let (ub, vb) = project_analytic(b, p, u_range_b, v_range_b);
2490 let pa = surf_a(ua, va);
2491 let pb = surf_b(ub, vb);
2492 let na = norm_a(ua, va);
2493 let nb = norm_b(ub, vb);
2494 let pv = Vec3::new(p.x(), p.y(), p.z());
2495
2496 let da = (pv - Vec3::new(pa.x(), pa.y(), pa.z())).dot(na);
2497 let db = (pv - Vec3::new(pb.x(), pb.y(), pb.z())).dot(nb);
2498
2499 if da.abs() < 1e-7 && db.abs() < 1e-7 {
2500 break;
2501 }
2502
2503 let t = na.cross(nb);
2504 let t_len = t.length();
2505 if t_len < 1e-10 {
2506 return Point3::new(
2508 (pa.x() + pb.x()) * 0.5,
2509 (pa.y() + pb.y()) * 0.5,
2510 (pa.z() + pb.z()) * 0.5,
2511 );
2512 }
2513 let t_hat = t * (1.0 / t_len);
2514
2515 let det = na.x() * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2517 - na.y() * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2518 + na.z() * (nb.x() * t_hat.y() - nb.y() * t_hat.x());
2519 if det.abs() < 1e-15 {
2520 return Point3::new(
2521 (pa.x() + pb.x()) * 0.5,
2522 (pa.y() + pb.y()) * 0.5,
2523 (pa.z() + pb.z()) * 0.5,
2524 );
2525 }
2526 let inv = 1.0 / det;
2527 let dx = inv
2529 * (-da * (nb.y() * t_hat.z() - nb.z() * t_hat.y())
2530 + db * (na.y() * t_hat.z() - na.z() * t_hat.y()));
2531 let dy = inv
2532 * (da * (nb.x() * t_hat.z() - nb.z() * t_hat.x())
2533 - db * (na.x() * t_hat.z() - na.z() * t_hat.x()));
2534 let dz = inv
2535 * (-da * (nb.x() * t_hat.y() - nb.y() * t_hat.x())
2536 + db * (na.x() * t_hat.y() - na.y() * t_hat.x()));
2537 let candidate = Point3::new(p.x() + dx, p.y() + dy, p.z() + dz);
2538
2539 let (uc, vc) = project_analytic(a, candidate, u_range_a, v_range_a);
2542 let (ud, vd) = project_analytic(b, candidate, u_range_b, v_range_b);
2543 let pc_a = surf_a(uc, vc);
2544 let pc_b = surf_b(ud, vd);
2545 let cv = Vec3::new(candidate.x(), candidate.y(), candidate.z());
2546 let da_new = (cv - Vec3::new(pc_a.x(), pc_a.y(), pc_a.z()))
2547 .dot(norm_a(uc, vc))
2548 .abs();
2549 let db_new = (cv - Vec3::new(pc_b.x(), pc_b.y(), pc_b.z()))
2550 .dot(norm_b(ud, vd))
2551 .abs();
2552 if da_new > da.abs() && db_new > db.abs() {
2553 return p;
2554 }
2555
2556 p = candidate;
2557 }
2558 p
2559}
2560
2561#[allow(clippy::too_many_arguments)]
2567fn march_analytic_intersection(
2568 a: &AnalyticSurface<'_>,
2569 b: &AnalyticSurface<'_>,
2570 surf_a: &dyn Fn(f64, f64) -> Point3,
2571 norm_a: &dyn Fn(f64, f64) -> Vec3,
2572 surf_b: &dyn Fn(f64, f64) -> Point3,
2573 norm_b: &dyn Fn(f64, f64) -> Vec3,
2574 seed: Point3,
2575 u_range_a: (f64, f64),
2576 v_range_a: (f64, f64),
2577 u_range_b: (f64, f64),
2578 v_range_b: (f64, f64),
2579 initial_step: f64,
2580 u_periodic_a: bool,
2581 u_periodic_b: bool,
2582) -> Vec<Point3> {
2583 let max_steps = 500;
2584 let h_min = 1e-6;
2585 let h_max = initial_step * 4.0;
2586 let closure_dist = initial_step * 5.0;
2590 let max_angle = 10.0_f64.to_radians();
2592 let min_angle = 2.0_f64.to_radians();
2593
2594 let mut forward = Vec::new();
2596 let mut backward = Vec::new();
2598
2599 for (direction, points) in [(1.0_f64, &mut forward), (-1.0_f64, &mut backward)] {
2600 let mut current = seed;
2601 let mut h = initial_step;
2602 let mut prev_tangent: Option<Vec3> = None;
2603
2604 for _ in 0..max_steps {
2605 let (ua, va) = project_analytic(a, current, u_range_a, v_range_a);
2606 let (ub, vb) = project_analytic(b, current, u_range_b, v_range_b);
2607
2608 let na = norm_a(ua, va);
2609 let nb = norm_b(ub, vb);
2610
2611 let tangent = na.cross(nb);
2612 let t_len = tangent.length();
2613 if t_len < 1e-10 {
2614 break;
2615 }
2616 let t_dir = tangent * (direction / t_len);
2617
2618 if let Some(prev_t) = prev_tangent {
2620 let cos_angle = prev_t.dot(t_dir).clamp(-1.0, 1.0);
2621 let angle = cos_angle.acos();
2622 if angle > max_angle && h > h_min {
2623 h = (h * 0.5).max(h_min);
2624 } else if angle < min_angle {
2625 h = (h * 2.0).min(h_max);
2626 }
2627 }
2628 prev_tangent = Some(t_dir);
2629
2630 let next = Point3::new(
2631 h.mul_add(t_dir.x(), current.x()),
2632 h.mul_add(t_dir.y(), current.y()),
2633 h.mul_add(t_dir.z(), current.z()),
2634 );
2635
2636 let (ua2, va2) = project_analytic(a, next, u_range_a, v_range_a);
2637 let (ub2, vb2) = project_analytic(b, next, u_range_b, v_range_b);
2638
2639 let pa = surf_a(ua2, va2);
2640 let pb = surf_b(ub2, vb2);
2641 let mid = Point3::new(
2642 (pa.x() + pb.x()) * 0.5,
2643 (pa.y() + pb.y()) * 0.5,
2644 (pa.z() + pb.z()) * 0.5,
2645 );
2646 let out_a = (!u_periodic_a && (ua2 <= u_range_a.0 || ua2 >= u_range_a.1))
2647 || va2 <= v_range_a.0
2648 || va2 >= v_range_a.1;
2649 let out_b = (!u_periodic_b && (ub2 <= u_range_b.0 || ub2 >= u_range_b.1))
2650 || vb2 <= v_range_b.0
2651 || vb2 >= v_range_b.1;
2652
2653 if out_a || out_b {
2654 break;
2655 }
2656
2657 let dist_to_seed = (mid - seed).length();
2661 if points.len() > 10 && dist_to_seed < closure_dist {
2662 points.push(seed);
2663 break;
2664 }
2665
2666 points.push(mid);
2667 current = mid;
2668 }
2669 }
2670
2671 backward.reverse();
2673 let mut result = backward;
2674 result.push(seed);
2675 result.append(&mut forward);
2676
2677 for pt in &mut result {
2679 *pt = correct_to_intersection(
2680 a, b, surf_a, norm_a, surf_b, norm_b, *pt, u_range_a, v_range_a, u_range_b, v_range_b,
2681 5,
2682 );
2683 }
2684
2685 result
2686}
2687
2688fn project_analytic(
2692 surface: &AnalyticSurface<'_>,
2693 point: Point3,
2694 u_range: (f64, f64),
2695 v_range: (f64, f64),
2696) -> (f64, f64) {
2697 match surface {
2698 AnalyticSurface::Cylinder(cyl) => {
2699 let (u, v) = cyl.project_point(point);
2700 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2701 }
2702 AnalyticSurface::Sphere(sphere) => {
2703 let (u, v) = sphere.project_point(point);
2704 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2705 }
2706 AnalyticSurface::Cone(cone) => {
2707 let (u, v) = cone.project_point(point);
2708 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2709 }
2710 AnalyticSurface::Torus(torus) => {
2711 let (u, v) = torus.project_point(point);
2712 (u.clamp(u_range.0, u_range.1), v.clamp(v_range.0, v_range.1))
2713 }
2714 }
2715}
2716
2717fn is_u_periodic(surface: &AnalyticSurface<'_>) -> bool {
2721 matches!(
2722 surface,
2723 AnalyticSurface::Cylinder(_)
2724 | AnalyticSurface::Cone(_)
2725 | AnalyticSurface::Sphere(_)
2726 | AnalyticSurface::Torus(_)
2727 )
2728}
2729
2730#[allow(clippy::type_complexity)]
2732fn surface_closures<'a>(
2733 surface: &'a AnalyticSurface<'a>,
2734) -> (
2735 Box<dyn Fn(f64, f64) -> Point3 + 'a>,
2736 Box<dyn Fn(f64, f64) -> Vec3 + 'a>,
2737 (f64, f64),
2738 (f64, f64),
2739) {
2740 match surface {
2741 AnalyticSurface::Cylinder(cyl) => (
2742 Box::new(|u, v| cyl.evaluate(u, v)),
2743 Box::new(|u, v| cyl.normal(u, v)),
2744 (0.0, TAU),
2745 (-1.0, 1.0),
2746 ),
2747 AnalyticSurface::Cone(cone) => (
2748 Box::new(|u, v| cone.evaluate(u, v)),
2749 Box::new(|u, v| cone.normal(u, v)),
2750 (0.0, TAU),
2751 (0.01, 2.0),
2752 ),
2753 AnalyticSurface::Sphere(sphere) => (
2754 Box::new(|u, v| sphere.evaluate(u, v)),
2755 Box::new(|u, v| sphere.normal(u, v)),
2756 (0.0, TAU),
2757 (-FRAC_PI_2, FRAC_PI_2),
2758 ),
2759 AnalyticSurface::Torus(torus) => (
2760 Box::new(|u, v| torus.evaluate(u, v)),
2761 Box::new(|u, v| torus.normal(u, v)),
2762 (0.0, TAU),
2763 (0.0, TAU),
2764 ),
2765 }
2766}
2767
2768#[cfg(test)]
2769#[allow(clippy::unwrap_used, clippy::expect_used)]
2770mod tests {
2771 use super::*;
2772 use crate::tolerance::Tolerance;
2773
2774 #[test]
2775 fn plane_cylinder_perpendicular() {
2776 let cyl =
2777 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
2778 .unwrap();
2779
2780 let curves = intersect_plane_cylinder(&cyl, Vec3::new(0.0, 0.0, 1.0), 3.0).unwrap();
2782 assert!(!curves.is_empty(), "should find intersection curve");
2783 assert!(
2784 curves[0].points.len() > 10,
2785 "should have many sample points"
2786 );
2787
2788 let tol = Tolerance::loose();
2789 for pt in &curves[0].points {
2790 assert!(
2791 tol.approx_eq(pt.point.z(), 3.0),
2792 "z should be ~3.0, got {}",
2793 pt.point.z()
2794 );
2795 let r = pt.point.x().hypot(pt.point.y());
2796 assert!(tol.approx_eq(r, 2.0), "radius should be ~2.0, got {r}");
2797 }
2798 }
2799
2800 #[test]
2801 fn plane_sphere_equator() {
2802 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0).unwrap();
2803
2804 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
2805 assert!(!curves.is_empty());
2806
2807 let tol = Tolerance::loose();
2808 for pt in &curves[0].points {
2809 assert!(
2810 tol.approx_eq(pt.point.z(), 0.0),
2811 "z should be ~0, got {}",
2812 pt.point.z()
2813 );
2814 let r = pt.point.x().hypot(pt.point.y());
2815 assert!(tol.approx_eq(r, 3.0), "radius should be ~3.0, got {r}");
2816 }
2817 }
2818
2819 #[test]
2820 fn plane_sphere_no_intersection() {
2821 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
2822
2823 let curves = intersect_plane_sphere(&sphere, Vec3::new(0.0, 0.0, 1.0), 5.0).unwrap();
2824 assert!(curves.is_empty());
2825 }
2826
2827 #[test]
2828 fn plane_cone_cross_section() {
2829 let cone = ConicalSurface::new(
2830 Point3::new(0.0, 0.0, 0.0),
2831 Vec3::new(0.0, 0.0, 1.0),
2832 std::f64::consts::FRAC_PI_4,
2833 )
2834 .unwrap();
2835
2836 let curves = intersect_plane_cone(&cone, Vec3::new(0.0, 0.0, 1.0), 1.0).unwrap();
2837 assert!(!curves.is_empty(), "should find intersection with cone");
2838 }
2839
2840 #[test]
2847 fn offset_parallel_equal_angle_cones_give_one_exact_ellipse() {
2848 let c1 = ConicalSurface::new(
2849 Point3::new(
2850 -16.999_999_999_999_975,
2851 -16.999_999_999_999_975,
2852 5.849_999_999_999_951,
2853 ),
2854 Vec3::new(0.0, 0.0, -1.0),
2855 0.785_398_163_397_433_5,
2856 )
2857 .unwrap();
2858 let c2 = ConicalSurface::new(
2859 Point3::new(
2860 -16.750_000_000_000_036,
2861 -16.750_000_000_000_018,
2862 0.749_999_999_999_881,
2863 ),
2864 Vec3::new(0.0, 0.0, 1.0),
2865 0.785_398_163_397_467_6,
2866 )
2867 .unwrap();
2868
2869 let curves = exact_cone_cone(&c1, &c2)
2870 .unwrap()
2871 .expect("offset parallel equal-angle cones must take the radical-plane path");
2872 assert_eq!(curves.len(), 1, "expected exactly one section conic");
2873 assert!(
2874 matches!(curves[0], ExactIntersectionCurve::Ellipse(_)),
2875 "expected an ellipse section, got {:?}",
2876 curves[0]
2877 );
2878 let ExactIntersectionCurve::Ellipse(ellipse) = &curves[0] else {
2879 return;
2880 };
2881
2882 for i in 0..16 {
2886 let p = crate::traits::ParametricCurve::evaluate(ellipse, TAU * f64::from(i) / 16.0);
2887 for (cone, label) in [(&c1, "c1"), (&c2, "c2")] {
2888 let rel = p - cone.apex();
2889 let rel_v = Vec3::new(rel.x(), rel.y(), rel.z());
2890 let axial = rel_v.dot(cone.axis());
2891 let radial = (rel_v - cone.axis() * axial).length();
2892 assert!(
2893 axial > 0.0,
2894 "{label}: sample on phantom nappe (axial {axial})"
2895 );
2896 let expect = cone.half_angle().tan() * axial;
2897 assert!(
2898 (radial - expect).abs() < 1e-9,
2899 "{label}: sample off surface by {}",
2900 (radial - expect).abs()
2901 );
2902 }
2903 }
2904 }
2905
2906 #[test]
2910 fn offset_parallel_cones_opening_apart_have_no_real_intersection() {
2911 let c1 = ConicalSurface::new(
2912 Point3::new(0.0, 0.0, 5.0),
2913 Vec3::new(0.0, 0.0, -1.0),
2914 std::f64::consts::FRAC_PI_4,
2915 )
2916 .unwrap();
2917 let c2 = ConicalSurface::new(
2918 Point3::new(0.25, 0.25, 20.0),
2919 Vec3::new(0.0, 0.0, 1.0),
2920 std::f64::consts::FRAC_PI_4,
2921 )
2922 .unwrap();
2923 let curves = exact_cone_cone(&c1, &c2)
2924 .unwrap()
2925 .expect("radical-plane path");
2926 assert!(curves.is_empty(), "disjoint nappes must yield no curves");
2927 }
2928
2929 #[test]
2932 fn offset_parallel_cones_with_unequal_angles_defer() {
2933 let c1 = ConicalSurface::new(
2934 Point3::new(0.0, 0.0, 5.0),
2935 Vec3::new(0.0, 0.0, -1.0),
2936 std::f64::consts::FRAC_PI_4,
2937 )
2938 .unwrap();
2939 let c2 = ConicalSurface::new(Point3::new(0.25, 0.25, 0.5), Vec3::new(0.0, 0.0, 1.0), 0.6)
2940 .unwrap();
2941 assert!(exact_cone_cone(&c1, &c2).unwrap().is_none());
2942 }
2943
2944 #[test]
2945 fn coaxial_cones_cross_at_single_circle() {
2946 let outer = ConicalSurface::new(
2951 Point3::new(0.0, 0.0, 50.0),
2952 Vec3::new(0.0, 0.0, -1.0),
2953 5.0_f64.atan(),
2954 )
2955 .unwrap();
2956 let inner = ConicalSurface::new(
2957 Point3::new(0.0, 0.0, 90.0),
2958 Vec3::new(0.0, 0.0, -1.0),
2959 10.0_f64.atan(),
2960 )
2961 .unwrap();
2962
2963 let curves = intersect_analytic_analytic_bounded(
2964 AnalyticSurface::Cone(&outer),
2965 AnalyticSurface::Cone(&inner),
2966 32,
2967 None,
2968 None,
2969 )
2970 .unwrap();
2971
2972 assert_eq!(
2973 curves.len(),
2974 1,
2975 "coaxial cones crossing at one circle must yield exactly one curve, got {}",
2976 curves.len()
2977 );
2978 for p in &curves[0].points {
2979 let r = p.point.x().hypot(p.point.y());
2980 assert!(
2981 (p.point.z() - 10.0).abs() < 1e-6 && (r - 8.0).abs() < 1e-6,
2982 "intersection point off the expected z=10,r=8 circle: {:?}",
2983 p.point
2984 );
2985 }
2986 }
2987
2988 #[test]
2989 fn plane_torus_cross_section() {
2990 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0, 1.0).unwrap();
2991
2992 let curves = intersect_plane_torus(&torus, Vec3::new(0.0, 0.0, 1.0), 0.0).unwrap();
2993 assert!(
2994 !curves.is_empty(),
2995 "should find intersection curves with torus"
2996 );
2997 }
2998
2999 fn torus_implicit(p: Point3, major: f64, minor: f64) -> f64 {
3002 let rho = p.x().hypot(p.y());
3003 ((rho - major).hypot(p.z())) - minor
3004 }
3005
3006 #[test]
3012 fn parallel_cone_cylinder_gives_two_exact_branches() {
3013 use crate::traits::ParametricCurve;
3014 let cone = ConicalSurface::new(
3015 Point3::new(-5.45, -36.55, -4.85),
3016 Vec3::new(0.0, 0.0, 1.0),
3017 std::f64::consts::FRAC_PI_4,
3018 )
3019 .unwrap();
3020 let cyl = CylindricalSurface::new(
3021 Point3::new(-8.0, -34.0, -5.0),
3022 Vec3::new(0.0, 0.0, 1.0),
3023 4.45,
3024 )
3025 .unwrap();
3026 let v_hint = (1.484_924_240_492_058, 2.616_295_090_390_43);
3028 let curves = intersect_analytic_analytic_bounded(
3029 AnalyticSurface::Cone(&cone),
3030 AnalyticSurface::Cylinder(&cyl),
3031 32,
3032 Some(v_hint),
3033 Some((0.0, 2.5)),
3034 )
3035 .unwrap();
3036
3037 assert_eq!(curves.len(), 2, "expected exactly the two branches");
3038 for c in &curves {
3039 let (t0, t1) = c.curve.domain();
3040 for k in 0..=32 {
3041 let t = (t1 - t0).mul_add(f64::from(k) / 32.0, t0);
3042 let p = ParametricCurve::evaluate(&c.curve, t);
3043 let radial = ((p.x() + 8.0).powi(2) + (p.y() + 34.0).powi(2)).sqrt();
3045 assert!((radial - 4.45).abs() < 1e-6, "off cylinder: {radial}");
3046 let cone_r = ((p.x() + 5.45).powi(2) + (p.y() + 36.55).powi(2)).sqrt();
3048 assert!((cone_r - (p.z() + 4.85)).abs() < 1e-6, "off cone at {p:?}");
3049 assert!(p.z() >= -3.8 - 1e-9 && p.z() <= -3.0 + 1e-9, "z={}", p.z());
3051 }
3052 }
3053 }
3054
3055 #[test]
3058 fn coaxial_cone_cylinder_defers_to_other_paths() {
3059 let cone = ConicalSurface::new(
3060 Point3::new(0.0, 0.0, 0.0),
3061 Vec3::new(0.0, 0.0, 1.0),
3062 std::f64::consts::FRAC_PI_4,
3063 )
3064 .unwrap();
3065 let cyl =
3066 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 2.0)
3067 .unwrap();
3068 assert!(
3069 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
3070 .unwrap()
3071 .is_none()
3072 );
3073 }
3074
3075 #[test]
3076 fn oblique_cone_cylinder_defers_to_other_paths() {
3077 let cone = ConicalSurface::new(
3078 Point3::new(0.0, 0.0, 0.0),
3079 Vec3::new(0.0, 0.0, 1.0),
3080 std::f64::consts::FRAC_PI_4,
3081 )
3082 .unwrap();
3083 let cyl =
3084 CylindricalSurface::new(Point3::new(3.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3085 .unwrap();
3086 assert!(
3087 algebraic_parallel_cone_cylinder(&cone, &cyl, None, None)
3088 .unwrap()
3089 .is_none()
3090 );
3091 }
3092
3093 #[test]
3094 fn plane_torus_lobe_closes_and_stays_on_surface() {
3095 use crate::traits::ParametricCurve;
3096 let (major, minor) = (10.0, 3.0);
3097 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
3098
3099 for (n, d) in [
3103 (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), ] {
3107 let curves = intersect_plane_torus(&torus, n, d).unwrap();
3108 assert!(!curves.is_empty(), "plane n={n:?} d={d} found no curves");
3109 for c in &curves {
3110 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
3111 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
3112 assert!(
3113 (p0 - p1).length() < 1e-7,
3114 "lobe not closed: gap={} (n={n:?} d={d})",
3115 (p0 - p1).length()
3116 );
3117 for k in 0..=64 {
3119 let t = f64::from(k) / 64.0;
3120 let p = ParametricCurve::evaluate(&c.curve, t);
3121 assert!(
3122 torus_implicit(p, major, minor).abs() < 1e-2,
3123 "off-surface point {p:?} implicit={}",
3124 torus_implicit(p, major, minor)
3125 );
3126 }
3127 }
3128 }
3129 }
3130
3131 #[test]
3132 fn plane_torus_inner_tangent_figure_eight_stays_open() {
3133 use crate::traits::ParametricCurve;
3134 let (major, minor) = (10.0, 3.0);
3135 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), major, minor).unwrap();
3136
3137 let curves =
3143 intersect_plane_torus(&torus, Vec3::new(-1.0, 0.0, 0.0), -(major - minor)).unwrap();
3144 assert!(!curves.is_empty(), "inner-tangent plane found no curves");
3145 let max_gap = curves
3146 .iter()
3147 .map(|c| {
3148 let p0 = ParametricCurve::evaluate(&c.curve, 0.0);
3149 let p1 = ParametricCurve::evaluate(&c.curve, 1.0);
3150 (p0 - p1).length()
3151 })
3152 .fold(0.0_f64, f64::max);
3153 assert!(
3154 max_gap > 1e-2,
3155 "figure-eight chain was wrongly force-closed (max end-gap={max_gap})"
3156 );
3157 }
3158
3159 #[test]
3160 fn line_torus_box_edge_crossing_is_exact() {
3161 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
3164 let ts = intersect_line_torus(
3165 &torus,
3166 Point3::new(6.0, -4.0, -5.0),
3167 Vec3::new(0.0, 0.0, 1.0),
3168 );
3169 assert_eq!(ts.len(), 2, "expected 2 crossings, got {ts:?}");
3171 let zs: Vec<f64> = ts.iter().map(|t| -5.0 + t).collect();
3172 let rho = 6.0_f64.hypot(4.0);
3173 let z_exp = (9.0 - (rho - 10.0).powi(2)).sqrt();
3174 assert!(
3175 (zs[0] - (-z_exp)).abs() < 1e-9,
3176 "z0={} exp={}",
3177 zs[0],
3178 -z_exp
3179 );
3180 assert!((zs[1] - z_exp).abs() < 1e-9, "z1={} exp={}", zs[1], z_exp);
3181 for &t in &ts {
3183 let p = Point3::new(6.0, -4.0, -5.0 + t);
3184 let rho = p.x().hypot(p.y());
3185 let impl_v = (rho - 10.0).hypot(p.z()) - 3.0;
3186 assert!(impl_v.abs() < 1e-9, "off-torus impl={impl_v}");
3187 }
3188 }
3189
3190 #[test]
3191 fn line_torus_miss_and_tangent() {
3192 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 10.0, 3.0).unwrap();
3193 let miss = intersect_line_torus(
3195 &torus,
3196 Point3::new(20.0, 0.0, 0.0),
3197 Vec3::new(0.0, 0.0, 1.0),
3198 );
3199 assert!(miss.is_empty(), "expected no crossings, got {miss:?}");
3200 let axis =
3202 intersect_line_torus(&torus, Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
3203 assert!(axis.is_empty(), "z-axis should miss the tube, got {axis:?}");
3204 }
3205
3206 #[test]
3207 fn dispatch_via_analytic_surface() {
3208 let cyl =
3209 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3210 .unwrap();
3211 let curves = intersect_plane_analytic(
3212 AnalyticSurface::Cylinder(&cyl),
3213 Vec3::new(0.0, 0.0, 1.0),
3214 0.0,
3215 )
3216 .unwrap();
3217 assert!(!curves.is_empty());
3218 }
3219
3220 #[test]
3221 fn perpendicular_cylinders_intersect() {
3222 let cyl_z =
3223 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3224 .unwrap();
3225 let cyl_x =
3226 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3227 .unwrap();
3228
3229 let curves = intersect_analytic_analytic(
3230 AnalyticSurface::Cylinder(&cyl_z),
3231 AnalyticSurface::Cylinder(&cyl_x),
3232 16,
3233 )
3234 .unwrap();
3235
3236 assert!(
3237 !curves.is_empty(),
3238 "perpendicular cylinders should intersect"
3239 );
3240
3241 for c in &curves {
3242 assert!(
3243 c.points.len() >= 2,
3244 "intersection curve should have >= 2 points, got {}",
3245 c.points.len()
3246 );
3247 }
3248 }
3249
3250 #[test]
3253 fn partially_overlapping_cylinders_meet_in_one_closed_loop() {
3254 let cyl_z =
3255 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3256 .unwrap();
3257 let cyl_x =
3258 CylindricalSurface::new(Point3::new(0.0, 1.2, 0.0), Vec3::new(1.0, 0.0, 0.0), 1.0)
3259 .unwrap();
3260 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
3261 .unwrap()
3262 .unwrap();
3263 assert_eq!(curves.len(), 1);
3264 let curve = &curves[0].curve;
3265 let (t0, t1) = curve.domain();
3266 assert!((curve.evaluate(t0) - curve.evaluate(t1)).length() < 1e-9);
3267 let off = |p: Point3| {
3268 let on_z = (p.x().hypot(p.y()) - 1.0).abs();
3269 let on_x = ((p.y() - 1.2).hypot(p.z()) - 1.0).abs();
3270 on_z.max(on_x)
3271 };
3272 let worst = (0..=400)
3273 .map(|k| off(curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0)))
3274 .fold(0.0, f64::max);
3275 assert!(worst < 2e-4, "curve leaves the cylinders by {worst}");
3276 }
3277
3278 #[test]
3282 fn near_tangent_cylinders_find_their_loop_on_the_thinner_sweep() {
3283 let cyl_z =
3284 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3285 .unwrap();
3286 let cyl_x =
3287 CylindricalSurface::new(Point3::new(0.0, 1.1998, 0.0), Vec3::new(1.0, 0.0, 0.0), 0.2)
3288 .unwrap();
3289 let curves = algebraic_cylinder_cylinder(&cyl_z, &cyl_x)
3290 .unwrap()
3291 .expect("the thin cylinder's sweep finds the loop");
3292 assert_eq!(curves.len(), 1);
3293 }
3294
3295 #[test]
3296 fn sphere_cylinder_intersect() {
3297 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
3298 let cyl =
3299 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
3300 .unwrap();
3301
3302 let curves = intersect_analytic_analytic(
3303 AnalyticSurface::Sphere(&sphere),
3304 AnalyticSurface::Cylinder(&cyl),
3305 16,
3306 )
3307 .unwrap();
3308
3309 assert!(!curves.is_empty(), "sphere and cylinder should intersect");
3313 }
3314
3315 #[test]
3316 fn exact_sphere_cylinder_coaxial_two_circles() {
3317 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
3320 let cyl =
3321 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
3322 .unwrap();
3323 let circles = exact_sphere_cylinder(&sphere, &cyl)
3324 .unwrap()
3325 .expect("coaxial case returns Some");
3326 assert_eq!(circles.len(), 2, "through-bore meets the sphere twice");
3327 let mut zs: Vec<f64> = circles
3328 .iter()
3329 .filter_map(|c| match c {
3330 ExactIntersectionCurve::Circle(circle) => {
3331 assert!(
3332 (circle.radius() - 3.0).abs() < 1e-9,
3333 "rim radius == cyl radius"
3334 );
3335 Some(circle.center().z())
3336 }
3337 _ => None,
3338 })
3339 .collect();
3340 assert_eq!(zs.len(), 2, "both sections must be exact circles");
3341 zs.sort_by(f64::total_cmp);
3342 let z = 27.0_f64.sqrt();
3343 assert!((zs[0] + z).abs() < 1e-9 && (zs[1] - z).abs() < 1e-9);
3344 }
3345
3346 #[test]
3347 fn exact_sphere_cylinder_non_coaxial_defers() {
3348 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 6.0).unwrap();
3350 let cyl =
3351 CylindricalSurface::new(Point3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 3.0)
3352 .unwrap();
3353 assert!(
3354 exact_sphere_cylinder(&sphere, &cyl).unwrap().is_none(),
3355 "non-coaxial sphere/cylinder defers to the marcher"
3356 );
3357 }
3358
3359 fn off_axis_loops(cylinder_origin: Point3, cylinder_radius: f64) -> (usize, f64) {
3362 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 2.0).unwrap();
3363 let cyl =
3364 CylindricalSurface::new(cylinder_origin, Vec3::new(0.0, 0.0, 1.0), cylinder_radius)
3365 .unwrap();
3366 let curves = algebraic_sphere_cylinder(&sphere, &cyl, true)
3367 .unwrap()
3368 .unwrap();
3369 let mut worst: f64 = 0.0;
3370 for c in &curves {
3371 for ip in &c.points {
3372 let on_sphere = sphere.evaluate(ip.param1.0, ip.param1.1);
3373 let on_cylinder = cyl.evaluate(ip.param2.0, ip.param2.1);
3374 worst = worst
3375 .max((on_sphere - ip.point).length())
3376 .max((on_cylinder - ip.point).length());
3377 }
3378 let (t0, t1) = c.curve.domain();
3379 assert!((c.curve.evaluate(t0) - c.curve.evaluate(t1)).length() < 1e-9);
3380 for k in 0..=400 {
3381 let p = c.curve.evaluate(t0 + (t1 - t0) * f64::from(k) / 400.0);
3382 let on_sphere = ((p - Point3::new(0.0, 0.0, 0.0)).length() - 2.0).abs();
3383 let on_cylinder = ((p.x() - cylinder_origin.x())
3384 .hypot(p.y() - cylinder_origin.y())
3385 - cylinder_radius)
3386 .abs();
3387 worst = worst.max(on_sphere).max(on_cylinder);
3388 }
3389 }
3390 (curves.len(), worst)
3391 }
3392
3393 #[test]
3396 fn off_axis_drill_through_a_sphere_meets_it_in_two_loops() {
3397 let (count, worst) = off_axis_loops(Point3::new(0.5, 0.0, 0.0), 0.2);
3398 assert_eq!(count, 2);
3399 assert!(worst < 1e-5, "loops leave the surfaces by {worst}");
3400 }
3401
3402 #[test]
3404 fn cylinder_over_a_spheres_side_meets_it_in_one_loop() {
3405 let (count, worst) = off_axis_loops(Point3::new(1.8, 0.0, 0.0), 0.5);
3406 assert_eq!(count, 1);
3407 assert!(worst < 5e-4, "loop leaves the surfaces by {worst}");
3408 }
3409
3410 #[test]
3411 fn disjoint_cylinders_no_intersection() {
3412 let cyl_a =
3413 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
3414 .unwrap();
3415 let cyl_b =
3416 CylindricalSurface::new(Point3::new(5.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 0.5)
3417 .unwrap();
3418
3419 let curves = intersect_analytic_analytic(
3420 AnalyticSurface::Cylinder(&cyl_a),
3421 AnalyticSurface::Cylinder(&cyl_b),
3422 16,
3423 )
3424 .unwrap();
3425
3426 assert!(curves.is_empty(), "disjoint cylinders should not intersect");
3427 }
3428
3429 fn collect_points(curve: &ExactIntersectionCurve) -> Vec<Point3> {
3433 use crate::traits::ParametricCurve;
3434 match curve {
3435 ExactIntersectionCurve::Circle(c) => (0..=64)
3436 .map(|i| ParametricCurve::evaluate(c, TAU * f64::from(i) / 64.0))
3437 .collect(),
3438 ExactIntersectionCurve::Ellipse(e) => (0..=64)
3439 .map(|i| ParametricCurve::evaluate(e, TAU * f64::from(i) / 64.0))
3440 .collect(),
3441 ExactIntersectionCurve::Points(pts) => pts.clone(),
3442 }
3443 }
3444
3445 fn assert_on_plane_and_cone(
3448 curves: &[ExactIntersectionCurve],
3449 cone: &ConicalSurface,
3450 n: Vec3,
3451 d: f64,
3452 z_bound: (f64, f64),
3453 ) {
3454 assert!(!curves.is_empty(), "expected at least one section curve");
3455 let mut total = 0;
3456 for curve in curves {
3457 for p in collect_points(curve) {
3458 total += 1;
3459 let plane_err = (n.x() * p.x() + n.y() * p.y() + n.z() * p.z() - d).abs();
3460 assert!(
3461 plane_err < 1e-9,
3462 "point off plane by {plane_err:.2e}: {p:?}"
3463 );
3464 let (u, v) = cone.project_point(p);
3465 let q = cone.evaluate(u, v);
3466 let cone_err =
3467 ((p.x() - q.x()).powi(2) + (p.y() - q.y()).powi(2) + (p.z() - q.z()).powi(2))
3468 .sqrt();
3469 assert!(cone_err < 1e-7, "point off cone by {cone_err:.2e}: {p:?}");
3470 assert!(v >= -1e-9, "point on phantom nappe (v={v:.4}): {p:?}");
3471 assert!(
3472 p.z() >= z_bound.0 - 1e-6 && p.z() <= z_bound.1 + 1e-6,
3473 "point z={:.4} outside sane bound {z_bound:?}: {p:?}",
3474 p.z()
3475 );
3476 }
3477 }
3478 assert!(total >= 8, "too few section points ({total})");
3479 }
3480
3481 #[test]
3482 fn oblique_plane_cone_ellipse_is_exact_and_on_both() {
3483 let cone = ConicalSurface::new(
3487 Point3::new(0.0, 0.0, 0.0),
3488 Vec3::new(0.0, 0.0, 1.0),
3489 std::f64::consts::FRAC_PI_4,
3490 )
3491 .unwrap();
3492 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
3493 let d = n.z() * 5.0;
3495 let curves = exact_plane_cone(&cone, n, d).unwrap();
3496 assert!(
3497 curves
3498 .iter()
3499 .any(|c| matches!(c, ExactIntersectionCurve::Ellipse(_))),
3500 "oblique steep plane × cone must yield an exact Ellipse"
3501 );
3502 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 12.0));
3504 }
3505
3506 #[test]
3507 fn oblique_plane_cone_wrong_nappe_is_empty() {
3508 let cone = ConicalSurface::new(
3512 Point3::new(0.0, 0.0, 0.0),
3513 Vec3::new(0.0, 0.0, 1.0),
3514 std::f64::consts::FRAC_PI_4,
3515 )
3516 .unwrap();
3517 let n = Vec3::new(0.3, 0.0, 1.0).normalize().unwrap();
3518 let d = n.z() * -5.0;
3519 let curves = exact_plane_cone(&cone, n, d).unwrap();
3520 assert!(
3521 curves.is_empty(),
3522 "plane on the phantom-nappe side must yield no real curve, got {}",
3523 curves.len()
3524 );
3525 }
3526
3527 #[test]
3528 fn oblique_plane_cone_parabola_on_both_single_branch() {
3529 let cone = ConicalSurface::new(
3532 Point3::new(0.0, 0.0, 0.0),
3533 Vec3::new(0.0, 0.0, 1.0),
3534 std::f64::consts::FRAC_PI_4,
3535 )
3536 .unwrap();
3537 let n = Vec3::new(1.0, 0.0, 1.0).normalize().unwrap();
3538 let d = n.x() * 3.0 + n.z() * 3.0; let curves = exact_plane_cone(&cone, n, d).unwrap();
3540 assert_eq!(
3541 curves.len(),
3542 1,
3543 "a parabola is a single branch, got {}",
3544 curves.len()
3545 );
3546 assert_on_plane_and_cone(&curves, &cone, n, d, (0.0, 400.0));
3548 }
3549
3550 #[test]
3551 fn oblique_plane_cone_hyperbola_real_nappe_only() {
3552 let cone = ConicalSurface::new(
3560 Point3::new(-59.0, -59.0, 15.85),
3561 Vec3::new(0.0, 0.0, -1.0),
3562 std::f64::consts::FRAC_PI_4,
3563 )
3564 .unwrap();
3565 let n = Vec3::new(0.0, 0.995_18, 0.098_02).normalize().unwrap();
3566 let d = -58.360_56;
3567 let cos_theta = n.dot(cone.axis()).abs();
3568 assert!(cos_theta < 0.2, "expected a shallow (hyperbola) plane");
3569 let curves = exact_plane_cone(&cone, n, d).unwrap();
3570 assert_on_plane_and_cone(&curves, &cone, n, d, (5.0, 15.85));
3573 for c in &curves {
3575 assert!(
3576 matches!(c, ExactIntersectionCurve::Points(_)),
3577 "hyperbola must be sampled Points, not a closed conic"
3578 );
3579 }
3580 }
3581}