traffic_sim/math/curve/
algorithms.rs

1use super::ParametricCurve2d;
2use crate::{math::Point2d, util::Interval};
3use cgmath::prelude::*;
4
5/// Projects a point onto a parametric curve.
6pub fn project_point_onto_curve(
7    curve: &impl ParametricCurve2d,
8    point: Point2d,
9    max_error: f64,
10    t0: Option<f64>,
11) -> Option<f64> {
12    let bounds = curve.bounds();
13
14    // Get initial guess for `t`
15    let mut t = t0.unwrap_or_else(|| {
16        (0..=8)
17            .map(|i| bounds.lerp(i as f64 / 8.0))
18            .map(|t| (t, (point - curve.sample(t)).magnitude()))
19            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
20            .unwrap()
21            .0
22    });
23    let (mut p, mut p_dt) = (curve.sample(t), curve.sample_dt(t));
24
25    // Refine `t` with Newton's method
26    for _ in 0..64 {
27        let error = p_dt.dot(point - p) / p_dt.magnitude();
28        t += error;
29        if error.abs() < max_error || !bounds.contains(t) {
30            return Some(t);
31        }
32        (p, p_dt) = (curve.sample(t), curve.sample_dt(t));
33    }
34
35    None
36}
37
38/// Finds intersection between two curves, if one exists.
39pub fn intersect_curves(
40    curve1: &impl ParametricCurve2d,
41    curve2: &impl ParametricCurve2d,
42) -> Option<(f64, f64)> {
43    let mut pos1 = curve1.bounds().min;
44    let mut pos2 = None;
45    while pos1 < curve1.bounds().max {
46        let point1 = curve1.sample(pos1);
47        pos2 = project_point_onto_curve(curve2, point1, 0.1, pos2);
48        if let Some(pos2) = pos2 {
49            let point2 = curve2.sample(pos2);
50            let distance = point1.distance(point2);
51            if distance < 0.01 {
52                return Some((pos1, pos2));
53            } else {
54                pos1 += distance;
55            }
56        } else {
57            pos1 += 1.0;
58        }
59    }
60    None
61}
62
63/// Finds a set of evenly spaced points along the given parametric curve.
64pub fn equidistant_points_along_curve(
65    curve: &impl ParametricCurve2d,
66    dist: f64,
67) -> (Vec<Point2d>, f64) {
68    let end_ts = curve.bounds();
69    let end_ps = [curve.sample(end_ts.min), curve.sample(end_ts.max)];
70
71    let mut ts = end_ts;
72    let mut ps = end_ps;
73    let mut dists = Interval::new(0.0, (ps[1] - ps[0]).magnitude());
74
75    let mut points = vec![end_ps[0]];
76    let mut last_p = end_ps[0];
77
78    while dists.max > dist {
79        for _ in 0..100 {
80            let new_t = ts.lerp(dists.inv_lerp(dist));
81            let new_p = curve.sample(new_t);
82            let new_dist = (new_p - last_p).magnitude();
83            let f = new_dist / dist;
84
85            if f < 0.99 {
86                ts.min = new_t;
87                ps[0] = new_p;
88                dists.min = new_dist;
89            } else if f > 1.01 {
90                ts.max = new_t;
91                ps[1] = new_p;
92                dists.max = new_dist;
93            } else {
94                // Append the point
95                points.push(new_p);
96                last_p = new_p;
97
98                // Setup for the next iteration
99                ts = Interval::new(new_t, end_ts.max);
100                ps = [new_p, end_ps[1]];
101                dists = Interval::new(0.0, (ps[1] - ps[0]).magnitude());
102
103                break;
104            }
105        }
106    }
107
108    let last_point = *points.last().unwrap();
109    let end_vec = ps[1] - last_point;
110    let end_magnitude = end_vec.magnitude();
111    let mut length = (points.len() - 1) as f64 * dist;
112    if end_magnitude > 0.001 * dist {
113        length += end_magnitude;
114        points.push(last_point + end_vec.normalize_to(dist));
115    }
116
117    (points, length)
118}
119
120/// Approximates a curve by subdividing it until all segments are no longer than `max_length` units in length.
121pub fn subdivided_points_along_curve(
122    curve: &impl ParametricCurve2d,
123    max_length: f64,
124) -> Vec<Point2d> {
125    SubdividedSamples::new(curve, max_length)
126        .map(|(_, p)| p)
127        .collect()
128}
129
130/// Approximates a curve by subdividing it until all segments are no longer than `max_length` units in length.
131pub fn subdivided_samples_along_curve(
132    curve: &impl ParametricCurve2d,
133    max_length: f64,
134) -> impl Iterator<Item = (f64, Point2d)> + '_ {
135    SubdividedSamples::new(curve, max_length)
136}
137
138/// See [subdivided_samples_along_curve].
139struct SubdividedSamples<'a, C> {
140    curve: &'a C,
141    stack: Vec<(f64, Point2d)>,
142    length2: f64,
143}
144
145impl<'a, C: ParametricCurve2d> SubdividedSamples<'a, C> {
146    fn new(curve: &'a C, max_length: f64) -> Self {
147        let Interval { min, max } = curve.bounds();
148        let mid = 0.5 * (min + max);
149        Self {
150            curve,
151            stack: vec![
152                (max, curve.sample(max)),
153                (mid, curve.sample(mid)),
154                (min, curve.sample(min)),
155            ],
156            length2: max_length.powi(2),
157        }
158    }
159}
160
161impl<'a, C: ParametricCurve2d> Iterator for SubdividedSamples<'a, C> {
162    type Item = (f64, Point2d);
163
164    fn next(&mut self) -> Option<Self::Item> {
165        let (t1, p1) = self.stack.pop()?;
166        if let Some((mut t2, mut p2)) = self.stack.last().copied() {
167            while (p2 - p1).magnitude2() > self.length2 {
168                let mid_t = 0.5 * (t1 + t2);
169                (t2, p2) = (mid_t, self.curve.sample(mid_t));
170                self.stack.push((t2, p2));
171            }
172        }
173        Some((t1, p1))
174    }
175}
176
177#[cfg(test)]
178mod test {
179    use super::*;
180    use crate::math::LineSegment2d;
181    use assert_approx_eq::assert_approx_eq;
182
183    #[test]
184    pub fn equidistant_points_along_curve_is_stable() {
185        for i in 0..100 {
186            let len = 0.1 * i as f64;
187            let points = equidistant_points_along_curve(
188                &LineSegment2d::from_ends(Point2d::new(10.0, 10.0), Point2d::new(10.0 + len, 10.0)),
189                0.5,
190            );
191            assert_approx_eq!(points.1, len);
192            for point in points.0.into_iter() {
193                assert!(!point.x.is_nan() && !point.y.is_nan());
194            }
195        }
196    }
197}