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)) => cylinder_sphere(c, s.sphere(), tol),
83 (S::Sphere(s), S::Cylinder(c)) => cylinder_sphere(c, 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
202#[must_use]
217pub fn drum_onto_plane(
218 plane: &ogeom_math::Plane,
219 cylinder: &ogeom_math::Cylinder,
220 tol: Tolerances,
221) -> Option<Vector> {
222 let axis = cylinder.axis();
223 if plane.normal().dot(axis.direction).abs() > tol.angular() {
224 return None;
225 }
226 let gap = plane.signed_distance_to(axis.location);
227 let r = cylinder.radius();
228 let rounding = 4.0
231 * f64::EPSILON
232 * (axis.location.to_vector().magnitude() + plane.origin().to_vector().magnitude() + r);
233 if (gap.abs() - r).abs() > tol.confusion() + rounding {
234 return None;
235 }
236 Some(plane.normal().vector() * (r.copysign(gap) - gap))
237}
238
239fn plane_cylinder(
245 plane: ogeom_math::Plane,
246 cylinder: ogeom_math::Cylinder,
247 tol: Tolerances,
248) -> OgeomResult<Meeting> {
249 let axis = cylinder.axis();
250 let along = plane.normal().dot(axis.direction);
251
252 if along.abs() <= tol.angular() {
255 let gap = plane.signed_distance_to(axis.location);
256 let reach = gap.abs();
257 let foot = plane.project(axis.location);
258 if drum_onto_plane(&plane, &cylinder, tol).is_some() {
259 return Ok(Meeting::Along(vec![line_through(foot, axis.direction)]));
261 }
262 if reach > cylinder.radius() {
263 return Ok(Meeting::Apart);
264 }
265 let offset = cylinder
267 .radius()
268 .mul_add(cylinder.radius(), -(gap * gap))
269 .max(0.0)
270 .sqrt();
271 let sideways =
272 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
273 return Ok(Meeting::Along(vec![
274 line_through(foot + sideways.vector() * offset, axis.direction),
275 line_through(foot - sideways.vector() * offset, axis.direction),
276 ]));
277 }
278
279 let centre = intersect_axis_plane(axis, plane, tol)?;
281 if square_to_axis(along, tol) {
282 return Ok(
283 match circle_on(centre, plane.normal(), cylinder.radius(), tol) {
284 Some(circle) => Meeting::Along(vec![circle]),
285 None => Meeting::Apart,
286 },
287 );
288 }
289
290 let minor = cylinder.radius();
293 let major = minor / along.abs();
294 let minor_direction =
297 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
298 let major_direction =
299 Direction::from_cross(minor_direction.vector(), plane.normal().vector(), tol)?;
300 let frame = Frame::from_axes(
301 centre,
302 major_direction,
303 minor_direction,
304 plane.normal(),
305 tol,
306 )?;
307 Ok(Meeting::Along(vec![
308 ogeom_geom::EllipseCurve::new(Ellipse::new(frame, major, minor, tol)?).into(),
309 ]))
310}
311
312fn sphere_sphere(a: ogeom_math::Sphere, b: ogeom_math::Sphere, tol: Tolerances) -> Meeting {
314 let between = b.centre() - a.centre();
315 let distance = between.magnitude();
316 if distance <= tol.confusion() {
317 return if (a.radius() - b.radius()).abs() <= tol.confusion() {
318 Meeting::Same
319 } else {
320 Meeting::Apart
322 };
323 }
324 let (ra, rb) = (a.radius(), b.radius());
325 if distance > ra + rb + tol.confusion() || distance < (ra - rb).abs() - tol.confusion() {
326 return Meeting::Apart;
327 }
328 let Ok(direction) = Direction::new(between, tol) else {
329 return Meeting::Apart;
330 };
331 let reach = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
333 let centre = a.centre() + direction.vector() * reach;
334 let squared = ra.mul_add(ra, -(reach * reach));
335 if squared <= tol.confusion() * tol.confusion() {
336 return Meeting::Touching(vec![centre]);
337 }
338 match circle_on(centre, direction, squared.max(0.0).sqrt(), tol) {
339 Some(circle) => Meeting::Along(vec![circle]),
340 None => Meeting::Touching(vec![centre]),
341 }
342}
343
344fn coaxial_cylinders(
350 a: ogeom_math::Cylinder,
351 b: ogeom_math::Cylinder,
352 tol: Tolerances,
353) -> OgeomResult<Meeting> {
354 if !a.axis().is_coaxial(b.axis(), tol) {
355 if (a.radius() - b.radius()).abs() <= tol.confusion() {
362 let (da, db) = (a.axis().direction.vector(), b.axis().direction.vector());
363 let normal = da.cross(db);
364 if normal.magnitude() > tol.angular() {
365 let (pa, pb) = (a.axis().location, b.axis().location);
366 let w = pb - pa;
369 let dd = da.dot(db);
370 let denom = dd.mul_add(-dd, 1.0);
371 let s = dd.mul_add(-db.dot(w), da.dot(w)) / denom;
372 let t = dd.mul_add(da.dot(w), -db.dot(w)) / denom;
373 let on_a = pa + da * s;
374 let on_b = pb + db * t;
375 if on_a.distance(on_b) <= tol.confusion() {
376 let centre = on_a;
377 let mut curves = Vec::new();
378 for m in [da - db, da + db] {
379 if m.magnitude() <= tol.angular() {
380 continue;
381 }
382 let plane =
383 ogeom_math::Plane::through(centre, ogeom_math::Direction::new(m, tol)?);
384 if let Meeting::Along(mut found) = plane_cylinder(plane, a, tol)? {
385 curves.append(&mut found);
386 }
387 }
388 if !curves.is_empty() {
389 return Ok(Meeting::Along(curves));
390 }
391 }
392 }
393 }
394 ogeom_bail!(
395 NotDone,
396 "two cylinders that do not share an axis meet in a quartic space \
397 curve, which needs the general marching intersector"
398 );
399 }
400 Ok(if (a.radius() - b.radius()).abs() <= tol.confusion() {
401 Meeting::Same
402 } else {
403 Meeting::Apart
405 })
406}
407
408fn cylinder_sphere(
412 drum: &ogeom_geom::CylinderSurface,
413 sphere: ogeom_math::Sphere,
414 tol: Tolerances,
415) -> OgeomResult<Meeting> {
416 let cylinder = drum.cylinder();
417 if cylinder.axis().distance_to(sphere.centre()) <= tol.confusion() {
418 return coaxial_cylinder_sphere(cylinder, sphere, tol);
419 }
420 match sphere_beside_cylinder(drum, sphere, tol) {
421 Some(meeting) => Ok(meeting),
422 None => ogeom_bail!(
423 NotDone,
424 "a sphere off a cylinder's axis and crossing it meets it in a \
425 quartic space curve, which needs the general marching intersector"
426 ),
427 }
428}
429
430fn sphere_beside_cylinder(
442 drum: &ogeom_geom::CylinderSurface,
443 sphere: ogeom_math::Sphere,
444 tol: Tolerances,
445) -> Option<Meeting> {
446 let cylinder = drum.cylinder();
447 let axis = cylinder.axis();
448 let (centre, radius) = (sphere.centre(), sphere.radius());
449 let outward = centre - axis.project(centre);
450 let off = outward.magnitude();
451 let outward = Direction::new(outward, tol).ok()?;
452 let r = cylinder.radius();
453 let inner = r - (off + radius);
456 let outer = (off - radius) - r;
457 if inner > tol.confusion() || outer > tol.confusion() {
458 return Some(Meeting::Apart);
459 }
460 let touch = if inner.abs() <= tol.confusion() {
461 centre + outward.vector() * radius
462 } else if outer.abs() <= tol.confusion() {
463 centre - outward.vector() * radius
464 } else {
465 return None;
466 };
467 let (h0, h1) = ogeom_geom::Surface::domain(drum).1;
468 let height = (touch - axis.location).dot(axis.direction.vector());
469 Some(
470 if height < h0.min(h1) - tol.confusion() || height > h0.max(h1) + tol.confusion() {
471 Meeting::Apart
472 } else {
473 Meeting::Touching(vec![touch])
474 },
475 )
476}
477
478fn coaxial_cylinder_sphere(
480 cylinder: ogeom_math::Cylinder,
481 sphere: ogeom_math::Sphere,
482 tol: Tolerances,
483) -> OgeomResult<Meeting> {
484 let axis = cylinder.axis();
485 let (r, radius) = (cylinder.radius(), sphere.radius());
486 if r > radius + tol.confusion() {
487 return Ok(Meeting::Apart);
488 }
489 if (r - radius).abs() <= tol.confusion() {
490 let centre = sphere.centre();
493 return Ok(match circle_on(centre, axis.direction, r, tol) {
494 Some(circle) => Meeting::Along(vec![circle]),
495 None => Meeting::Apart,
496 });
497 }
498 let reach = radius.mul_add(radius, -(r * r)).max(0.0).sqrt();
500 let mut out = Vec::with_capacity(2);
501 for side in [reach, -reach] {
502 let centre = sphere.centre() + axis.direction.vector() * side;
503 if let Some(circle) = circle_on(centre, axis.direction, r, tol) {
504 out.push(circle);
505 }
506 }
507 Ok(if out.is_empty() {
508 Meeting::Apart
509 } else {
510 Meeting::Along(out)
511 })
512}
513
514fn axial_plane_torus(
530 plane: ogeom_math::Plane,
531 torus: ogeom_math::Torus,
532 tol: Tolerances,
533) -> OgeomResult<Meeting> {
534 let axis = torus.axis();
535 let along = plane.normal().dot(axis.direction);
536 if along.abs() <= tol.angular()
537 && plane.signed_distance_to(axis.location).abs() <= tol.confusion()
538 {
539 return Ok(meridians(plane, torus, tol));
540 }
541 if !square_to_axis(along, tol) {
542 if let Some(meeting) = plane_beside_torus(plane, torus, along, tol) {
543 return Ok(meeting);
544 }
545 ogeom_bail!(
546 NotDone,
547 "a plane oblique to a torus's axis, or parallel to it and off it, \
548 crossing it meets it in a quartic, which needs the general \
549 marching intersector"
550 );
551 }
552 let height = -plane.signed_distance_to(axis.location) * along.signum();
554 let minor = torus.minor_radius();
555 if height.abs() > minor + tol.confusion() {
556 return Ok(Meeting::Apart);
557 }
558 let centre = axis.location + axis.direction.vector() * height;
559 if (height.abs() - minor).abs() <= tol.confusion() {
560 return Ok(
562 match circle_on(centre, axis.direction, torus.major_radius(), tol) {
563 Some(circle) => Meeting::Along(vec![circle]),
564 None => Meeting::Apart,
565 },
566 );
567 }
568 let spread = minor.mul_add(minor, -(height * height)).max(0.0).sqrt();
572 let circles: Vec<Curve> = [
573 torus.major_radius() + spread,
574 (torus.major_radius() - spread).abs(),
575 ]
576 .into_iter()
577 .filter_map(|radius| circle_on(centre, axis.direction, radius, tol))
578 .collect();
579 Ok(if circles.is_empty() {
580 Meeting::Apart
581 } else {
582 Meeting::Along(circles)
583 })
584}
585
586fn plane_beside_torus(
601 plane: ogeom_math::Plane,
602 torus: ogeom_math::Torus,
603 along: f64,
604 tol: Tolerances,
605) -> Option<Meeting> {
606 let axis = torus.axis();
607 let normal = plane.normal().vector();
608 let across = normal - axis.direction.vector() * along;
609 let sine = across.magnitude();
610 let (major, minor) = (torus.major_radius(), torus.minor_radius());
611 if 2.0 * major * sine <= tol.confusion() {
612 return None;
613 }
614 let reach = major.mul_add(sine, minor);
615 let offset = plane.signed_distance_to(axis.location);
618 let gap = offset.abs() - reach;
619 if gap > tol.confusion() {
620 return Some(Meeting::Apart);
621 }
622 if gap.abs() > tol.confusion() {
623 return None;
624 }
625 let toward = -offset.signum();
628 let touch = axis.location + (across * (major / sine) + normal * minor) * toward;
629 Some(Meeting::Touching(vec![touch]))
630}
631
632fn meridians(plane: ogeom_math::Plane, torus: ogeom_math::Torus, tol: Tolerances) -> Meeting {
636 let axis = torus.axis();
637 let normal = plane.normal();
638 let Ok(out) = Direction::from_cross(axis.direction.vector(), normal.vector(), tol) else {
639 return Meeting::Apart;
640 };
641 let circles: Vec<Curve> = [out.vector(), -out.vector()]
642 .into_iter()
643 .filter_map(|radial| {
644 let centre = axis.location + radial * torus.major_radius();
645 let x = Direction::new(radial, tol).ok()?;
646 let frame = Frame::new(centre, normal, x, tol).ok()?;
647 let circle = Circle::new(frame, torus.minor_radius(), tol).ok()?;
648 Some(ogeom_geom::CircleCurve::new(circle).into())
649 })
650 .collect();
651 Meeting::Along(circles)
652}
653
654fn coaxial_cylinder_torus(
657 cylinder: ogeom_math::Cylinder,
658 torus: ogeom_math::Torus,
659 tol: Tolerances,
660) -> OgeomResult<Meeting> {
661 if !cylinder.axis().is_coaxial(torus.axis(), tol) {
662 ogeom_bail!(
663 NotDone,
664 "a cylinder off a torus's axis meets it in a quartic space curve, \
665 which needs the general marching intersector"
666 );
667 }
668 let axis = torus.axis();
669 let minor = torus.minor_radius();
670 let mut heights: Vec<f64> = Vec::new();
675 for reach in [
676 (cylinder.radius() - torus.major_radius()).abs(),
677 cylinder.radius() + torus.major_radius(),
678 ] {
679 if reach > minor + tol.confusion() {
680 continue;
681 }
682 if (reach - minor).abs() <= tol.confusion() {
683 heights.push(0.0);
684 continue;
685 }
686 let rise = minor.mul_add(minor, -(reach * reach)).max(0.0).sqrt();
687 heights.extend([rise, -rise]);
688 }
689 let mut kept: Vec<f64> = Vec::new();
690 for height in heights {
691 if kept.iter().all(|k| (k - height).abs() > tol.confusion()) {
692 kept.push(height);
693 }
694 }
695 let circles: Vec<Curve> = kept
696 .into_iter()
697 .filter_map(|height| {
698 circle_on(
699 axis.location + axis.direction.vector() * height,
700 axis.direction,
701 cylinder.radius(),
702 tol,
703 )
704 })
705 .collect();
706 Ok(if circles.is_empty() {
707 Meeting::Apart
708 } else {
709 Meeting::Along(circles)
710 })
711}
712
713fn plane_cone(
721 plane: ogeom_math::Plane,
722 cone: ogeom_math::Cone,
723 heights: (f64, f64),
724 tol: Tolerances,
725) -> OgeomResult<Meeting> {
726 let axis = cone.axis();
727 let along = plane.normal().dot(axis.direction);
728 if !square_to_axis(along, tol) && plane.signed_distance_to(cone.apex()).abs() <= tol.confusion()
729 {
730 return Ok(rulings_in(plane, &cone, heights, tol));
731 }
732 if !square_to_axis(along, tol) {
733 return conic_of(plane, &cone, heights, tol);
734 }
735 let height = -plane.signed_distance_to(axis.location) * along.signum();
737 let radius = cone.radius_at(height);
738 if radius.abs() <= tol.confusion() {
739 return Ok(Meeting::Touching(vec![cone.apex()]));
742 }
743 let centre = axis.location + axis.direction.vector() * height;
747 Ok(match cone_parallel(&cone, centre, radius.abs(), tol) {
748 Some(circle) => Meeting::Along(vec![circle]),
749 None => Meeting::Apart,
750 })
751}
752
753fn conic_of(
766 plane: ogeom_math::Plane,
767 cone: &ogeom_math::Cone,
768 heights: (f64, f64),
769 tol: Tolerances,
770) -> OgeomResult<Meeting> {
771 let a = cone.axis().direction.vector();
772 let n = plane.normal().vector();
773 let apex = cone.apex();
774 let half = cone.half_angle().abs();
775 let (sin, cos) = half.sin_cos();
776 let c1 = a.dot(n);
777 let Ok(e2) = Direction::new(a - n * c1, tol) else {
778 return Ok(Meeting::Apart);
779 };
780 let e2 = e2.vector();
781 let e1 = n.cross(e2);
782 let c2 = a.dot(e2);
783 let k = (plane.project(apex) - apex).dot(n);
784 let foot = apex + n * k;
785 let big_c = cos * cos;
786 let d = c1.mul_add(c1, -(sin * sin));
787 let apex_height = cone.frame().to_local(apex).z;
790 let (lo, hi) = (
791 heights.0.min(heights.1) - apex_height,
792 heights.0.max(heights.1) - apex_height,
793 );
794 let reach = lo.abs().max(hi.abs());
795 let on_window_nappe = |p: Point| {
796 let h = (p - apex).dot(a);
797 (h >= 0.0 && hi > tol.confusion()) || (h <= 0.0 && lo < -tol.confusion())
798 };
799 if !reach.is_finite() {
800 ogeom_bail!(NotDone, "an unbounded cone window carries no conic span");
801 }
802 let span = |at: &dyn Fn(f64) -> Point| -> f64 {
805 let mut t = 1.0_f64;
806 for _ in 0..64 {
807 let far = [at(t), at(-t)]
808 .iter()
809 .all(|p| (*p - apex).dot(a).abs() > reach + tol.confusion());
810 if far {
811 break;
812 }
813 t *= 2.0;
814 }
815 t
816 };
817 let frame = |origin: Point, x: Vector| -> OgeomResult<Frame> {
818 let x = Direction::new(x, tol)?;
819 Frame::new(origin, Direction::new(n, tol)?, x, tol)
820 };
821 let mut out: Vec<Curve> = Vec::new();
822 if d.abs() <= 1e-9 {
823 let q = big_c / (2.0 * k * c1 * c2);
825 if !q.is_finite() {
826 return Ok(Meeting::Apart);
827 }
828 let v_apex = (big_c - c1 * c1) * k / (2.0 * c1 * c2);
829 let vertex = foot + e2 * v_apex;
830 if !on_window_nappe(vertex) {
831 return Ok(Meeting::Apart);
832 }
833 let opening = if q > 0.0 { e2 } else { -e2 };
834 let parabola =
835 ogeom_math::Parabola::new(frame(vertex, opening)?, 1.0 / (4.0 * q.abs()), tol)?;
836 let extent = span(&|t| ogeom_math::elementary::parabola_at(¶bola, t).point);
837 out.push(ogeom_geom::ParabolaCurve::new(parabola, extent)?.into());
838 return Ok(Meeting::Along(out));
839 }
840 let v0 = k * c1 * c2 / d;
841 let r = d * v0 * v0 - (big_c - c1 * c1) * k * k;
842 let centre = foot + e2 * v0;
843 if d > 0.0 {
844 if r <= tol.confusion() * tol.confusion() {
845 return Ok(Meeting::Apart);
846 }
847 let (along_u, along_v) = ((r / big_c).sqrt(), (r / d).sqrt());
848 if !on_window_nappe(centre + e2 * along_v) {
849 return Ok(Meeting::Apart);
850 }
851 let ellipse = if along_v >= along_u {
852 Ellipse::new(frame(centre, e2)?, along_v, along_u, tol)?
853 } else {
854 Ellipse::new(frame(centre, e1)?, along_u, along_v, tol)?
855 };
856 out.push(ogeom_geom::EllipseCurve::new(ellipse).into());
857 return Ok(Meeting::Along(out));
858 }
859 let (major_dir, major, minor) = if r > 0.0 {
862 (e1, (r / big_c).sqrt(), (r / -d).sqrt())
863 } else {
864 (e2, (-r / -d).sqrt(), (-r / big_c).sqrt())
865 };
866 if !(major > tol.confusion() && minor > tol.confusion()) {
867 return Ok(Meeting::Touching(vec![apex]));
868 }
869 for toward in [major_dir, -major_dir] {
870 if !on_window_nappe(centre + toward * major) {
871 continue;
872 }
873 let hyperbola = ogeom_math::Hyperbola::new(frame(centre, toward)?, major, minor, tol)?;
874 let extent = span(&|t| ogeom_math::elementary::hyperbola_at(&hyperbola, t).point);
875 out.push(ogeom_geom::HyperbolaCurve::new(hyperbola, extent)?.into());
876 }
877 Ok(if out.is_empty() {
878 Meeting::Apart
879 } else {
880 Meeting::Along(out)
881 })
882}
883
884fn heights(cone: &ogeom_geom::ConeSurface) -> (f64, f64) {
886 ogeom_geom::Surface::domain(cone).1
887}
888
889fn rulings_in(
895 plane: ogeom_math::Plane,
896 cone: &ogeom_math::Cone,
897 heights: (f64, f64),
898 tol: Tolerances,
899) -> Meeting {
900 let apex = cone.apex();
901 let apex_height = cone.frame().to_local(apex).z;
902 let stated = |direction: Direction| -> Vec<Curve> {
905 let climb = direction.vector().dot(cone.axis().direction.vector());
906 if climb.abs() <= tol.angular() {
907 return Vec::new();
908 }
909 let (lo, hi) = (heights.0.min(heights.1), heights.0.max(heights.1));
910 let mut out = Vec::new();
911 for (from, to) in [(lo.max(apex_height), hi), (lo, hi.min(apex_height))] {
912 if !(from.is_finite() && to.is_finite()) || to - from <= tol.confusion() {
913 continue;
914 }
915 let (a, b) = ((from - apex_height) / climb, (to - apex_height) / climb);
916 if let Ok(line) = ogeom_geom::LineCurve::over(
917 ogeom_math::Axis::new(apex, direction),
918 a.min(b),
919 a.max(b),
920 ) {
921 out.push(line.into());
922 }
923 }
924 out
925 };
926 let a = cone.axis().direction.vector();
927 let n = plane.normal().vector();
928 let half = cone.half_angle().abs();
929 let across = n.dot(a);
930 let lean = across.abs().clamp(0.0, 1.0).asin() - half;
933 if lean.abs() <= 1e-10 {
934 let toward = a - n * across;
937 return match Direction::new(toward, tol).map(stated) {
938 Ok(lines) if !lines.is_empty() => Meeting::Along(lines),
939 _ => Meeting::Touching(vec![apex]),
940 };
941 }
942 if lean > 0.0 {
943 return Meeting::Touching(vec![apex]);
944 }
945 let side = n - a * across;
948 let Ok(e) = Direction::new(side, tol) else {
949 return Meeting::Touching(vec![apex]);
950 };
951 let e = e.vector();
952 let f = a.cross(e);
953 let (sin, cos) = half.sin_cos();
954 let k = (-across * cos / (side.magnitude() * sin)).clamp(-1.0, 1.0);
955 let s = (1.0 - k * k).max(0.0).sqrt();
956 let lines: Vec<Curve> = [s, -s]
957 .into_iter()
958 .filter_map(|t| {
959 let d = a * cos + (e * k + f * t) * sin;
960 Direction::new(d, tol).ok()
961 })
962 .flat_map(stated)
963 .collect();
964 if lines.is_empty() {
965 Meeting::Touching(vec![apex])
966 } else {
967 Meeting::Along(lines)
968 }
969}
970
971fn coaxial_cylinder_cone(
981 cylinder: ogeom_math::Cylinder,
982 cone: ogeom_math::Cone,
983 tol: Tolerances,
984) -> OgeomResult<Meeting> {
985 if !cylinder.axis().is_coaxial(cone.axis(), tol) {
986 ogeom_bail!(
987 NotDone,
988 "a cylinder off a cone's axis meets it in a curve only the \
989 general marching intersector can trace"
990 );
991 }
992 let axis = cone.axis();
993 let slope = cone.half_angle().tan();
994 let circles: Vec<Curve> = [cylinder.radius(), -cylinder.radius()]
995 .into_iter()
996 .filter_map(|radius| {
997 let height = (radius - cone.reference_radius()) / slope;
998 cone_parallel(
999 &cone,
1000 axis.location + axis.direction.vector() * height,
1001 cylinder.radius(),
1002 tol,
1003 )
1004 })
1005 .collect();
1006 Ok(if circles.is_empty() {
1007 Meeting::Apart
1008 } else {
1009 Meeting::Along(circles)
1010 })
1011}
1012
1013fn coaxial_cones(
1020 a: ogeom_math::Cone,
1021 b: ogeom_math::Cone,
1022 tol: Tolerances,
1023) -> OgeomResult<Meeting> {
1024 if !a.axis().is_coaxial(b.axis(), tol) {
1025 ogeom_bail!(
1026 NotDone,
1027 "two cones that do not share an axis meet in a curve only the \
1028 general marching intersector can trace"
1029 );
1030 }
1031 let axis = a.axis();
1032 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
1035 let (slope_a, slope_b) = (a.half_angle().tan(), b.half_angle().tan());
1036 let (ref_a, ref_b) = (
1037 a.reference_radius(),
1038 slope_b.mul_add(-lift, b.reference_radius()),
1039 );
1040 if (slope_a - slope_b).abs() <= tol.angular() && (ref_a - ref_b).abs() <= tol.confusion() {
1041 return Ok(Meeting::Same);
1042 }
1043 let mut heights: Vec<f64> = Vec::new();
1047 if (slope_a - slope_b).abs() > tol.angular() {
1048 heights.push((ref_b - ref_a) / (slope_a - slope_b));
1049 }
1050 if (slope_a + slope_b).abs() > tol.angular() {
1051 heights.push(-(ref_a + ref_b) / (slope_a + slope_b));
1052 }
1053 let mut circles: Vec<Curve> = Vec::new();
1054 let mut touches: Vec<Point> = Vec::new();
1055 for height in heights {
1056 let radius = a.radius_at(height).abs();
1057 let centre = axis.location + axis.direction.vector() * height;
1058 if radius <= tol.confusion() {
1059 if touches.iter().all(|t| t.distance(centre) > tol.confusion()) {
1060 touches.push(centre);
1061 }
1062 continue;
1063 }
1064 let repeated = circles.iter().any(|c| {
1065 matches!(c, Curve::Circle(k) if k.circle().centre().distance(centre) <= tol.confusion())
1066 });
1067 if !repeated && let Some(circle) = cone_parallel(&a, centre, radius, tol) {
1068 circles.push(circle);
1069 }
1070 }
1071 Ok(if !circles.is_empty() {
1072 Meeting::Along(circles)
1073 } else if !touches.is_empty() {
1074 Meeting::Touching(touches)
1075 } else {
1076 Meeting::Apart
1077 })
1078}
1079
1080fn cone_parallel(
1082 cone: &ogeom_math::Cone,
1083 centre: Point,
1084 radius: f64,
1085 tol: Tolerances,
1086) -> Option<Curve> {
1087 if radius <= tol.confusion() {
1088 return None;
1089 }
1090 let frame = cone.frame();
1091 let placed = Frame::new(centre, frame.z(), frame.x(), tol).ok()?;
1092 Some(ogeom_geom::CircleCurve::new(Circle::new(placed, radius, tol).ok()?).into())
1093}
1094
1095fn coaxial_tori(
1102 a: ogeom_math::Torus,
1103 b: ogeom_math::Torus,
1104 tol: Tolerances,
1105) -> OgeomResult<Meeting> {
1106 if !a.axis().is_coaxial(b.axis(), tol) {
1107 ogeom_bail!(
1108 NotDone,
1109 "two tori that do not share an axis meet in a curve only the \
1110 general marching intersector can trace"
1111 );
1112 }
1113 let axis = a.axis();
1114 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
1115 if (a.major_radius() - b.major_radius()).abs() <= tol.confusion()
1116 && lift.abs() <= tol.confusion()
1117 && (a.minor_radius() - b.minor_radius()).abs() <= tol.confusion()
1118 {
1119 return Ok(Meeting::Same);
1120 }
1121 let (ra, rb) = (a.minor_radius(), b.minor_radius());
1126 let profile_points = profile_meetings(
1127 &[
1128 (ogeom_math::Point2::new(a.major_radius(), 0.0), ra),
1129 (ogeom_math::Point2::new(-a.major_radius(), 0.0), ra),
1130 ],
1131 &[
1132 (ogeom_math::Point2::new(b.major_radius(), lift), rb),
1133 (ogeom_math::Point2::new(-b.major_radius(), lift), rb),
1134 ],
1135 tol,
1136 );
1137 let circles: Vec<Curve> = profile_points
1138 .into_iter()
1139 .filter_map(|p| {
1140 circle_on(
1141 axis.location + axis.direction.vector() * p.y,
1142 axis.direction,
1143 p.x,
1144 tol,
1145 )
1146 })
1147 .collect();
1148 Ok(if circles.is_empty() {
1149 Meeting::Apart
1150 } else {
1151 Meeting::Along(circles)
1152 })
1153}
1154
1155fn profile_meetings(
1160 a: &[(ogeom_math::Point2, f64)],
1161 b: &[(ogeom_math::Point2, f64)],
1162 tol: Tolerances,
1163) -> Vec<ogeom_math::Point2> {
1164 let mut points: Vec<ogeom_math::Point2> = Vec::new();
1165 for &(ca, ra) in a {
1166 for &(cb, rb) in b {
1167 let between = cb - ca;
1168 let distance = between.magnitude();
1169 if distance <= tol.confusion()
1172 || distance > ra + rb + tol.confusion()
1173 || distance < (ra - rb).abs() - tol.confusion()
1174 {
1175 continue;
1176 }
1177 let along = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
1178 let squared = ra.mul_add(ra, -(along * along));
1179 let direction = between * (1.0 / distance);
1180 let foot = ca + direction * along;
1181 let found = if squared <= tol.confusion() * tol.confusion() {
1182 vec![foot]
1183 } else {
1184 let offset =
1185 ogeom_math::Vector2::new(-direction.y, direction.x) * squared.max(0.0).sqrt();
1186 vec![foot + offset, foot - offset]
1187 };
1188 for p in found {
1189 if p.x > tol.confusion() && points.iter().all(|q| q.distance(p) > tol.confusion()) {
1190 points.push(p);
1191 }
1192 }
1193 }
1194 }
1195 points
1196}
1197
1198fn axial_sphere_torus(
1204 sphere: ogeom_math::Sphere,
1205 torus: ogeom_math::Torus,
1206 tol: Tolerances,
1207) -> OgeomResult<Meeting> {
1208 let axis = torus.axis();
1209 let lift = (sphere.centre() - axis.location).dot(axis.direction.vector());
1210 let foot = axis.location + axis.direction.vector() * lift;
1211 if foot.distance(sphere.centre()) > tol.confusion() {
1212 ogeom_bail!(
1213 NotDone,
1214 "a sphere off a torus's axis meets it in a curve only the general \
1215 marching intersector can trace"
1216 );
1217 }
1218 let tube = torus.minor_radius();
1219 let points = profile_meetings(
1220 &[(ogeom_math::Point2::new(0.0, lift), sphere.radius())],
1221 &[
1222 (ogeom_math::Point2::new(torus.major_radius(), 0.0), tube),
1223 (ogeom_math::Point2::new(-torus.major_radius(), 0.0), tube),
1224 ],
1225 tol,
1226 );
1227 let circles: Vec<Curve> = points
1228 .into_iter()
1229 .filter_map(|p| {
1230 circle_on(
1231 axis.location + axis.direction.vector() * p.y,
1232 axis.direction,
1233 p.x,
1234 tol,
1235 )
1236 })
1237 .collect();
1238 Ok(if circles.is_empty() {
1239 Meeting::Apart
1240 } else {
1241 Meeting::Along(circles)
1242 })
1243}
1244
1245fn intersect_axis_plane(
1247 axis: ogeom_math::Axis,
1248 plane: ogeom_math::Plane,
1249 tol: Tolerances,
1250) -> OgeomResult<Point> {
1251 let along = plane.normal().dot(axis.direction);
1252 if along.abs() <= tol.angular() {
1253 ogeom_bail!(Domain, "the axis runs along the plane and never crosses it");
1254 }
1255 let t = -plane.signed_distance_to(axis.location) / along;
1256 Ok(axis.location + axis.direction.vector() * t)
1257}
1258
1259fn circle_on(centre: Point, normal: Direction, radius: f64, tol: Tolerances) -> Option<Curve> {
1261 if radius <= tol.confusion() {
1262 return None;
1263 }
1264 let reference = if normal.vector().cross(Vector::X).magnitude() > 0.5 {
1266 Vector::X
1267 } else {
1268 Vector::Y
1269 };
1270 let x = Direction::from_cross(normal.vector(), reference, tol).ok()?;
1271 let frame = Frame::new(centre, normal, x, tol).ok()?;
1272 Some(ogeom_geom::CircleCurve::new(Circle::new(frame, radius, tol).ok()?).into())
1273}
1274
1275fn line_through(through: Point, direction: Direction) -> Curve {
1277 ogeom_geom::LineCurve::new(ogeom_math::Axis::new(through, direction)).into()
1278}