1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
31use ogeom_geom::{Curve, SurfaceGeometry};
32use ogeom_math::{Circle, Direction, Ellipse, Frame, Point, Vector};
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum Meeting {
37 Apart,
39 Touching(Vec<Point>),
45 Along(Vec<Curve>),
47 Same,
54}
55
56pub fn surface_surface(
64 a: &SurfaceGeometry,
65 b: &SurfaceGeometry,
66 tol: Tolerances,
67) -> OgeomResult<Meeting> {
68 use SurfaceGeometry as S;
69 match (a, b) {
70 (S::Plane(p), S::Plane(q)) => Ok(plane_plane(
71 p.plane(),
72 q.plane(),
73 window_reach(p).min(window_reach(q)),
74 tol,
75 )),
76 (S::Plane(p), S::Sphere(s)) => Ok(plane_sphere(p.plane(), s.sphere(), tol)),
77 (S::Sphere(s), S::Plane(p)) => Ok(plane_sphere(p.plane(), s.sphere(), tol)),
78 (S::Plane(p), S::Cylinder(c)) => plane_cylinder(p.plane(), c.cylinder(), tol),
79 (S::Cylinder(c), S::Plane(p)) => plane_cylinder(p.plane(), c.cylinder(), tol),
80 (S::Sphere(x), S::Sphere(y)) => Ok(sphere_sphere(x.sphere(), y.sphere(), tol)),
81 (S::Cylinder(x), S::Cylinder(y)) => coaxial_cylinders(x.cylinder(), y.cylinder(), tol),
82 (S::Cylinder(c), S::Sphere(s)) => coaxial_cylinder_sphere(c.cylinder(), s.sphere(), tol),
83 (S::Sphere(s), S::Cylinder(c)) => coaxial_cylinder_sphere(c.cylinder(), s.sphere(), tol),
84 (S::Plane(p), S::Torus(t)) => axial_plane_torus(p.plane(), t.torus(), tol),
85 (S::Torus(t), S::Plane(p)) => axial_plane_torus(p.plane(), t.torus(), tol),
86 (S::Cylinder(c), S::Torus(t)) => coaxial_cylinder_torus(c.cylinder(), t.torus(), tol),
87 (S::Torus(t), S::Cylinder(c)) => coaxial_cylinder_torus(c.cylinder(), t.torus(), tol),
88 (S::Torus(x), S::Torus(y)) => coaxial_tori(x.torus(), y.torus(), tol),
89 (S::Sphere(s), S::Torus(t)) => axial_sphere_torus(s.sphere(), t.torus(), tol),
90 (S::Torus(t), S::Sphere(s)) => axial_sphere_torus(s.sphere(), t.torus(), tol),
91 (S::Plane(p), S::Cone(c)) => plane_cone(p.plane(), c.cone(), heights(c), tol),
92 (S::Cone(c), S::Plane(p)) => plane_cone(p.plane(), c.cone(), heights(c), tol),
93 (S::Cylinder(x), S::Cone(c)) => coaxial_cylinder_cone(x.cylinder(), c.cone(), tol),
94 (S::Cone(c), S::Cylinder(x)) => coaxial_cylinder_cone(x.cylinder(), c.cone(), tol),
95 (S::Cone(x), S::Cone(y)) => coaxial_cones(x.cone(), y.cone(), tol),
96 _ => ogeom_bail!(
97 NotDone,
98 "this pair of surfaces has no closed-form intersection; it needs \
99 the general marching intersector, which is gated on the benchmark \
100 these cases provide the ground truth for"
101 ),
102 }
103}
104
105fn window_reach(plane: &ogeom_geom::PlaneSurface) -> f64 {
109 let ((u0, u1), (v0, v1)) = ogeom_geom::Surface::domain(plane);
110 let reach = (u1 - u0).hypot(v1 - v0);
111 if reach.is_finite() && reach > 0.0 {
112 reach.min(1e9)
113 } else {
114 1e9
115 }
116}
117
118fn square_to_axis(along: f64, tol: Tolerances) -> bool {
129 (along.abs() - 1.0).abs() <= tol.angular()
130}
131
132fn plane_plane(a: ogeom_math::Plane, b: ogeom_math::Plane, reach: f64, tol: Tolerances) -> Meeting {
142 let turn = a.normal().angle(b.normal());
143 let turn = turn.min(core::f64::consts::PI - turn);
144 if turn <= tol.angular().max(tol.confusion() / reach) {
145 return if a.distance_to(b.origin()) <= tol.confusion() {
148 Meeting::Same
149 } else {
150 Meeting::Apart
151 };
152 }
153 let Ok(direction) = Direction::from_cross(a.normal().vector(), b.normal().vector(), tol) else {
162 return Meeting::Apart;
163 };
164 let (na, nb) = (a.normal().vector(), b.normal().vector());
165 let dot = na.dot(nb);
166 let denominator = na.cross(nb).square_magnitude();
169 if denominator <= 0.0 {
170 return Meeting::Apart;
171 }
172 let origin = a.origin();
173 let off = b.signed_distance_to(origin);
174 let through = origin + (na * (off * dot) - nb * off) / denominator;
175 Meeting::Along(vec![line_through(through, direction)])
176}
177
178fn plane_sphere(plane: ogeom_math::Plane, sphere: ogeom_math::Sphere, tol: Tolerances) -> Meeting {
180 let gap = plane.signed_distance_to(sphere.centre());
181 let reach = gap.abs();
182 if reach > sphere.radius() + tol.confusion() {
183 return Meeting::Apart;
184 }
185 let foot = plane.project(sphere.centre());
186 if (reach - sphere.radius()).abs() <= tol.confusion() {
187 return Meeting::Touching(vec![foot]);
188 }
189 let radius = sphere
192 .radius()
193 .mul_add(sphere.radius(), -(gap * gap))
194 .max(0.0)
195 .sqrt();
196 match circle_on(foot, plane.normal(), radius, tol) {
197 Some(circle) => Meeting::Along(vec![circle]),
198 None => Meeting::Touching(vec![foot]),
199 }
200}
201
202fn plane_cylinder(
208 plane: ogeom_math::Plane,
209 cylinder: ogeom_math::Cylinder,
210 tol: Tolerances,
211) -> OgeomResult<Meeting> {
212 let axis = cylinder.axis();
213 let along = plane.normal().dot(axis.direction);
214
215 if along.abs() <= tol.angular() {
218 let gap = plane.signed_distance_to(axis.location);
219 let reach = gap.abs();
220 if reach > cylinder.radius() + tol.confusion() {
221 return Ok(Meeting::Apart);
222 }
223 let offset = cylinder
225 .radius()
226 .mul_add(cylinder.radius(), -(gap * gap))
227 .max(0.0)
228 .sqrt();
229 let foot = plane.project(axis.location);
230 let sideways =
231 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
232 if offset <= tol.confusion() {
233 return Ok(Meeting::Along(vec![line_through(foot, axis.direction)]));
235 }
236 return Ok(Meeting::Along(vec![
237 line_through(foot + sideways.vector() * offset, axis.direction),
238 line_through(foot - sideways.vector() * offset, axis.direction),
239 ]));
240 }
241
242 let centre = intersect_axis_plane(axis, plane, tol)?;
244 if square_to_axis(along, tol) {
245 return Ok(
246 match circle_on(centre, plane.normal(), cylinder.radius(), tol) {
247 Some(circle) => Meeting::Along(vec![circle]),
248 None => Meeting::Apart,
249 },
250 );
251 }
252
253 let minor = cylinder.radius();
256 let major = minor / along.abs();
257 let minor_direction =
260 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
261 let major_direction =
262 Direction::from_cross(minor_direction.vector(), plane.normal().vector(), tol)?;
263 let frame = Frame::from_axes(
264 centre,
265 major_direction,
266 minor_direction,
267 plane.normal(),
268 tol,
269 )?;
270 Ok(Meeting::Along(vec![
271 ogeom_geom::EllipseCurve::new(Ellipse::new(frame, major, minor, tol)?).into(),
272 ]))
273}
274
275fn sphere_sphere(a: ogeom_math::Sphere, b: ogeom_math::Sphere, tol: Tolerances) -> Meeting {
277 let between = b.centre() - a.centre();
278 let distance = between.magnitude();
279 if distance <= tol.confusion() {
280 return if (a.radius() - b.radius()).abs() <= tol.confusion() {
281 Meeting::Same
282 } else {
283 Meeting::Apart
285 };
286 }
287 let (ra, rb) = (a.radius(), b.radius());
288 if distance > ra + rb + tol.confusion() || distance < (ra - rb).abs() - tol.confusion() {
289 return Meeting::Apart;
290 }
291 let Ok(direction) = Direction::new(between, tol) else {
292 return Meeting::Apart;
293 };
294 let reach = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
296 let centre = a.centre() + direction.vector() * reach;
297 let squared = ra.mul_add(ra, -(reach * reach));
298 if squared <= tol.confusion() * tol.confusion() {
299 return Meeting::Touching(vec![centre]);
300 }
301 match circle_on(centre, direction, squared.max(0.0).sqrt(), tol) {
302 Some(circle) => Meeting::Along(vec![circle]),
303 None => Meeting::Touching(vec![centre]),
304 }
305}
306
307fn coaxial_cylinders(
313 a: ogeom_math::Cylinder,
314 b: ogeom_math::Cylinder,
315 tol: Tolerances,
316) -> OgeomResult<Meeting> {
317 if !a.axis().is_coaxial(b.axis(), tol) {
318 if (a.radius() - b.radius()).abs() <= tol.confusion() {
325 let (da, db) = (a.axis().direction.vector(), b.axis().direction.vector());
326 let normal = da.cross(db);
327 if normal.magnitude() > tol.angular() {
328 let (pa, pb) = (a.axis().location, b.axis().location);
329 let w = pb - pa;
332 let dd = da.dot(db);
333 let denom = dd.mul_add(-dd, 1.0);
334 let s = dd.mul_add(-db.dot(w), da.dot(w)) / denom;
335 let t = dd.mul_add(da.dot(w), -db.dot(w)) / denom;
336 let on_a = pa + da * s;
337 let on_b = pb + db * t;
338 if on_a.distance(on_b) <= tol.confusion() {
339 let centre = on_a;
340 let mut curves = Vec::new();
341 for m in [da - db, da + db] {
342 if m.magnitude() <= tol.angular() {
343 continue;
344 }
345 let plane =
346 ogeom_math::Plane::through(centre, ogeom_math::Direction::new(m, tol)?);
347 if let Meeting::Along(mut found) = plane_cylinder(plane, a, tol)? {
348 curves.append(&mut found);
349 }
350 }
351 if !curves.is_empty() {
352 return Ok(Meeting::Along(curves));
353 }
354 }
355 }
356 }
357 ogeom_bail!(
358 NotDone,
359 "two cylinders that do not share an axis meet in a quartic space \
360 curve, which needs the general marching intersector"
361 );
362 }
363 Ok(if (a.radius() - b.radius()).abs() <= tol.confusion() {
364 Meeting::Same
365 } else {
366 Meeting::Apart
368 })
369}
370
371fn coaxial_cylinder_sphere(
373 cylinder: ogeom_math::Cylinder,
374 sphere: ogeom_math::Sphere,
375 tol: Tolerances,
376) -> OgeomResult<Meeting> {
377 let axis = cylinder.axis();
378 if axis.distance_to(sphere.centre()) > tol.confusion() {
379 ogeom_bail!(
380 NotDone,
381 "a sphere off a cylinder's axis meets it in a quartic space curve, \
382 which needs the general marching intersector"
383 );
384 }
385 let (r, radius) = (cylinder.radius(), sphere.radius());
386 if r > radius + tol.confusion() {
387 return Ok(Meeting::Apart);
388 }
389 if (r - radius).abs() <= tol.confusion() {
390 let centre = sphere.centre();
393 return Ok(match circle_on(centre, axis.direction, r, tol) {
394 Some(circle) => Meeting::Along(vec![circle]),
395 None => Meeting::Apart,
396 });
397 }
398 let reach = radius.mul_add(radius, -(r * r)).max(0.0).sqrt();
400 let mut out = Vec::with_capacity(2);
401 for side in [reach, -reach] {
402 let centre = sphere.centre() + axis.direction.vector() * side;
403 if let Some(circle) = circle_on(centre, axis.direction, r, tol) {
404 out.push(circle);
405 }
406 }
407 Ok(if out.is_empty() {
408 Meeting::Apart
409 } else {
410 Meeting::Along(out)
411 })
412}
413
414fn axial_plane_torus(
428 plane: ogeom_math::Plane,
429 torus: ogeom_math::Torus,
430 tol: Tolerances,
431) -> OgeomResult<Meeting> {
432 let axis = torus.axis();
433 let along = plane.normal().dot(axis.direction);
434 if along.abs() <= tol.angular()
435 && plane.signed_distance_to(axis.location).abs() <= tol.confusion()
436 {
437 return Ok(meridians(plane, torus, tol));
438 }
439 if !square_to_axis(along, tol) {
440 ogeom_bail!(
441 NotDone,
442 "a plane oblique to a torus's axis, or parallel to it and off it, \
443 meets it in a quartic, which needs the general marching \
444 intersector"
445 );
446 }
447 let height = -plane.signed_distance_to(axis.location) * along.signum();
449 let minor = torus.minor_radius();
450 if height.abs() > minor + tol.confusion() {
451 return Ok(Meeting::Apart);
452 }
453 let centre = axis.location + axis.direction.vector() * height;
454 if (height.abs() - minor).abs() <= tol.confusion() {
455 return Ok(
457 match circle_on(centre, axis.direction, torus.major_radius(), tol) {
458 Some(circle) => Meeting::Along(vec![circle]),
459 None => Meeting::Apart,
460 },
461 );
462 }
463 let spread = minor.mul_add(minor, -(height * height)).max(0.0).sqrt();
467 let circles: Vec<Curve> = [
468 torus.major_radius() + spread,
469 (torus.major_radius() - spread).abs(),
470 ]
471 .into_iter()
472 .filter_map(|radius| circle_on(centre, axis.direction, radius, tol))
473 .collect();
474 Ok(if circles.is_empty() {
475 Meeting::Apart
476 } else {
477 Meeting::Along(circles)
478 })
479}
480
481fn meridians(plane: ogeom_math::Plane, torus: ogeom_math::Torus, tol: Tolerances) -> Meeting {
485 let axis = torus.axis();
486 let normal = plane.normal();
487 let Ok(out) = Direction::from_cross(axis.direction.vector(), normal.vector(), tol) else {
488 return Meeting::Apart;
489 };
490 let circles: Vec<Curve> = [out.vector(), -out.vector()]
491 .into_iter()
492 .filter_map(|radial| {
493 let centre = axis.location + radial * torus.major_radius();
494 let x = Direction::new(radial, tol).ok()?;
495 let frame = Frame::new(centre, normal, x, tol).ok()?;
496 let circle = Circle::new(frame, torus.minor_radius(), tol).ok()?;
497 Some(ogeom_geom::CircleCurve::new(circle).into())
498 })
499 .collect();
500 Meeting::Along(circles)
501}
502
503fn coaxial_cylinder_torus(
506 cylinder: ogeom_math::Cylinder,
507 torus: ogeom_math::Torus,
508 tol: Tolerances,
509) -> OgeomResult<Meeting> {
510 if !cylinder.axis().is_coaxial(torus.axis(), tol) {
511 ogeom_bail!(
512 NotDone,
513 "a cylinder off a torus's axis meets it in a quartic space curve, \
514 which needs the general marching intersector"
515 );
516 }
517 let axis = torus.axis();
518 let minor = torus.minor_radius();
519 let mut heights: Vec<f64> = Vec::new();
524 for reach in [
525 (cylinder.radius() - torus.major_radius()).abs(),
526 cylinder.radius() + torus.major_radius(),
527 ] {
528 if reach > minor + tol.confusion() {
529 continue;
530 }
531 if (reach - minor).abs() <= tol.confusion() {
532 heights.push(0.0);
533 continue;
534 }
535 let rise = minor.mul_add(minor, -(reach * reach)).max(0.0).sqrt();
536 heights.extend([rise, -rise]);
537 }
538 let mut kept: Vec<f64> = Vec::new();
539 for height in heights {
540 if kept.iter().all(|k| (k - height).abs() > tol.confusion()) {
541 kept.push(height);
542 }
543 }
544 let circles: Vec<Curve> = kept
545 .into_iter()
546 .filter_map(|height| {
547 circle_on(
548 axis.location + axis.direction.vector() * height,
549 axis.direction,
550 cylinder.radius(),
551 tol,
552 )
553 })
554 .collect();
555 Ok(if circles.is_empty() {
556 Meeting::Apart
557 } else {
558 Meeting::Along(circles)
559 })
560}
561
562fn plane_cone(
571 plane: ogeom_math::Plane,
572 cone: ogeom_math::Cone,
573 heights: (f64, f64),
574 tol: Tolerances,
575) -> OgeomResult<Meeting> {
576 let axis = cone.axis();
577 let along = plane.normal().dot(axis.direction);
578 if !square_to_axis(along, tol) && plane.signed_distance_to(cone.apex()).abs() <= tol.confusion()
579 {
580 return Ok(rulings_in(plane, &cone, heights, tol));
581 }
582 if !square_to_axis(along, tol) {
583 ogeom_bail!(
584 NotDone,
585 "a plane oblique to a cone's axis meets it in a conic, which \
586 needs the general marching intersector"
587 );
588 }
589 let height = -plane.signed_distance_to(axis.location) * along.signum();
591 let radius = cone.radius_at(height);
592 if radius.abs() <= tol.confusion() {
593 return Ok(Meeting::Touching(vec![cone.apex()]));
596 }
597 let centre = axis.location + axis.direction.vector() * height;
601 Ok(match cone_parallel(&cone, centre, radius.abs(), tol) {
602 Some(circle) => Meeting::Along(vec![circle]),
603 None => Meeting::Apart,
604 })
605}
606
607fn heights(cone: &ogeom_geom::ConeSurface) -> (f64, f64) {
609 ogeom_geom::Surface::domain(cone).1
610}
611
612fn rulings_in(
618 plane: ogeom_math::Plane,
619 cone: &ogeom_math::Cone,
620 heights: (f64, f64),
621 tol: Tolerances,
622) -> Meeting {
623 let apex = cone.apex();
624 let apex_height = cone.frame().to_local(apex).z;
625 let stated = |direction: Direction| -> Vec<Curve> {
628 let climb = direction.vector().dot(cone.axis().direction.vector());
629 if climb.abs() <= tol.angular() {
630 return Vec::new();
631 }
632 let (lo, hi) = (heights.0.min(heights.1), heights.0.max(heights.1));
633 let mut out = Vec::new();
634 for (from, to) in [(lo.max(apex_height), hi), (lo, hi.min(apex_height))] {
635 if !(from.is_finite() && to.is_finite()) || to - from <= tol.confusion() {
636 continue;
637 }
638 let (a, b) = ((from - apex_height) / climb, (to - apex_height) / climb);
639 if let Ok(line) = ogeom_geom::LineCurve::over(
640 ogeom_math::Axis::new(apex, direction),
641 a.min(b),
642 a.max(b),
643 ) {
644 out.push(line.into());
645 }
646 }
647 out
648 };
649 let a = cone.axis().direction.vector();
650 let n = plane.normal().vector();
651 let half = cone.half_angle().abs();
652 let across = n.dot(a);
653 let lean = across.abs().clamp(0.0, 1.0).asin() - half;
656 if lean.abs() <= 1e-10 {
657 let toward = a - n * across;
660 return match Direction::new(toward, tol).map(stated) {
661 Ok(lines) if !lines.is_empty() => Meeting::Along(lines),
662 _ => Meeting::Touching(vec![apex]),
663 };
664 }
665 if lean > 0.0 {
666 return Meeting::Touching(vec![apex]);
667 }
668 let side = n - a * across;
671 let Ok(e) = Direction::new(side, tol) else {
672 return Meeting::Touching(vec![apex]);
673 };
674 let e = e.vector();
675 let f = a.cross(e);
676 let (sin, cos) = half.sin_cos();
677 let k = (-across * cos / (side.magnitude() * sin)).clamp(-1.0, 1.0);
678 let s = (1.0 - k * k).max(0.0).sqrt();
679 let lines: Vec<Curve> = [s, -s]
680 .into_iter()
681 .filter_map(|t| {
682 let d = a * cos + (e * k + f * t) * sin;
683 Direction::new(d, tol).ok()
684 })
685 .flat_map(stated)
686 .collect();
687 if lines.is_empty() {
688 Meeting::Touching(vec![apex])
689 } else {
690 Meeting::Along(lines)
691 }
692}
693
694fn coaxial_cylinder_cone(
704 cylinder: ogeom_math::Cylinder,
705 cone: ogeom_math::Cone,
706 tol: Tolerances,
707) -> OgeomResult<Meeting> {
708 if !cylinder.axis().is_coaxial(cone.axis(), tol) {
709 ogeom_bail!(
710 NotDone,
711 "a cylinder off a cone's axis meets it in a curve only the \
712 general marching intersector can trace"
713 );
714 }
715 let axis = cone.axis();
716 let slope = cone.half_angle().tan();
717 let circles: Vec<Curve> = [cylinder.radius(), -cylinder.radius()]
718 .into_iter()
719 .filter_map(|radius| {
720 let height = (radius - cone.reference_radius()) / slope;
721 cone_parallel(
722 &cone,
723 axis.location + axis.direction.vector() * height,
724 cylinder.radius(),
725 tol,
726 )
727 })
728 .collect();
729 Ok(if circles.is_empty() {
730 Meeting::Apart
731 } else {
732 Meeting::Along(circles)
733 })
734}
735
736fn coaxial_cones(
743 a: ogeom_math::Cone,
744 b: ogeom_math::Cone,
745 tol: Tolerances,
746) -> OgeomResult<Meeting> {
747 if !a.axis().is_coaxial(b.axis(), tol) {
748 ogeom_bail!(
749 NotDone,
750 "two cones that do not share an axis meet in a curve only the \
751 general marching intersector can trace"
752 );
753 }
754 let axis = a.axis();
755 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
758 let (slope_a, slope_b) = (a.half_angle().tan(), b.half_angle().tan());
759 let (ref_a, ref_b) = (
760 a.reference_radius(),
761 slope_b.mul_add(-lift, b.reference_radius()),
762 );
763 if (slope_a - slope_b).abs() <= tol.angular() && (ref_a - ref_b).abs() <= tol.confusion() {
764 return Ok(Meeting::Same);
765 }
766 let mut heights: Vec<f64> = Vec::new();
770 if (slope_a - slope_b).abs() > tol.angular() {
771 heights.push((ref_b - ref_a) / (slope_a - slope_b));
772 }
773 if (slope_a + slope_b).abs() > tol.angular() {
774 heights.push(-(ref_a + ref_b) / (slope_a + slope_b));
775 }
776 let mut circles: Vec<Curve> = Vec::new();
777 let mut touches: Vec<Point> = Vec::new();
778 for height in heights {
779 let radius = a.radius_at(height).abs();
780 let centre = axis.location + axis.direction.vector() * height;
781 if radius <= tol.confusion() {
782 if touches.iter().all(|t| t.distance(centre) > tol.confusion()) {
783 touches.push(centre);
784 }
785 continue;
786 }
787 let repeated = circles.iter().any(|c| {
788 matches!(c, Curve::Circle(k) if k.circle().centre().distance(centre) <= tol.confusion())
789 });
790 if !repeated && let Some(circle) = cone_parallel(&a, centre, radius, tol) {
791 circles.push(circle);
792 }
793 }
794 Ok(if !circles.is_empty() {
795 Meeting::Along(circles)
796 } else if !touches.is_empty() {
797 Meeting::Touching(touches)
798 } else {
799 Meeting::Apart
800 })
801}
802
803fn cone_parallel(
805 cone: &ogeom_math::Cone,
806 centre: Point,
807 radius: f64,
808 tol: Tolerances,
809) -> Option<Curve> {
810 if radius <= tol.confusion() {
811 return None;
812 }
813 let frame = cone.frame();
814 let placed = Frame::new(centre, frame.z(), frame.x(), tol).ok()?;
815 Some(ogeom_geom::CircleCurve::new(Circle::new(placed, radius, tol).ok()?).into())
816}
817
818fn coaxial_tori(
825 a: ogeom_math::Torus,
826 b: ogeom_math::Torus,
827 tol: Tolerances,
828) -> OgeomResult<Meeting> {
829 if !a.axis().is_coaxial(b.axis(), tol) {
830 ogeom_bail!(
831 NotDone,
832 "two tori that do not share an axis meet in a curve only the \
833 general marching intersector can trace"
834 );
835 }
836 let axis = a.axis();
837 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
838 if (a.major_radius() - b.major_radius()).abs() <= tol.confusion()
839 && lift.abs() <= tol.confusion()
840 && (a.minor_radius() - b.minor_radius()).abs() <= tol.confusion()
841 {
842 return Ok(Meeting::Same);
843 }
844 let (ra, rb) = (a.minor_radius(), b.minor_radius());
849 let profile_points = profile_meetings(
850 &[
851 (ogeom_math::Point2::new(a.major_radius(), 0.0), ra),
852 (ogeom_math::Point2::new(-a.major_radius(), 0.0), ra),
853 ],
854 &[
855 (ogeom_math::Point2::new(b.major_radius(), lift), rb),
856 (ogeom_math::Point2::new(-b.major_radius(), lift), rb),
857 ],
858 tol,
859 );
860 let circles: Vec<Curve> = profile_points
861 .into_iter()
862 .filter_map(|p| {
863 circle_on(
864 axis.location + axis.direction.vector() * p.y,
865 axis.direction,
866 p.x,
867 tol,
868 )
869 })
870 .collect();
871 Ok(if circles.is_empty() {
872 Meeting::Apart
873 } else {
874 Meeting::Along(circles)
875 })
876}
877
878fn profile_meetings(
883 a: &[(ogeom_math::Point2, f64)],
884 b: &[(ogeom_math::Point2, f64)],
885 tol: Tolerances,
886) -> Vec<ogeom_math::Point2> {
887 let mut points: Vec<ogeom_math::Point2> = Vec::new();
888 for &(ca, ra) in a {
889 for &(cb, rb) in b {
890 let between = cb - ca;
891 let distance = between.magnitude();
892 if distance <= tol.confusion()
895 || distance > ra + rb + tol.confusion()
896 || distance < (ra - rb).abs() - tol.confusion()
897 {
898 continue;
899 }
900 let along = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
901 let squared = ra.mul_add(ra, -(along * along));
902 let direction = between * (1.0 / distance);
903 let foot = ca + direction * along;
904 let found = if squared <= tol.confusion() * tol.confusion() {
905 vec![foot]
906 } else {
907 let offset =
908 ogeom_math::Vector2::new(-direction.y, direction.x) * squared.max(0.0).sqrt();
909 vec![foot + offset, foot - offset]
910 };
911 for p in found {
912 if p.x > tol.confusion() && points.iter().all(|q| q.distance(p) > tol.confusion()) {
913 points.push(p);
914 }
915 }
916 }
917 }
918 points
919}
920
921fn axial_sphere_torus(
927 sphere: ogeom_math::Sphere,
928 torus: ogeom_math::Torus,
929 tol: Tolerances,
930) -> OgeomResult<Meeting> {
931 let axis = torus.axis();
932 let lift = (sphere.centre() - axis.location).dot(axis.direction.vector());
933 let foot = axis.location + axis.direction.vector() * lift;
934 if foot.distance(sphere.centre()) > tol.confusion() {
935 ogeom_bail!(
936 NotDone,
937 "a sphere off a torus's axis meets it in a curve only the general \
938 marching intersector can trace"
939 );
940 }
941 let tube = torus.minor_radius();
942 let points = profile_meetings(
943 &[(ogeom_math::Point2::new(0.0, lift), sphere.radius())],
944 &[
945 (ogeom_math::Point2::new(torus.major_radius(), 0.0), tube),
946 (ogeom_math::Point2::new(-torus.major_radius(), 0.0), tube),
947 ],
948 tol,
949 );
950 let circles: Vec<Curve> = points
951 .into_iter()
952 .filter_map(|p| {
953 circle_on(
954 axis.location + axis.direction.vector() * p.y,
955 axis.direction,
956 p.x,
957 tol,
958 )
959 })
960 .collect();
961 Ok(if circles.is_empty() {
962 Meeting::Apart
963 } else {
964 Meeting::Along(circles)
965 })
966}
967
968fn intersect_axis_plane(
970 axis: ogeom_math::Axis,
971 plane: ogeom_math::Plane,
972 tol: Tolerances,
973) -> OgeomResult<Point> {
974 let along = plane.normal().dot(axis.direction);
975 if along.abs() <= tol.angular() {
976 ogeom_bail!(Domain, "the axis runs along the plane and never crosses it");
977 }
978 let t = -plane.signed_distance_to(axis.location) / along;
979 Ok(axis.location + axis.direction.vector() * t)
980}
981
982fn circle_on(centre: Point, normal: Direction, radius: f64, tol: Tolerances) -> Option<Curve> {
984 if radius <= tol.confusion() {
985 return None;
986 }
987 let reference = if normal.vector().cross(Vector::X).magnitude() > 0.5 {
989 Vector::X
990 } else {
991 Vector::Y
992 };
993 let x = Direction::from_cross(normal.vector(), reference, tol).ok()?;
994 let frame = Frame::new(centre, normal, x, tol).ok()?;
995 Some(ogeom_geom::CircleCurve::new(Circle::new(frame, radius, tol).ok()?).into())
996}
997
998fn line_through(through: Point, direction: Direction) -> Curve {
1000 ogeom_geom::LineCurve::new(ogeom_math::Axis::new(through, direction)).into()
1001}