1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
35use ogeom_geom::{Curve, SurfaceGeometry};
36use ogeom_math::{Circle, Direction, Ellipse, Frame, Point, Vector};
37
38#[derive(Debug, Clone, PartialEq)]
40pub enum Meeting {
41 Apart,
43 Touching(Vec<Point>),
49 Along(Vec<Curve>),
51 Same,
58}
59
60pub fn surface_surface(
69 a: &SurfaceGeometry,
70 b: &SurfaceGeometry,
71 tol: Tolerances,
72) -> OgeomResult<Meeting> {
73 use SurfaceGeometry as S;
74 match (a, b) {
75 (S::Plane(p), S::Plane(q)) => Ok(plane_plane(p.plane(), q.plane(), tol)),
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::Plane(p), S::Cone(c)) => plane_cone(p.plane(), c.cone(), tol),
90 (S::Cone(c), S::Plane(p)) => plane_cone(p.plane(), c.cone(), tol),
91 (S::Cylinder(x), S::Cone(c)) => coaxial_cylinder_cone(x.cylinder(), c.cone(), tol),
92 (S::Cone(c), S::Cylinder(x)) => coaxial_cylinder_cone(x.cylinder(), c.cone(), tol),
93 (S::Cone(x), S::Cone(y)) => coaxial_cones(x.cone(), y.cone(), tol),
94 _ => ogeom_bail!(
95 NotDone,
96 "this pair of surfaces has no closed-form intersection; it needs \
97 the general marching intersector, which is gated on the benchmark \
98 these cases provide the ground truth for"
99 ),
100 }
101}
102
103fn plane_plane(a: ogeom_math::Plane, b: ogeom_math::Plane, tol: Tolerances) -> Meeting {
105 let along = a.normal().dot(b.normal());
106 if (along.abs() - 1.0).abs() <= tol.angular() {
107 return if a.distance_to(b.origin()) <= tol.confusion() {
110 Meeting::Same
111 } else {
112 Meeting::Apart
113 };
114 }
115 let Ok(direction) = Direction::from_cross(a.normal().vector(), b.normal().vector(), tol) else {
118 return Meeting::Apart;
119 };
120 let (da, db) = (
121 a.normal().dot_vector(a.origin().to_vector()),
122 b.normal().dot_vector(b.origin().to_vector()),
123 );
124 let (na, nb) = (a.normal().vector(), b.normal().vector());
125 let dot = na.dot(nb);
126 let denominator = dot.mul_add(-dot, 1.0);
127 if denominator.abs() <= tol.angular() {
128 return Meeting::Apart;
129 }
130 let ca = da.mul_add(1.0, -(db * dot)) / denominator;
131 let cb = db.mul_add(1.0, -(da * dot)) / denominator;
132 let through = Point::from_vector(na * ca + nb * cb);
133 Meeting::Along(vec![line_through(through, direction)])
134}
135
136fn plane_sphere(plane: ogeom_math::Plane, sphere: ogeom_math::Sphere, tol: Tolerances) -> Meeting {
138 let gap = plane.signed_distance_to(sphere.centre());
139 let reach = gap.abs();
140 if reach > sphere.radius() + tol.confusion() {
141 return Meeting::Apart;
142 }
143 let foot = plane.project(sphere.centre());
144 if (reach - sphere.radius()).abs() <= tol.confusion() {
145 return Meeting::Touching(vec![foot]);
146 }
147 let radius = sphere
150 .radius()
151 .mul_add(sphere.radius(), -(gap * gap))
152 .max(0.0)
153 .sqrt();
154 match circle_on(foot, plane.normal(), radius, tol) {
155 Some(circle) => Meeting::Along(vec![circle]),
156 None => Meeting::Touching(vec![foot]),
157 }
158}
159
160fn plane_cylinder(
166 plane: ogeom_math::Plane,
167 cylinder: ogeom_math::Cylinder,
168 tol: Tolerances,
169) -> OgeomResult<Meeting> {
170 let axis = cylinder.axis();
171 let along = plane.normal().dot(axis.direction);
172
173 if along.abs() <= tol.angular() {
176 let gap = plane.signed_distance_to(axis.location);
177 let reach = gap.abs();
178 if reach > cylinder.radius() + tol.confusion() {
179 return Ok(Meeting::Apart);
180 }
181 let offset = cylinder
183 .radius()
184 .mul_add(cylinder.radius(), -(gap * gap))
185 .max(0.0)
186 .sqrt();
187 let foot = plane.project(axis.location);
188 let sideways =
189 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
190 if offset <= tol.confusion() {
191 return Ok(Meeting::Along(vec![line_through(foot, axis.direction)]));
193 }
194 return Ok(Meeting::Along(vec![
195 line_through(foot + sideways.vector() * offset, axis.direction),
196 line_through(foot - sideways.vector() * offset, axis.direction),
197 ]));
198 }
199
200 let centre = intersect_axis_plane(axis, plane, tol)?;
202 if (along.abs() - 1.0).abs() <= tol.angular() {
203 return Ok(
204 match circle_on(centre, plane.normal(), cylinder.radius(), tol) {
205 Some(circle) => Meeting::Along(vec![circle]),
206 None => Meeting::Apart,
207 },
208 );
209 }
210
211 let minor = cylinder.radius();
214 let major = minor / along.abs();
215 let minor_direction =
218 Direction::from_cross(plane.normal().vector(), axis.direction.vector(), tol)?;
219 let major_direction =
220 Direction::from_cross(minor_direction.vector(), plane.normal().vector(), tol)?;
221 let frame = Frame::from_axes(
222 centre,
223 major_direction,
224 minor_direction,
225 plane.normal(),
226 tol,
227 )?;
228 Ok(Meeting::Along(vec![
229 ogeom_geom::EllipseCurve::new(Ellipse::new(frame, major, minor, tol)?).into(),
230 ]))
231}
232
233fn sphere_sphere(a: ogeom_math::Sphere, b: ogeom_math::Sphere, tol: Tolerances) -> Meeting {
235 let between = b.centre() - a.centre();
236 let distance = between.magnitude();
237 if distance <= tol.confusion() {
238 return if (a.radius() - b.radius()).abs() <= tol.confusion() {
239 Meeting::Same
240 } else {
241 Meeting::Apart
243 };
244 }
245 let (ra, rb) = (a.radius(), b.radius());
246 if distance > ra + rb + tol.confusion() || distance < (ra - rb).abs() - tol.confusion() {
247 return Meeting::Apart;
248 }
249 let Ok(direction) = Direction::new(between, tol) else {
250 return Meeting::Apart;
251 };
252 let reach = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
254 let centre = a.centre() + direction.vector() * reach;
255 let squared = ra.mul_add(ra, -(reach * reach));
256 if squared <= tol.confusion() * tol.confusion() {
257 return Meeting::Touching(vec![centre]);
258 }
259 match circle_on(centre, direction, squared.max(0.0).sqrt(), tol) {
260 Some(circle) => Meeting::Along(vec![circle]),
261 None => Meeting::Touching(vec![centre]),
262 }
263}
264
265fn coaxial_cylinders(
271 a: ogeom_math::Cylinder,
272 b: ogeom_math::Cylinder,
273 tol: Tolerances,
274) -> OgeomResult<Meeting> {
275 if !a.axis().is_coaxial(b.axis(), tol) {
276 if (a.radius() - b.radius()).abs() <= tol.confusion() {
283 let (da, db) = (a.axis().direction.vector(), b.axis().direction.vector());
284 let normal = da.cross(db);
285 if normal.magnitude() > tol.angular() {
286 let (pa, pb) = (a.axis().location, b.axis().location);
287 let w = pb - pa;
290 let dd = da.dot(db);
291 let denom = dd.mul_add(-dd, 1.0);
292 let s = dd.mul_add(-db.dot(w), da.dot(w)) / denom;
293 let t = dd.mul_add(da.dot(w), -db.dot(w)) / denom;
294 let on_a = pa + da * s;
295 let on_b = pb + db * t;
296 if on_a.distance(on_b) <= tol.confusion() {
297 let centre = on_a;
298 let mut curves = Vec::new();
299 for m in [da - db, da + db] {
300 if m.magnitude() <= tol.angular() {
301 continue;
302 }
303 let plane =
304 ogeom_math::Plane::through(centre, ogeom_math::Direction::new(m, tol)?);
305 if let Meeting::Along(mut found) = plane_cylinder(plane, a, tol)? {
306 curves.append(&mut found);
307 }
308 }
309 if !curves.is_empty() {
310 return Ok(Meeting::Along(curves));
311 }
312 }
313 }
314 }
315 ogeom_bail!(
316 NotDone,
317 "two cylinders that do not share an axis meet in a quartic space \
318 curve, which needs the general marching intersector"
319 );
320 }
321 Ok(if (a.radius() - b.radius()).abs() <= tol.confusion() {
322 Meeting::Same
323 } else {
324 Meeting::Apart
326 })
327}
328
329fn coaxial_cylinder_sphere(
331 cylinder: ogeom_math::Cylinder,
332 sphere: ogeom_math::Sphere,
333 tol: Tolerances,
334) -> OgeomResult<Meeting> {
335 let axis = cylinder.axis();
336 if axis.distance_to(sphere.centre()) > tol.confusion() {
337 ogeom_bail!(
338 NotDone,
339 "a sphere off a cylinder's axis meets it in a quartic space curve, \
340 which needs the general marching intersector"
341 );
342 }
343 let (r, radius) = (cylinder.radius(), sphere.radius());
344 if r > radius + tol.confusion() {
345 return Ok(Meeting::Apart);
346 }
347 if (r - radius).abs() <= tol.confusion() {
348 let centre = sphere.centre();
351 return Ok(match circle_on(centre, axis.direction, r, tol) {
352 Some(circle) => Meeting::Along(vec![circle]),
353 None => Meeting::Apart,
354 });
355 }
356 let reach = radius.mul_add(radius, -(r * r)).max(0.0).sqrt();
358 let mut out = Vec::with_capacity(2);
359 for side in [reach, -reach] {
360 let centre = sphere.centre() + axis.direction.vector() * side;
361 if let Some(circle) = circle_on(centre, axis.direction, r, tol) {
362 out.push(circle);
363 }
364 }
365 Ok(if out.is_empty() {
366 Meeting::Apart
367 } else {
368 Meeting::Along(out)
369 })
370}
371
372fn axial_plane_torus(
386 plane: ogeom_math::Plane,
387 torus: ogeom_math::Torus,
388 tol: Tolerances,
389) -> OgeomResult<Meeting> {
390 let axis = torus.axis();
391 let along = plane.normal().dot(axis.direction);
392 if along.abs() <= tol.angular()
393 && plane.signed_distance_to(axis.location).abs() <= tol.confusion()
394 {
395 return Ok(meridians(plane, torus, tol));
396 }
397 if (along.abs() - 1.0).abs() > tol.angular() {
398 ogeom_bail!(
399 NotDone,
400 "a plane oblique to a torus's axis, or parallel to it and off it, \
401 meets it in a quartic, which needs the general marching \
402 intersector"
403 );
404 }
405 let height = -plane.signed_distance_to(axis.location) * along.signum();
407 let minor = torus.minor_radius();
408 if height.abs() > minor + tol.confusion() {
409 return Ok(Meeting::Apart);
410 }
411 let centre = axis.location + axis.direction.vector() * height;
412 if (height.abs() - minor).abs() <= tol.confusion() {
413 return Ok(
415 match circle_on(centre, axis.direction, torus.major_radius(), tol) {
416 Some(circle) => Meeting::Along(vec![circle]),
417 None => Meeting::Apart,
418 },
419 );
420 }
421 let spread = minor.mul_add(minor, -(height * height)).max(0.0).sqrt();
424 let circles: Vec<Curve> = [torus.major_radius() + spread, torus.major_radius() - spread]
425 .into_iter()
426 .filter_map(|radius| circle_on(centre, axis.direction, radius, tol))
427 .collect();
428 Ok(if circles.is_empty() {
429 Meeting::Apart
430 } else {
431 Meeting::Along(circles)
432 })
433}
434
435fn meridians(plane: ogeom_math::Plane, torus: ogeom_math::Torus, tol: Tolerances) -> Meeting {
439 let axis = torus.axis();
440 let normal = plane.normal();
441 let Ok(out) = Direction::from_cross(axis.direction.vector(), normal.vector(), tol) else {
442 return Meeting::Apart;
443 };
444 let circles: Vec<Curve> = [out.vector(), -out.vector()]
445 .into_iter()
446 .filter_map(|radial| {
447 let centre = axis.location + radial * torus.major_radius();
448 let x = Direction::new(radial, tol).ok()?;
449 let frame = Frame::new(centre, normal, x, tol).ok()?;
450 let circle = Circle::new(frame, torus.minor_radius(), tol).ok()?;
451 Some(ogeom_geom::CircleCurve::new(circle).into())
452 })
453 .collect();
454 Meeting::Along(circles)
455}
456
457fn coaxial_cylinder_torus(
460 cylinder: ogeom_math::Cylinder,
461 torus: ogeom_math::Torus,
462 tol: Tolerances,
463) -> OgeomResult<Meeting> {
464 if !cylinder.axis().is_coaxial(torus.axis(), tol) {
465 ogeom_bail!(
466 NotDone,
467 "a cylinder off a torus's axis meets it in a quartic space curve, \
468 which needs the general marching intersector"
469 );
470 }
471 let axis = torus.axis();
472 let reach = (cylinder.radius() - torus.major_radius()).abs();
473 let minor = torus.minor_radius();
474 if reach > minor + tol.confusion() {
475 return Ok(Meeting::Apart);
476 }
477 if (reach - minor).abs() <= tol.confusion() {
478 return Ok(
480 match circle_on(axis.location, axis.direction, cylinder.radius(), tol) {
481 Some(circle) => Meeting::Along(vec![circle]),
482 None => Meeting::Apart,
483 },
484 );
485 }
486 let rise = minor.mul_add(minor, -(reach * reach)).max(0.0).sqrt();
487 let circles: Vec<Curve> = [rise, -rise]
488 .into_iter()
489 .filter_map(|height| {
490 circle_on(
491 axis.location + axis.direction.vector() * height,
492 axis.direction,
493 cylinder.radius(),
494 tol,
495 )
496 })
497 .collect();
498 Ok(if circles.is_empty() {
499 Meeting::Apart
500 } else {
501 Meeting::Along(circles)
502 })
503}
504
505fn plane_cone(
514 plane: ogeom_math::Plane,
515 cone: ogeom_math::Cone,
516 tol: Tolerances,
517) -> OgeomResult<Meeting> {
518 let axis = cone.axis();
519 let along = plane.normal().dot(axis.direction);
520 if (along.abs() - 1.0).abs() > tol.angular() {
521 ogeom_bail!(
522 NotDone,
523 "a plane oblique to a cone's axis meets it in a conic, which \
524 needs the general marching intersector"
525 );
526 }
527 let height = -plane.signed_distance_to(axis.location) * along.signum();
529 let radius = cone.radius_at(height);
530 if radius.abs() <= tol.confusion() {
531 return Ok(Meeting::Touching(vec![cone.apex()]));
534 }
535 if radius < 0.0 {
536 ogeom_bail!(
540 NotDone,
541 "the plane crosses the cone past its apex, where the chart runs \
542 mirrored; that configuration needs the general machinery"
543 );
544 }
545 let centre = axis.location + axis.direction.vector() * height;
546 Ok(match cone_parallel(&cone, centre, radius, tol) {
547 Some(circle) => Meeting::Along(vec![circle]),
548 None => Meeting::Apart,
549 })
550}
551
552fn coaxial_cylinder_cone(
562 cylinder: ogeom_math::Cylinder,
563 cone: ogeom_math::Cone,
564 tol: Tolerances,
565) -> OgeomResult<Meeting> {
566 if !cylinder.axis().is_coaxial(cone.axis(), tol) {
567 ogeom_bail!(
568 NotDone,
569 "a cylinder off a cone's axis meets it in a curve only the \
570 general marching intersector can trace"
571 );
572 }
573 let axis = cone.axis();
574 let slope = cone.half_angle().tan();
575 let height = (cylinder.radius() - cone.reference_radius()) / slope;
576 Ok(
577 match cone_parallel(
578 &cone,
579 axis.location + axis.direction.vector() * height,
580 cylinder.radius(),
581 tol,
582 ) {
583 Some(circle) => Meeting::Along(vec![circle]),
584 None => Meeting::Apart,
585 },
586 )
587}
588
589fn coaxial_cones(
596 a: ogeom_math::Cone,
597 b: ogeom_math::Cone,
598 tol: Tolerances,
599) -> OgeomResult<Meeting> {
600 if !a.axis().is_coaxial(b.axis(), tol) {
601 ogeom_bail!(
602 NotDone,
603 "two cones that do not share an axis meet in a curve only the \
604 general marching intersector can trace"
605 );
606 }
607 let axis = a.axis();
608 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
611 let (slope_a, slope_b) = (a.half_angle().tan(), b.half_angle().tan());
612 let (ref_a, ref_b) = (
613 a.reference_radius(),
614 slope_b.mul_add(-lift, b.reference_radius()),
615 );
616 if (slope_a - slope_b).abs() <= tol.angular() {
617 return Ok(if (ref_a - ref_b).abs() <= tol.confusion() {
619 Meeting::Same
620 } else {
621 Meeting::Apart
622 });
623 }
624 let height = (ref_b - ref_a) / (slope_a - slope_b);
628 let radius = a.radius_at(height);
629 if radius.abs() <= tol.confusion() {
630 return Ok(Meeting::Touching(vec![a.apex()]));
632 }
633 if radius < 0.0 {
634 ogeom_bail!(
635 NotDone,
636 "two coaxial cones that meet only past their apexes, where the \
637 charts run mirrored, need the general machinery"
638 );
639 }
640 Ok(
641 match cone_parallel(
642 &a,
643 axis.location + axis.direction.vector() * height,
644 radius,
645 tol,
646 ) {
647 Some(circle) => Meeting::Along(vec![circle]),
648 None => Meeting::Apart,
649 },
650 )
651}
652
653fn cone_parallel(
655 cone: &ogeom_math::Cone,
656 centre: Point,
657 radius: f64,
658 tol: Tolerances,
659) -> Option<Curve> {
660 if radius <= tol.confusion() {
661 return None;
662 }
663 let frame = cone.frame();
664 let placed = Frame::new(centre, frame.z(), frame.x(), tol).ok()?;
665 Some(ogeom_geom::CircleCurve::new(Circle::new(placed, radius, tol).ok()?).into())
666}
667
668fn coaxial_tori(
675 a: ogeom_math::Torus,
676 b: ogeom_math::Torus,
677 tol: Tolerances,
678) -> OgeomResult<Meeting> {
679 if !a.axis().is_coaxial(b.axis(), tol) {
680 ogeom_bail!(
681 NotDone,
682 "two tori that do not share an axis meet in a curve only the \
683 general marching intersector can trace"
684 );
685 }
686 let axis = a.axis();
687 let lift = (b.axis().location - a.axis().location).dot(axis.direction.vector());
688 if (a.major_radius() - b.major_radius()).abs() <= tol.confusion()
689 && lift.abs() <= tol.confusion()
690 && (a.minor_radius() - b.minor_radius()).abs() <= tol.confusion()
691 {
692 return Ok(Meeting::Same);
693 }
694 let (ca, cb) = (
697 ogeom_math::Point2::new(a.major_radius(), 0.0),
698 ogeom_math::Point2::new(b.major_radius(), lift),
699 );
700 let between = cb - ca;
701 let distance = between.magnitude();
702 let (ra, rb) = (a.minor_radius(), b.minor_radius());
703 if distance <= tol.confusion() {
704 return Ok(Meeting::Apart);
707 }
708 if distance > ra + rb + tol.confusion() || distance < (ra - rb).abs() - tol.confusion() {
709 return Ok(Meeting::Apart);
710 }
711 let along = distance.mul_add(distance, ra.mul_add(ra, -(rb * rb))) / (2.0 * distance);
712 let squared = ra.mul_add(ra, -(along * along));
713 let direction = between * (1.0 / distance);
714 let foot = ca + direction * along;
715 let mut profile_points = Vec::new();
716 if squared <= tol.confusion() * tol.confusion() {
717 profile_points.push(foot);
718 } else {
719 let offset = ogeom_math::Vector2::new(-direction.y, direction.x) * squared.max(0.0).sqrt();
720 profile_points.push(foot + offset);
721 profile_points.push(foot - offset);
722 }
723 let circles: Vec<Curve> = profile_points
724 .into_iter()
725 .filter_map(|p| {
726 circle_on(
727 axis.location + axis.direction.vector() * p.y,
728 axis.direction,
729 p.x,
730 tol,
731 )
732 })
733 .collect();
734 Ok(if circles.is_empty() {
735 Meeting::Apart
736 } else {
737 Meeting::Along(circles)
738 })
739}
740
741fn intersect_axis_plane(
743 axis: ogeom_math::Axis,
744 plane: ogeom_math::Plane,
745 tol: Tolerances,
746) -> OgeomResult<Point> {
747 let along = plane.normal().dot(axis.direction);
748 if along.abs() <= tol.angular() {
749 ogeom_bail!(Domain, "the axis runs along the plane and never crosses it");
750 }
751 let t = -plane.signed_distance_to(axis.location) / along;
752 Ok(axis.location + axis.direction.vector() * t)
753}
754
755fn circle_on(centre: Point, normal: Direction, radius: f64, tol: Tolerances) -> Option<Curve> {
757 if radius <= tol.confusion() {
758 return None;
759 }
760 let reference = if normal.vector().cross(Vector::X).magnitude() > 0.5 {
762 Vector::X
763 } else {
764 Vector::Y
765 };
766 let x = Direction::from_cross(normal.vector(), reference, tol).ok()?;
767 let frame = Frame::new(centre, normal, x, tol).ok()?;
768 Some(ogeom_geom::CircleCurve::new(Circle::new(frame, radius, tol).ok()?).into())
769}
770
771fn line_through(through: Point, direction: Direction) -> Curve {
773 ogeom_geom::LineCurve::new(ogeom_math::Axis::new(through, direction)).into()
774}