math_utils/geometry/intersect/
mod.rs

1//! Primitive intersection routines
2
3use approx;
4
5use crate::*;
6use super::*;
7
8pub mod integer;
9
10/// Discrete intersection test of 1D axis-aligned bounding boxes (intervals).
11///
12/// AABBs that are merely touching are not counted as intersecting:
13///
14/// ```
15/// # use math_utils::geometry::*;
16/// # use math_utils::geometry::intersect::*;
17/// let a = Interval::from_points ( 0.0, 1.0);
18/// let b = Interval::from_points (-1.0, 0.0);
19/// assert!(!discrete_interval (&a, &b));
20/// ```
21#[inline]
22pub fn discrete_interval <S : OrderedRing> (a : &Interval <S>, b : &Interval <S>)
23  -> bool
24{
25  let (min_a, max_a) = (a.min(), a.max());
26  let (min_b, max_b) = (b.min(), b.max());
27  // intersection if intervals overlap
28  max_a > min_b && min_a < max_b
29}
30
31/// Continuous intersection test of 1D axis-aligned bounding boxes (intervals).
32///
33/// AABBs that are merely touching return no intersection:
34///
35/// ```
36/// # use math_utils::geometry::*;
37/// # use math_utils::geometry::intersect::*;
38/// let a = Interval::from_points ( 0.0, 1.0);
39/// let b = Interval::from_points (-1.0, 0.0);
40/// assert!(continuous_interval (&a, &b).is_none());
41/// ```
42#[inline]
43pub fn continuous_interval <S> (a : &Interval <S>, b : &Interval <S>)
44  -> Option <Interval <S>>
45where S : OrderedRing + std::fmt::Debug {
46  if discrete_interval (a, b) {
47    Some (
48      Interval::with_minmax (S::max (a.min(), b.min()), S::min (a.max(), b.max())))
49  } else {
50    None
51  }
52}
53
54/// Discrete intersection test of 2D axis-aligned bounding boxes.
55///
56/// AABBs that are merely touching are not counted as intersecting:
57///
58/// ```
59/// # use math_utils::geometry::*;
60/// # use math_utils::geometry::intersect::*;
61/// let a = Aabb2::from_points ([ 0.0, 0.0].into(), [1.0, 1.0].into());
62/// let b = Aabb2::from_points ([-1.0, 0.0].into(), [0.0, 1.0].into());
63/// assert!(!discrete_aabb2_aabb2 (&a, &b));
64/// ```
65#[inline]
66pub fn discrete_aabb2_aabb2 <S : OrderedRing> (a : &Aabb2 <S>, b : &Aabb2 <S>) -> bool {
67  let (min_a, max_a) = (a.min(), a.max());
68  let (min_b, max_b) = (b.min(), b.max());
69  // intersection if overlap exists on both axes
70  max_a.0.x > min_b.0.x && min_a.0.x < max_b.0.x &&
71  max_a.0.y > min_b.0.y && min_a.0.y < max_b.0.y
72}
73
74/// Continuous intersection test of 2D axis-aligned bounding boxes.
75///
76/// AABBs that are merely touching return no intersection:
77///
78/// ```
79/// # use math_utils::geometry::*;
80/// # use math_utils::geometry::intersect::*;
81/// let a = Aabb2::from_points ([ 0.0, 0.0].into(), [1.0, 1.0].into());
82/// let b = Aabb2::from_points ([-1.0, 0.0].into(), [0.0, 1.0].into());
83/// assert!(continuous_aabb2_aabb2 (&a, &b).is_none());
84/// ```
85#[inline]
86pub fn continuous_aabb2_aabb2 <S> (a : &Aabb2 <S>, b : &Aabb2 <S>)
87  -> Option <Aabb2 <S>>
88where S : OrderedRing + std::fmt::Debug {
89  if discrete_aabb2_aabb2 (a, b) {
90    Some (
91      Aabb2::with_minmax (point2_max (a.min(), b.min()), point2_min (a.max(), b.max())))
92  } else {
93    None
94  }
95}
96
97/// Discrete intersection test of 3D axis-aligned bounding boxes.
98///
99/// AABBs that are merely touching are not counted as intersecting:
100///
101/// ```
102/// # use math_utils::geometry::*;
103/// # use math_utils::geometry::intersect::*;
104/// let a = Aabb3::from_points ([ 0.0, 0.0, 0.0].into(), [1.0, 1.0, 1.0].into());
105/// let b = Aabb3::from_points ([-1.0, 0.0, 0.0].into(), [0.0, 1.0, 1.0].into());
106/// assert!(!discrete_aabb3_aabb3 (&a, &b));
107/// ```
108#[inline]
109pub fn discrete_aabb3_aabb3 <S : OrderedRing> (a : &Aabb3 <S>, b : &Aabb3 <S>) -> bool {
110  let (min_a, max_a) = (a.min(), a.max());
111  let (min_b, max_b) = (b.min(), b.max());
112  // intersection if overlap exists on all three axes
113  max_a.0.x > min_b.0.x && min_a.0.x < max_b.0.x &&
114  max_a.0.y > min_b.0.y && min_a.0.y < max_b.0.y &&
115  max_a.0.z > min_b.0.z && min_a.0.z < max_b.0.z
116}
117
118/// Continuous intersection test of 3D axis-aligned bounding boxes.
119///
120/// AABBs that are merely touching return no intersection:
121///
122/// ```
123/// # use math_utils::geometry::*;
124/// # use math_utils::geometry::intersect::*;
125/// let a = Aabb3::from_points ([ 0.0, 0.0, 0.0].into(), [1.0, 1.0, 1.0].into());
126/// let b = Aabb3::from_points ([-1.0, 0.0, 0.0].into(), [0.0, 1.0, 1.0].into());
127/// assert!(continuous_aabb3_aabb3 (&a, &b).is_none());
128/// ```
129#[inline]
130pub fn continuous_aabb3_aabb3 <S> (a : &Aabb3 <S>, b : &Aabb3 <S>)
131  -> Option <Aabb3 <S>>
132where S : OrderedRing + std::fmt::Debug {
133  if discrete_aabb3_aabb3 (a, b) {
134    Some (
135      Aabb3::with_minmax (point3_max (a.min(), b.min()), point3_min (a.max(), b.max())))
136  } else {
137    None
138  }
139}
140
141/// Compute the intersection of a 2D line with a 2D AABB (rectangle).
142///
143/// If the line and AABB intersect, the two intersection points are returned with the
144/// scalar parameter corresponding to that point along the line.
145///
146/// ```
147/// # use math_utils::*;
148/// # use math_utils::geometry::*;
149/// # use math_utils::geometry::intersect::*;
150/// let aabb = Aabb2::with_minmax ([-1.0, -1.0].into(), [1.0, 1.0].into());
151/// let line = Line2::new  ([0.0, 0.0].into(), Unit2::axis_x());
152/// assert_eq!(
153///   continuous_line2_aabb2 (&line, &aabb).unwrap(),
154///   ( (-1.0, [-1.0, 0.0].into()),
155///     ( 1.0, [ 1.0, 0.0].into())
156///   )
157/// );
158/// ```
159///
160/// Returns `None` if the line and AABB are tangent:
161///
162/// ```
163/// # use math_utils::*;
164/// # use math_utils::geometry::*;
165/// # use math_utils::geometry::intersect::*;
166/// let aabb = Aabb2::with_minmax ([-1.0, -1.0].into(), [1.0, 1.0].into());
167/// let line = Line2::new  ([0.0, 1.0].into(), Unit2::axis_x());
168/// assert_eq!(continuous_line2_aabb2 (&line, &aabb), None);
169/// ```
170#[expect(clippy::type_complexity)]
171pub fn continuous_line2_aabb2 <S> (line : &Line2 <S>, aabb : &Aabb2 <S>)
172  -> Option <((S, Point2 <S>), (S, Point2 <S>))>
173where S : Real + std::fmt::Debug {
174  let aabb_min = aabb.min();
175  let aabb_max = aabb.max();
176  if line.direction.x == S::zero() {
177    // parallel x-axis
178    if aabb_min.0.x < line.base.0.x && line.base.0.x < aabb_max.0.x {
179      let out = if line.direction.y > S::zero() {
180        let (t0, t1) = (aabb_min.0.y - line.base.0.y, aabb_max.0.y - line.base.0.y);
181        ( (t0, [line.base.0.x, aabb_min.0.y].into()),
182          (t1, [line.base.0.x, aabb_max.0.y].into())
183        )
184      } else {
185        let (t0, t1) = (line.base.0.y - aabb_max.0.y, line.base.0.y - aabb_min.0.y);
186        ( (t0, [line.base.0.x, aabb_max.0.y].into()),
187          (t1, [line.base.0.x, aabb_min.0.y].into())
188        )
189      };
190      Some (out)
191    } else {
192      None
193    }
194  } else if line.direction.y == S::zero() {
195    // parallel y-axis
196    if aabb_min.0.y < line.base.0.y && line.base.0.y < aabb_max.0.y {
197      let out = if line.direction.x > S::zero() {
198        let (t0, t1) = (aabb_min.0.x - line.base.0.x, aabb_max.0.x - line.base.0.x);
199        ( (t0, [aabb_min.0.x, line.base.0.y].into()),
200          (t1, [aabb_max.0.x, line.base.0.y].into())
201        )
202      } else {
203        let (t0, t1) = (line.base.0.x - aabb_max.0.x, line.base.0.x - aabb_min.0.x);
204        ( (t0, [aabb_max.0.x, line.base.0.y].into()),
205          (t1, [aabb_min.0.x, line.base.0.y].into())
206        )
207      };
208      Some (out)
209    } else {
210      None
211    }
212  } else {
213    let dir_reciprocal = line.direction.map (|s| S::one() / s);
214    let (t0_x, t1_x)   = {
215      let (near_x, far_x) = if line.direction.x.is_positive() {
216        (aabb_min.0.x, aabb_max.0.x)
217      } else {
218        (aabb_max.0.x, aabb_min.0.x)
219      };
220      ( (near_x - line.base.0.x) * dir_reciprocal.x,
221        (far_x  - line.base.0.x) * dir_reciprocal.x
222      )
223    };
224    let (t0_y, t1_y) = {
225      let (near_y, far_y) = if line.direction.y.is_positive() {
226        (aabb_min.0.y, aabb_max.0.y)
227      } else {
228        (aabb_max.0.y, aabb_min.0.y)
229      };
230      ( (near_y - line.base.0.y) * dir_reciprocal.y,
231        (far_y  - line.base.0.y) * dir_reciprocal.y
232      )
233    };
234    let interval_x = Interval::with_minmax (t0_x, t1_x);
235    let interval_y = Interval::with_minmax (t0_y, t1_y);
236    continuous_interval (&interval_x, &interval_y).map (|interval|{
237      let start = line.point (interval.min());
238      let end   = line.point (interval.max());
239      ( (interval.min(), start), (interval.max(), end) )
240    })
241  }
242}
243
244/// Compute the intersection of a 2D line with a 2D sphere (circle).
245///
246/// If the line and circle intersect, the two intersection points are returned with the
247/// scalar parameter corresponding to that point along the line.
248///
249/// ```
250/// # use math_utils::*;
251/// # use math_utils::geometry::*;
252/// # use math_utils::geometry::intersect::*;
253/// let sphere = Sphere2::unit();
254/// let line   = Line2::new  ([0.0, 0.0].into(), Unit2::axis_x());
255/// assert_eq!(
256///   continuous_line2_sphere2 (&line, &sphere).unwrap(),
257///   ( (-1.0, [-1.0, 0.0].into()),
258///     ( 1.0, [ 1.0, 0.0].into())
259///   )
260/// );
261/// ```
262///
263/// Returns `None` if the line and circle are tangent:
264///
265/// ```
266/// # use math_utils::*;
267/// # use math_utils::geometry::*;
268/// # use math_utils::geometry::intersect::*;
269/// let sphere = Sphere2::unit();
270/// let line   = Line2::new  ([0.0, 1.0].into(), Unit2::axis_x());
271/// assert_eq!(continuous_line2_sphere2 (&line, &sphere), None);
272/// ```
273#[expect(clippy::type_complexity)]
274pub fn continuous_line2_sphere2 <S : Real> (line : &Line2 <S>, sphere : &Sphere2 <S>)
275  -> Option <((S, Point2 <S>), (S, Point2 <S>))>
276{
277  // intersect the line with the cylinder circle in the XY plane
278  let two  = S::two();
279  let four = S::four();
280  // defining intersection points by the equation at^2 + bt + c = 0, solve for t using
281  // quadratic formula:
282  //
283  //  t = (-b +- sqrt (b^2 -4ac)) / 2a
284  //
285  // where a, b, and c are defined in terms of points p1, p2 of the line segment and the
286  // sphere center p3, and sphere radius r
287  let p1   = line.base;
288  #[expect(clippy::no_effect_underscore_binding)]
289  let _p2   = line.base + *line.direction;
290  let p3   = sphere.center;
291  let r    = *sphere.radius;
292  let p1p2 = &line.direction;
293  let p3p1 = p1 - p3;
294  let a    = S::one();
295  let b    = two * p1p2.dot (p3p1);
296  let c    = p3p1.norm_squared() - r * r;
297  // this is the portion of the quadratic equation inside the square root that
298  // determines whether the intersection is none, a tangent point, or a segment
299  let discriminant = b * b - four * a * c;
300  if discriminant <= S::zero() {
301    None
302  } else {
303    let discriminant_sqrt = discriminant.sqrt();
304    let frac_2a           = S::one() / (two * a);
305    let t1                = (-b - discriminant_sqrt) * frac_2a;
306    let t2                = (-b + discriminant_sqrt) * frac_2a;
307    let first  = p1 + (**p1p2) * t1;
308    let second = p1 + (**p1p2) * t2;
309    Some (((t1, first), (t2, second)))
310  }
311}
312
313/// Compute the continuous intersection of a 3D line with a 3D plane.
314///
315/// Returns `None` if the line and plane are parallel:
316///
317/// ```
318/// # use math_utils::*;
319/// # use math_utils::geometry::*;
320/// # use math_utils::geometry::intersect::*;
321/// let plane = Plane3::new ([0.0, 0.0,  0.0].into(), Unit3::axis_z());
322/// let line  = Line3::new  (
323///   [0.0, 0.0, 0.0].into(),
324///   Unit3::normalize ([1.0, 1.0, 0.0].into()));
325/// assert_eq!(continuous_line3_plane3 (&line, &plane), None);
326/// ```
327pub fn continuous_line3_plane3 <S> (line : &Line3 <S>, plane : &Plane3 <S>)
328  -> Option <(S, Point3 <S>)>
329where S : Real + approx::RelativeEq {
330  let normal_dot_direction = plane.normal.dot (*line.direction);
331  if approx::relative_eq!(normal_dot_direction, S::zero()) {
332    None
333  } else {
334    let plane_to_line = line.base - plane.base;
335    let t     = -plane.normal.dot (plane_to_line) / normal_dot_direction;
336    let point = line.point (t);
337    Some ((t, point))
338  }
339}
340
341/// Compute the intersection of a 3D line with a 3D AABB.
342///
343/// If the line and AABB intersect, the two intersection points are returned with the
344/// scalar parameter corresponding to that point along the line.
345///
346/// ```
347/// # use math_utils::*;
348/// # use math_utils::geometry::*;
349/// # use math_utils::geometry::intersect::*;
350/// let aabb = Aabb3::with_minmax (
351///   [-1.0, -1.0, -1.0].into(), [1.0, 1.0, 1.0].into());
352/// let line = Line3::new  ([0.0, 0.0, 0.0].into(), Unit3::axis_x());
353/// assert_eq!(
354///   continuous_line3_aabb3 (&line, &aabb).unwrap(),
355///   ( (-1.0, [-1.0, 0.0, 0.0].into()),
356///     ( 1.0, [ 1.0, 0.0, 0.0].into())
357///   )
358/// );
359/// ```
360///
361/// Returns `None` if the line and AABB are tangent:
362///
363/// ```
364/// # use math_utils::*;
365/// # use math_utils::geometry::*;
366/// # use math_utils::geometry::intersect::*;
367/// let aabb = Aabb3::with_minmax (
368///   [-1.0, -1.0, -1.0].into(), [1.0, 1.0, 1.0].into());
369/// let line = Line3::new  ([0.0, 1.0, 0.0].into(), Unit3::axis_x());
370/// assert_eq!(continuous_line3_aabb3 (&line, &aabb), None);
371/// ```
372#[expect(clippy::type_complexity)]
373pub fn continuous_line3_aabb3 <S> (line : &Line3 <S>, aabb : &Aabb3 <S>)
374  -> Option <((S, Point3 <S>), (S, Point3 <S>))>
375where S : Real + num_traits::Float + approx::RelativeEq <Epsilon=S> + std::fmt::Debug {
376  let aabb_min = aabb.min();
377  let aabb_max = aabb.max();
378  if line.direction.x == S::zero() {
379    if aabb_min.0.x < line.base.0.x && line.base.0.x < aabb_max.0.x {
380      let line2 = Line2::new (
381        [line.base.0.y, line.base.0.z].into(),
382        Unit2::unchecked_approx ([line.direction.y, line.direction.z].into()));
383      let aabb2 = Aabb2::with_minmax (
384        [aabb_min.0.y, aabb_min.0.z].into(),
385        [aabb_max.0.y, aabb_max.0.z].into());
386      continuous_line2_aabb2 (&line2, &aabb2).map (|((t0, p0), (t1, p1))|
387        ( (t0, [line.base.0.x, p0.0.x, p0.0.y].into()),
388          (t1, [line.base.0.x, p1.0.x, p1.0.y].into())
389        )
390      )
391    } else {
392      None
393    }
394  } else if line.direction.y == S::zero() {
395    if aabb_min.0.y < line.base.0.y && line.base.0.y < aabb_max.0.y {
396      let line2 = Line2::new (
397        [line.base.0.x, line.base.0.z].into(),
398        Unit2::unchecked_approx ([line.direction.x, line.direction.z].into()));
399      let aabb2 = Aabb2::with_minmax (
400        [aabb_min.0.x, aabb_min.0.z].into(),
401        [aabb_max.0.x, aabb_max.0.z].into());
402      continuous_line2_aabb2 (&line2, &aabb2).map (|((t0, p0), (t1, p1))|
403        ( (t0, [p0.0.x, line.base.0.y, p0.0.y].into()),
404          (t1, [p1.0.x, line.base.0.y, p1.0.y].into())
405        )
406      )
407    } else {
408      None
409    }
410  } else if line.direction.z == S::zero() {
411    if aabb_min.0.z < line.base.0.z && line.base.0.z < aabb_max.0.z {
412      let line2 = Line2::new (
413        [line.base.0.x, line.base.0.y].into(),
414        Unit2::unchecked_approx ([line.direction.x, line.direction.y].into()));
415      let aabb2 = Aabb2::with_minmax (
416        [aabb_min.0.x, aabb_min.0.y].into(),
417        [aabb_max.0.x, aabb_max.0.y].into());
418      continuous_line2_aabb2 (&line2, &aabb2).map (|((t0, p0), (t1, p1))|
419        ( (t0, [p0.0.x, p0.0.y, line.base.0.z].into()),
420          (t1, [p1.0.x, p1.0.y, line.base.0.z].into())
421        )
422      )
423    } else {
424      None
425    }
426  } else {
427    let dir_reciprocal = line.direction.map (|s| S::one() / s);
428    let (t0_x, t1_x)   = {
429      let (near_x, far_x) = if line.direction.x.is_positive() {
430        (aabb_min.0.x, aabb_max.0.x)
431      } else {
432        (aabb_max.0.x, aabb_min.0.x)
433      };
434      ( (near_x - line.base.0.x) * dir_reciprocal.x,
435        (far_x  - line.base.0.x) * dir_reciprocal.x
436      )
437    };
438    let (t0_y, t1_y) = {
439      let (near_y, far_y) = if line.direction.y.is_positive() {
440        (aabb_min.0.y, aabb_max.0.y)
441      } else {
442        (aabb_max.0.y, aabb_min.0.y)
443      };
444      ( (near_y - line.base.0.y) * dir_reciprocal.y,
445        (far_y  - line.base.0.y) * dir_reciprocal.y
446      )
447    };
448    let (t0_z, t1_z) = {
449      let (near_z, far_z) = if line.direction.z.is_positive() {
450        (aabb_min.0.z, aabb_max.0.z)
451      } else {
452        (aabb_max.0.z, aabb_min.0.z)
453      };
454      ( (near_z - line.base.0.z) * dir_reciprocal.z,
455        (far_z  - line.base.0.z) * dir_reciprocal.z
456      )
457    };
458    let interval_x = Interval::with_minmax (t0_x, t1_x);
459    let interval_y = Interval::with_minmax (t0_y, t1_y);
460    continuous_interval (&interval_x, &interval_y).and_then (|interval| {
461      let interval_z = Interval::with_minmax (t0_z, t1_z);
462      continuous_interval (&interval, &interval_z).map (|interval|{
463        let start = line.point (interval.min());
464        let end   = line.point (interval.max());
465        ( (interval.min(), start), (interval.max(), end) )
466      })
467    })
468  }
469}
470
471/// Compute the continuous intersection of a 3D line with a 3D sphere.
472///
473/// If the line and sphere intersect, the two intersection points are returned with the
474/// scalar parameter corresponding to that point along the line.
475///
476/// ```
477/// # use math_utils::*;
478/// # use math_utils::geometry::*;
479/// # use math_utils::geometry::intersect::*;
480/// let sphere = Sphere3::unit();
481/// let line = Line3::new  ([0.0, 0.0, 0.0].into(), Unit3::axis_x());
482/// assert_eq!(
483///   continuous_line3_sphere3 (&line, &sphere).unwrap(),
484///   ( (-1.0, [-1.0, 0.0, 0.0].into()),
485///     ( 1.0, [ 1.0, 0.0, 0.0].into())
486///   )
487/// );
488/// ```
489///
490/// Returns `None` if the line and sphere are tangent:
491///
492/// ```
493/// # use math_utils::*;
494/// # use math_utils::geometry::*;
495/// # use math_utils::geometry::intersect::*;
496/// let sphere = Sphere3::unit();
497/// let line   = Line3::new  ([0.0, 0.0, 1.0].into(), Unit3::axis_x());
498/// assert_eq!(continuous_line3_sphere3 (&line, &sphere), None);
499/// ```
500#[expect(clippy::type_complexity)]
501pub fn continuous_line3_sphere3 <S : Real> (line : &Line3 <S>, sphere : &Sphere3 <S>)
502  -> Option <((S, Point3 <S>), (S, Point3 <S>))>
503{
504  let two  = S::two();
505  let four = S::four();
506  // defining intersection points by the equation at^2 + bt + c = 0, solve for t using
507  // quadratic formula:
508  //
509  //  t = (-b +- sqrt (b^2 -4ac)) / 2a
510  //
511  // where a, b, and c are defined in terms of points p1, p2 of the line segment and the
512  // sphere center p3, and sphere radius r
513  let p1   = line.base;
514  #[expect(clippy::no_effect_underscore_binding)]
515  let _p2   = line.base + *line.direction;
516  let p3   = sphere.center;
517  let r    = *sphere.radius;
518  let p1p2 = &line.direction;
519  let p3p1 = p1 - p3;
520  let a    = S::one();
521  let b    = two * p1p2.dot (p3p1);
522  let c    = p3p1.norm_squared() - r * r;
523  // this is the portion of the quadratic equation inside the square root that
524  // determines whether the intersection is none, a tangent point, or a segment
525  let discriminant = b * b - four * a * c;
526  if discriminant <= S::zero() {
527    None
528  } else {
529    let discriminant_sqrt = discriminant.sqrt();
530    let frac_2a           = S::one() / (two * a);
531    let t1                = (-b - discriminant_sqrt) * frac_2a;
532    let t2                = (-b + discriminant_sqrt) * frac_2a;
533    let first  = p1 + (**p1p2) * t1;
534    let second = p1 + (**p1p2) * t2;
535    Some (((t1, first), (t2, second)))
536  }
537}
538
539/// Compute continuous intersection of a 2D line segment with a 2D AABB (rectangle).
540///
541/// The points are returned in the order given by the ordering of the points in the
542/// segment.
543///
544/// Note that a segment that is tangent to the AABB returns no intersection.
545#[expect(clippy::type_complexity)]
546pub fn continuous_segment2_aabb2 <S> (segment : &Segment2 <S>, aabb : &Aabb2 <S>)
547  -> Option <((S, Point2 <S>), (S, Point2 <S>))>
548where S : Real + std::fmt::Debug {
549  let vector = *segment.point_b() - segment.point_a();
550  let length = vector.norm();
551  let line   = Line2::new (*segment.point_a(), Unit2::unchecked (vector / length));
552  if let Some (((t0, _p0), (t1, _p1))) = continuous_line2_aabb2 (&line, aabb) {
553    let interval = Interval::with_minmax (S::zero(), length);
554    continuous_interval (&interval, &Interval::with_minmax (t0, t1)).map (
555      |interval|
556      ( (interval.min() / length, line.point (interval.min())),
557        (interval.max() / length, line.point (interval.max()))
558      )
559    )
560  } else {
561    None
562  }
563}
564
565/// Compute continuous intersection of a 2D line segment with a 2D sphere (circle).
566///
567/// The points are returned in the order given by the ordering of the points in the
568/// segment.
569///
570/// Note that a segment that is tangent to the surface of the circle returns no
571/// intersection.
572#[expect(clippy::type_complexity)]
573pub fn continuous_segment2_sphere2 <S> (segment : &Segment2 <S>, sphere : &Sphere2 <S>)
574  -> Option <((S, Point2 <S>), (S, Point2<S>))>
575where S : Real + std::fmt::Debug {
576  let vector = *segment.point_b() - segment.point_a();
577  let length = vector.norm();
578  let line   = Line2::new (*segment.point_a(), Unit2::unchecked (vector / length));
579  if let Some (((t0, _p0), (t1, _p1))) = continuous_line2_sphere2 (&line, sphere) {
580    let interval = Interval::with_minmax (S::zero(), length);
581    continuous_interval (&interval, &Interval::with_minmax (t0, t1)).map (|interval|
582      ( (interval.min() / length, line.point (interval.min())),
583        (interval.max() / length, line.point (interval.max()))
584      )
585    )
586  } else {
587    None
588  }
589}
590
591/// Compute continuous intersection of a 3D line segment with an AABB.
592///
593/// The points are returned in the order given by the ordering of the points in the
594/// segment.
595///
596/// Note that a segment that is tangent to the AABB returns no intersection.
597#[expect(clippy::type_complexity)]
598pub fn continuous_segment3_aabb3 <S> (segment : &Segment3 <S>, aabb : &Aabb3 <S>)
599  -> Option <((S, Point3 <S>), (S, Point3<S>))>
600where S : Real + num_traits::Float + approx::RelativeEq <Epsilon=S> + std::fmt::Debug {
601  let vector = *segment.point_b() - segment.point_a();
602  let length = vector.norm();
603  let line = Line3::new (*segment.point_a(), Unit3::unchecked_approx (vector / length));
604  if let Some (((t0, _p0), (t1, _p1))) = continuous_line3_aabb3 (&line, aabb) {
605    let interval = Interval::with_minmax (S::zero(), length);
606    continuous_interval (&interval, &Interval::with_minmax (t0, t1)).map (|interval|
607      ( (interval.min() / length, line.point (interval.min())),
608        (interval.max() / length, line.point (interval.max()))
609      )
610    )
611  } else {
612    None
613  }
614}
615
616/// Compute continuous intersection of a 3D line segment with a sphere.
617///
618/// The points are returned in the order given by the ordering of the points in the
619/// segment.
620///
621/// Note that a segment that is tangent to the surface of the sphere returns no
622/// intersection.
623#[expect(clippy::type_complexity)]
624pub fn continuous_segment3_sphere3 <S> (segment : &Segment3 <S>, sphere : &Sphere3 <S>)
625  -> Option <((S, Point3 <S>), (S, Point3<S>))>
626where S : OrderedField + Sqrt {
627  let two  = S::two();
628  let four = S::four();
629  // defining intersection points by the equation at^2 + bt + c = 0, solve for t using
630  // quadratic formula:
631  //
632  //  t = (-b +- sqrt (b^2 -4ac)) / 2a
633  //
634  // where a, b, and c are defined in terms of points p1, p2 of the line segment and the
635  // sphere center p3, and sphere radius r
636  let p1   = segment.point_a();
637  let p2   = segment.point_b();
638  let p3   = sphere.center;
639  let r    = *sphere.radius;
640  let p1p2 = *p2-p1;
641  let p3p1 = *p1-p3;
642  let a    = p1p2.norm_squared();
643  let b    = two * p1p2.dot (p3p1);
644  let c    = p3p1.norm_squared() - r * r;
645  // this is the portion of the quadratic equation inside the square root that
646  // determines whether the intersection is none, a tangent point, or a segment
647  let discriminant = b * b - four * a * c;
648  if discriminant <= S::zero() {
649    None
650  } else {
651    let discriminant_sqrt = discriminant.sqrt();
652    let frac_2a           = S::one() / (two * a);
653    let t1                = S::max ((-b - discriminant_sqrt) * frac_2a, S::zero());
654    let t2                = S::min ((-b + discriminant_sqrt) * frac_2a, S::one());
655    if t2 <= S::zero() || S::one() <= t1 {
656      None
657    } else {
658      let first  = *p1 + p1p2 * t1;
659      let second = *p1 + p1p2 * t2;
660      Some (((t1, first), (t2, second)))
661    }
662  }
663}
664
665/// Compute continuous intersection of a 3D line segment with an axis-aligned cylinder.
666///
667/// The points are returned in the order given by the ordering of the points in the
668/// segment.
669///
670/// Note that a segment that is tangent to the surface of the cylinder returns no
671/// intersection.
672#[expect(clippy::type_complexity)]
673pub fn continuous_segment3_cylinder3 <S> (
674  segment : &Segment3 <S>, cylinder : &Cylinder3 <S>
675) -> Option <((S, Point3 <S>), (S, Point3<S>))> where
676  S : Real + std::fmt::Debug
677{
678  let segment_aabb  = segment.aabb3();
679  let cylinder_aabb = cylinder.aabb3();
680  if !discrete_aabb3_aabb3 (&segment_aabb, &cylinder_aabb) {
681    None
682  } else {
683    let p1       = segment.point_a();
684    let p2       = segment.point_b();
685    let p3       = cylinder.center;
686    let r        = *cylinder.radius;
687    let r2       = r * r;
688    let p1p2     = *p2 - p1;
689    let p1_xy    = Point2::from ([p1.0.x, p1.0.y]);
690    let p2_xy    = Point2::from ([p2.0.x, p2.0.y]);
691    let p3_xy    = Point2::from ([p3.0.x, p3.0.y]);
692    let p3_z_max = cylinder_aabb.max().0.z;
693    let p3_z_min = cylinder_aabb.min().0.z;
694    if p1_xy == p2_xy {   // segment is aligned vertically (Z axis)
695      let d2 = (p1_xy - p3_xy).norm_squared();
696      if d2 >= r2 {
697        None
698      } else {
699        let (t1, begin_z) = if p1.0.z >= p3_z_max {
700          ((p3_z_max - p1.0.z) / p1p2.z, p3_z_max)
701        } else if p1.0.z <= p3_z_min {
702          ((p3_z_min - p1.0.z) / p1p2.z, p3_z_min)
703        } else {
704          (S::zero(), p1.0.z)
705        };
706        let (t2, end_z)   = if p2.0.z >= p3_z_max {
707          ((p3_z_max - p1.0.z) / p1p2.z, p3_z_max)
708        } else if p2.0.z <= p3_z_min {
709          ((p3_z_min - p1.0.z) / p1p2.z, p3_z_min)
710        } else {
711          (S::one(), p2.0.z)
712        };
713        let begin = [p1_xy.0.x, p1_xy.0.y, begin_z].into();
714        let end   = [p1_xy.0.x, p1_xy.0.y, end_z  ].into();
715        Some (((t1, begin), (t2, end)))
716      }
717    } else {    // segment is not aligned vertically
718      // intersect the line with the cylinder circle in the XY plane
719      let two     = S::two();
720      let four    = S::four();
721      let p1p2_xy = p1p2.xy();
722      let p3p1_xy = p1_xy - p3_xy;
723      let a       = p1p2_xy.norm_squared();
724      let b       = two * p1p2_xy.dot (p3p1_xy);
725      let c       = p3p1_xy.norm_squared() - r * r;
726      // this is the portion of the quadratic equation inside the square root that
727      // determines whether the intersection is none, a tangent point, or a segment
728      let discriminant = b * b - four * a * c;
729      if discriminant <= S::zero() {
730        None
731      } else {
732        let discriminant_sqrt = discriminant.sqrt();
733        let frac_2a           = S::one() / (two * a);
734        let t1_xy             = S::max ((-b - discriminant_sqrt) * frac_2a, S::zero());
735        let t2_xy             = S::min ((-b + discriminant_sqrt) * frac_2a, S::one());
736        if t2_xy <= S::zero() || S::one() <= t1_xy {
737          None
738        } else if let Some ((t1, t2)) = if p1.0.z == p2.0.z {
739          // segment aligned horizontally
740          Some ((t1_xy, t2_xy))
741        } else {
742          // segment not aligned horizontally;
743          // intersect the line with the top and bottom of the cylinder
744          let p1p3_z_max = p3_z_max - p1.0.z;
745          let p1p3_z_min = p3_z_min - p1.0.z;
746          let t_z_max    = S::max (S::min (p1p3_z_max / p1p2.z, S::one()), S::zero());
747          let t_z_min    = S::max (S::min (p1p3_z_min / p1p2.z, S::one()), S::zero());
748          let t1_z       = S::min (t_z_max, t_z_min);
749          let t2_z       = S::max (t_z_max, t_z_min);
750          let aabb_xy    = Interval::with_minmax (t1_xy, t2_xy);
751          let aabb_z     = Interval::with_minmax ( t1_z,  t2_z);
752          if !aabb_xy.intersects (&aabb_z) {
753            None
754          } else {
755            Some ((S::max (t1_xy, t1_z), S::min (t2_xy, t2_z)))
756          }
757        } /*then*/ {
758          debug_assert!(t1 < t2);
759          debug_assert!(t1 >= S::zero());
760          debug_assert!(t1 <  S::one());
761          debug_assert!(t2 >  S::zero());
762          debug_assert!(t2 <= S::one());
763          let first  = *p1 + p1p2 * t1;
764          let second = *p1 + p1p2 * t2;
765          Some (((t1, first), (t2, second)))
766        } else {
767          None
768        }
769      }
770    }
771  }
772}
773
774/// Compute continuous intersection of a line segment with an axis-aligned capsule.
775///
776/// The points are returned in the order given by the ordering of the points in the
777/// segment.
778///
779/// Note that a line that is tangent to the surface of the capsule returns no
780/// intersection.
781#[expect(clippy::type_complexity)]
782pub fn continuous_segment3_capsule3 <S> (
783  segment : &Segment3 <S>, capsule : &Capsule3 <S>
784) -> Option <((S, Point3 <S>), (S, Point3 <S>))> where
785  S : Real + std::fmt::Debug
786{
787  let segment_aabb = segment.aabb3();
788  let capsule_aabb = capsule.aabb3();
789  if !discrete_aabb3_aabb3 (&segment_aabb, &capsule_aabb) {
790    None
791  } else {
792    // decompose the capsule into spheres and cylinder
793    let (upper_sphere, cylinder, lower_sphere) = capsule.decompose();
794    let cylinder_result = cylinder
795      .and_then (|cylinder| segment.intersect_cylinder (&cylinder));
796    let upper_result = segment.intersect_sphere   (&upper_sphere);
797    let lower_result = segment.intersect_sphere   (&lower_sphere);
798    match (upper_result, cylinder_result, lower_result) {
799      (None, None, None) => None,
800      (one,  None, None) | (None,  one, None) | (None, None,  one) => one,
801      (Some (((t1,p1), (t2,p2))), Some (((u1,q1), (u2,q2))), None) |
802      (Some (((t1,p1), (t2,p2))), None, Some (((u1,q1), (u2,q2)))) |
803      (None, Some (((t1,p1), (t2,p2))), Some (((u1,q1), (u2,q2)))) => {
804        let first = if t1 < u1 {
805          (t1,p1)
806        } else {
807          (u1,q1)
808        };
809        let second = if t2 > u2 {
810          (t2,p2)
811        } else {
812          (u2,q2)
813        };
814        Some ((first, second))
815      }
816      ( Some (((t1,p1), (t2,p2))), Some (((u1,q1), (u2,q2))), Some (((v1,r1), (v2,r2)))
817      ) => {
818        let min1 = S::min (S::min (t1, u1), v1);
819        let max2 = S::max (S::max (t2, u2), v2);
820        let first = if min1 == t1 {
821          (t1,p1)
822        } else if min1 == u1 {
823          (u1,q1)
824        } else {
825          debug_assert_eq!(min1, v1);
826          (v1,r1)
827        };
828        let second = if max2 == t2 {
829          (t2,p2)
830        } else if max2 == u2 {
831          (u2,q2)
832        } else {
833          debug_assert_eq!(max2, v2);
834          (v2,r2)
835        };
836        Some ((first, second))
837      }
838    }
839  }
840}
841
842/// Discrete intersection test of 2D spheres.
843///
844/// Spheres that are merely touching are not counted as intersecting:
845///
846/// ```
847/// # use math_utils::num_traits::One;
848/// # use math_utils::*;
849/// # use math_utils::geometry::*;
850/// # use math_utils::geometry::intersect::*;
851/// let r = Positive::one();
852/// let a = Sphere2 { center: [ 1.0, 0.0].into(), radius: r };
853/// let b = Sphere2 { center: [-1.0, 0.0].into(), radius: r };
854/// assert!(!discrete_sphere2_sphere2 (&a, &b));
855/// ```
856#[inline]
857pub fn discrete_sphere2_sphere2 <S : OrderedRing> (a : &Sphere2 <S>, b : &Sphere2 <S>)
858  -> bool
859{
860  let r_ab = *(a.radius + b.radius);
861  (b.center - a.center).self_dot() < r_ab * r_ab
862}
863
864/// Discrete intersection test of 3D spheres.
865///
866/// Spheres that are merely touching are not counted as intersecting:
867///
868/// ```
869/// # use math_utils::num_traits::One;
870/// # use math_utils::*;
871/// # use math_utils::geometry::*;
872/// # use math_utils::geometry::intersect::*;
873/// let r = Positive::one();
874/// let a = Sphere3 { center: [ 1.0, 0.0,  0.0].into(), radius: r };
875/// let b = Sphere3 { center: [-1.0, 0.0,  0.0].into(), radius: r };
876/// assert!(!discrete_sphere3_sphere3 (&a, &b));
877/// ```
878#[inline]
879pub fn discrete_sphere3_sphere3 <S : OrderedRing> (a : &Sphere3 <S>, b : &Sphere3 <S>)
880  -> bool
881{
882  let r_ab = *(a.radius + b.radius);
883  (b.center - a.center).self_dot() < r_ab * r_ab
884}
885
886#[cfg(test)]
887mod tests {
888  use super::*;
889
890  #[test]
891  fn line2_aabb2() {
892    use std::f64::consts::SQRT_2;
893    let aabb = Aabb2::with_minmax ([-1.0, -1.0].into(), [1.0, 1.0].into());
894    let line = Line2::new ([0.0, 0.0].into(), Unit2::axis_y());
895    assert_eq!(
896      continuous_line2_aabb2 (&line, &aabb).unwrap(),
897      ((-1.0, [ 0.0, -1.0].into()), (1.0, [ 0.0, 1.0].into())));
898    let line = Line2::new ([0.0, 0.0].into(), Unit2::axis_x());
899    assert_eq!(
900      continuous_line2_aabb2 (&line, &aabb).unwrap(),
901      ((-1.0, [-1.0,  0.0].into()), (1.0, [ 1.0, 0.0].into())));
902    let line = Line2::new ([0.0, 0.0].into(), Unit2::normalize ([1.0, 1.0].into()));
903    assert_eq!(
904      continuous_line2_aabb2 (&line, &aabb).unwrap(),
905      ((-SQRT_2, [-1.0, -1.0].into()), (SQRT_2, [ 1.0,  1.0].into())));
906    let line = Line2::new ([0.0, 0.0].into(), Unit2::normalize ([-1.0, -1.0].into()));
907    assert_eq!(
908      continuous_line2_aabb2 (&line, &aabb).unwrap(),
909      ((-SQRT_2, [1.0,  1.0].into()), (SQRT_2, [-1.0, -1.0].into())));
910    let line = Line2::new ([0.0, 3.0].into(), Unit2::normalize ([-1.0, -1.0].into()));
911    assert!(continuous_line2_aabb2 (&line, &aabb).is_none());
912    let line = Line2::new ([0.0, -3.0].into(), Unit2::normalize ([1.0,  1.0].into()));
913    assert!(continuous_line2_aabb2 (&line, &aabb).is_none());
914  }
915
916  #[test]
917  fn line3_aabb3() {
918    use approx::assert_ulps_eq;
919    let aabb = Aabb3::with_minmax ([-1.0, -1.0, -1.0].into(), [1.0, 1.0, 1.0].into());
920    let line = Line3::new ([0.0, 0.0, 0.0].into(), Unit3::axis_z());
921    assert_eq!(
922      continuous_line3_aabb3 (&line, &aabb).unwrap(),
923      ((-1.0, [0.0, 0.0, -1.0].into()), (1.0, [0.0, 0.0, 1.0].into())));
924    let line = Line3::new ([0.0, 0.0, 0.0].into(), Unit3::axis_y());
925    assert_eq!(
926      continuous_line3_aabb3 (&line, &aabb).unwrap(),
927      ((-1.0, [0.0, -1.0,  0.0].into()), (1.0, [0.0, 1.0, 0.0].into())));
928    {
929      let line = Line3::new (
930        [0.0, 0.0, 0.0].into(),
931        Unit3::normalize ([1.0, 1.0, 1.0].into()));
932      let result = continuous_line3_aabb3 (&line, &aabb).unwrap();
933      assert_ulps_eq!((result.0).0, -f64::sqrt_3());
934      assert_ulps_eq!((result.1).0, f64::sqrt_3());
935      assert_eq!((result.0).1, [-1.0, -1.0, -1.0].into());
936      assert_eq!((result.1).1, [ 1.0,  1.0,  1.0].into());
937    }
938    {
939      let line = Line3::new (
940        [0.0, 0.0, 0.0].into(),
941        Unit3::normalize ([-1.0, -1.0, -1.0].into()));
942      let result = continuous_line3_aabb3 (&line, &aabb).unwrap();
943      assert_ulps_eq!((result.0).0, -f64::sqrt_3());
944      assert_ulps_eq!((result.1).0, f64::sqrt_3());
945      assert_eq!((result.0).1, [ 1.0,  1.0,  1.0].into());
946      assert_eq!((result.1).1, [-1.0, -1.0, -1.0].into());
947    }
948    let line = Line3::new (
949      [0.0, 0.0, 3.0].into(),
950      Unit3::normalize ([-1.0, -1.0, -1.0].into()));
951    assert!(continuous_line3_aabb3 (&line, &aabb).is_none());
952    let line = Line3::new (
953      [0.0, 0.0, -3.0].into(),
954      Unit3::normalize ([1.0, 1.0, 1.0].into()));
955    assert!(continuous_line3_aabb3 (&line, &aabb).is_none());
956  }
957
958  #[test]
959  fn segment3_sphere3() {
960    let sphere  = shape::Sphere::unit().sphere3 (Point3::origin());
961    let segment = Segment3::new ([-2.0, 0.0, 0.0].into(), [2.0, 0.0, 0.0].into());
962    assert_eq!(
963      continuous_segment3_sphere3 (&segment, &sphere).unwrap(),
964      ((0.25, [-1.0, 0.0, 0.0].into()), (0.75, [1.0, 0.0, 0.0].into())));
965    let segment = Segment3::new ([2.0, 0.0, 0.0].into(), [-2.0, 0.0, 0.0].into());
966    assert_eq!(
967      continuous_segment3_sphere3 (&segment, &sphere).unwrap(),
968      ((0.25, [1.0, 0.0, 0.0].into()), (0.75, [-1.0, 0.0, 0.0].into())));
969    let segment = Segment3::new ([0.0, 0.0, 0.0].into(), [0.0, 0.0, 2.0].into());
970    assert_eq!(
971      continuous_segment3_sphere3 (&segment, &sphere).unwrap(),
972      ((0.0, [0.0, 0.0, 0.0].into()), (0.5, [0.0, 0.0, 1.0].into())));
973    let segment = Segment3::new ([0.0, 0.0, -2.0].into(), [0.0, 0.0, 0.0].into());
974    assert_eq!(
975      continuous_segment3_sphere3 (&segment, &sphere).unwrap(),
976      ((0.5, [0.0, 0.0, -1.0].into()), (1.0, [0.0, 0.0, 0.0].into())));
977  }
978
979  #[test]
980  fn segment3_cylinder3() {
981    let cylinder = shape::Cylinder::unit().cylinder3 (Point3::origin());
982    let segment = Segment3::new ([-2.0, 0.0, 0.0].into(), [2.0, 0.0, 0.0].into());
983    assert_eq!(
984      continuous_segment3_cylinder3 (&segment, &cylinder).unwrap(),
985      ((0.25, [-1.0, 0.0, 0.0].into()), (0.75, [1.0, 0.0, 0.0].into())));
986    let segment = Segment3::new ([2.0, 0.0, 0.0].into(), [-2.0, 0.0, 0.0].into());
987    assert_eq!(
988      continuous_segment3_cylinder3 (&segment, &cylinder).unwrap(),
989      ((0.25, [1.0, 0.0, 0.0].into()), (0.75, [-1.0, 0.0, 0.0].into())));
990    let segment = Segment3::new ([0.0, 0.0, 0.0].into(), [0.0, 0.0, 2.0].into());
991    assert_eq!(
992      continuous_segment3_cylinder3 (&segment, &cylinder).unwrap(),
993      ((0.0, [0.0, 0.0, 0.0].into()), (0.5, [0.0, 0.0, 1.0].into())));
994    let segment = Segment3::new ([0.0, 0.0, -2.0].into(), [0.0, 0.0, 0.0].into());
995    assert_eq!(
996      continuous_segment3_cylinder3 (&segment, &cylinder).unwrap(),
997      ((0.5, [0.0, 0.0, -1.0].into()), (1.0, [0.0, 0.0, 0.0].into())));
998  }
999
1000  #[test]
1001  fn segment3_capsule3() {
1002    let capsule = shape::Capsule::noisy (1.0, 1.0).capsule3 (Point3::origin());
1003    let segment = Segment3::new ([-2.0, 0.0, 0.0].into(), [2.0, 0.0, 0.0].into());
1004    assert_eq!(
1005      continuous_segment3_capsule3 (&segment, &capsule).unwrap(),
1006      ((0.25, [-1.0, 0.0, 0.0].into()), (0.75, [1.0, 0.0, 0.0].into())));
1007    let segment = Segment3::new ([2.0, 0.0, 0.0].into(), [-2.0, 0.0, 0.0].into());
1008    assert_eq!(
1009      continuous_segment3_capsule3 (&segment, &capsule).unwrap(),
1010      ((0.25, [1.0, 0.0, 0.0].into()), (0.75, [-1.0, 0.0, 0.0].into())));
1011    let segment = Segment3::new ([0.0, 0.0, 0.0].into(), [0.0, 0.0, 4.0].into());
1012    assert_eq!(
1013      continuous_segment3_capsule3 (&segment, &capsule).unwrap(),
1014      ((0.0, [0.0, 0.0, 0.0].into()), (0.5, [0.0, 0.0, 2.0].into())));
1015    let segment = Segment3::new ([0.0, 0.0, -4.0].into(), [0.0, 0.0, 0.0].into());
1016    assert_eq!(
1017      continuous_segment3_capsule3 (&segment, &capsule).unwrap(),
1018      ((0.5, [0.0, 0.0, -2.0].into()), (1.0, [0.0, 0.0, 0.0].into())));
1019  }
1020
1021  #[test]
1022  fn segment3_aabb3() {
1023    let aabb    = Aabb3::with_minmax ([1.0, -0.5, 0.0].into(), [2.0, 0.5, 1.0].into());
1024    let segment = Segment3::new ([-1.0, 0.0, 0.5].into(), [2.0, 0.0, 0.5].into());
1025    assert_eq!(
1026      continuous_segment3_aabb3 (&segment, &aabb).unwrap(),
1027      ((2.0/3.0, [1.0, 0.0, 0.5].into()), (1.0, [2.0, 0.0, 0.5].into())));
1028  }
1029
1030} // end tests