Skip to main content

rust_robotics_planning/
bezier_path.rs

1#![allow(dead_code, clippy::needless_range_loop, clippy::type_complexity)]
2
3//! Bezier curve path computation (basic functions)
4//!
5//! Provides fundamental Bezier curve computation including
6//! Bernstein polynomials, control point manipulation, and curvature.
7
8pub fn calc_4points_bezier_path(
9    x_start: (f64, f64, f64),
10    x_goal: (f64, f64, f64),
11    offset: f64,
12) -> (Vec<(f64, f64)>, [(f64, f64); 4]) {
13    let x_diff = (x_start.0 - x_goal.0, x_start.1 - x_goal.1);
14    let dist = ((x_diff.0).powi(2) + (x_diff.1).powi(2)).sqrt() / offset;
15    let control_points = [
16        (x_start.0, x_start.1),
17        (
18            x_start.0 + dist * (x_start.2).cos(),
19            x_start.1 + dist * (x_start.2).sin(),
20        ),
21        (
22            x_goal.0 - dist * (x_goal.2).cos(),
23            x_goal.1 - dist * (x_goal.2).sin(),
24        ),
25        (x_goal.0, x_goal.1),
26    ];
27    let path = calc_bezier_path(control_points, 100);
28    (path, control_points)
29}
30
31pub fn calc_bezier_path(control_points: [(f64, f64); 4], n_points: usize) -> Vec<(f64, f64)> {
32    if n_points == 0 {
33        return Vec::new();
34    }
35
36    if n_points == 1 {
37        return vec![bezier(0.0, control_points)];
38    }
39
40    let mut traj: Vec<(f64, f64)> = Vec::with_capacity(n_points);
41    for i in 0..n_points {
42        let t = (i as f64) / ((n_points as f64) - 1.0);
43        traj.push(bezier(t, control_points));
44    }
45    traj
46}
47
48pub fn bernstein_poly(n: i32, i: i32, t: f64) -> f64 {
49    (binom(n, i) as f64) * t.powi(i) * (1. - t).powi(n - i)
50}
51
52pub fn bezier(t: f64, control_points: [(f64, f64); 4]) -> (f64, f64) {
53    let n = control_points.len() - 1;
54    let mut point = (0., 0.);
55    for i in 0..n + 1 {
56        let ber_poly = bernstein_poly(n as i32, i as i32, t);
57        point.0 += ber_poly * control_points[i].0;
58        point.1 += ber_poly * control_points[i].1;
59    }
60    point
61}
62
63/// Ref: Donald E. Knuth, "The Art of Computer Programming (2)"
64pub fn binom(n: i32, k: i32) -> i32 {
65    (0..n + 1).rev().zip(1..k + 1).fold(1, |mut r, (n, d)| {
66        r *= n;
67        r /= d;
68        r
69    })
70}
71
72pub fn bezier_derivatives_control_points(
73    control_points: [(f64, f64); 4],
74    n_derivatives: usize,
75) -> Vec<Vec<(f64, f64)>> {
76    let mut w = vec![control_points.to_vec()];
77    for i in 0..n_derivatives {
78        let current = &w[i];
79        let n = current.len();
80        if n <= 1 {
81            break;
82        }
83        let next = (0..n - 1)
84            .map(|j| {
85                (
86                    (n - 1) as f64 * (current[j + 1].0 - current[j].0),
87                    (n - 1) as f64 * (current[j + 1].1 - current[j].1),
88                )
89            })
90            .collect();
91        w.push(next);
92    }
93    w
94}
95
96pub fn curvature(dx: f64, dy: f64, ddx: f64, ddy: f64) -> f64 {
97    (dx * ddy - dy * ddx) / (dx.powi(2) + dy.powi(2)).powf(3. / 2.)
98}
99
100#[cfg(test)]
101#[allow(clippy::excessive_precision)]
102mod tests {
103    use super::*;
104
105    type PathFingerprint = (f64, f64, f64, f64);
106
107    fn assert_point_close(actual: (f64, f64), expected: (f64, f64), tolerance: f64) {
108        assert!(
109            (actual.0 - expected.0).abs() < tolerance,
110            "x mismatch: actual={actual:?}, expected={expected:?}"
111        );
112        assert!(
113            (actual.1 - expected.1).abs() < tolerance,
114            "y mismatch: actual={actual:?}, expected={expected:?}"
115        );
116    }
117
118    fn path_fingerprint(path: &[(f64, f64)]) -> PathFingerprint {
119        let sum_x = path.iter().map(|point| point.0).sum();
120        let sum_y = path.iter().map(|point| point.1).sum();
121        let weighted_sum_x = path
122            .iter()
123            .enumerate()
124            .map(|(index, point)| (index + 1) as f64 * point.0)
125            .sum();
126        let weighted_sum_y = path
127            .iter()
128            .enumerate()
129            .map(|(index, point)| (index + 1) as f64 * point.1)
130            .sum();
131        (sum_x, sum_y, weighted_sum_x, weighted_sum_y)
132    }
133
134    fn assert_path_fingerprint_close(
135        actual: PathFingerprint,
136        expected: PathFingerprint,
137        tolerance: f64,
138    ) {
139        assert!(
140            (actual.0 - expected.0).abs() < tolerance,
141            "sum_x mismatch: actual={actual:?}, expected={expected:?}"
142        );
143        assert!(
144            (actual.1 - expected.1).abs() < tolerance,
145            "sum_y mismatch: actual={actual:?}, expected={expected:?}"
146        );
147        assert!(
148            (actual.2 - expected.2).abs() < tolerance,
149            "weighted_sum_x mismatch: actual={actual:?}, expected={expected:?}"
150        );
151        assert!(
152            (actual.3 - expected.3).abs() < tolerance,
153            "weighted_sum_y mismatch: actual={actual:?}, expected={expected:?}"
154        );
155    }
156
157    #[test]
158    fn test_bezier_path() {
159        let start = (10., 1., std::f64::consts::PI);
160        let end = (0., -3., -45.0 / 180.0 * std::f64::consts::PI);
161        let (path, cp) = calc_4points_bezier_path(start, end, 3.0);
162        assert!(!path.is_empty());
163        assert_eq!(cp.len(), 4);
164    }
165
166    #[test]
167    fn test_binom() {
168        assert_eq!(binom(4, 2), 6);
169        assert_eq!(binom(3, 0), 1);
170        assert_eq!(binom(3, 3), 1);
171    }
172
173    #[test]
174    fn test_calc_4points_bezier_path_matches_upstream_main_example() {
175        let start = (10.0, 1.0, std::f64::consts::PI);
176        let end = (0.0, -3.0, -45.0_f64.to_radians());
177        let (path, control_points) = calc_4points_bezier_path(start, end, 3.0);
178
179        let expected_control_points = [
180            (10.0, 1.0),
181            (6.409_890_128_576_997, 1.0),
182            (-2.538_591_035_287_97, -0.461_408_964_712_031),
183            (0.0, -3.0),
184        ];
185        let expected_samples = [
186            (0usize, 10.0, 1.0),
187            (25, 6.526_392_761_056_504, 0.726_609_535_974_175),
188            (50, 2.630_199_272_944_397, -0.068_812_597_489_713),
189            (75, -0.060_978_738_129_225, -1.349_142_512_471_283),
190            (99, 0.0, -3.0),
191        ];
192
193        assert_eq!(path.len(), 100);
194        for (actual, expected) in control_points
195            .iter()
196            .copied()
197            .zip(expected_control_points.iter().copied())
198        {
199            assert_point_close(actual, expected, 1e-12);
200        }
201        for (index, x, y) in expected_samples {
202            assert_point_close(path[index], (x, y), 1e-12);
203        }
204    }
205
206    #[test]
207    fn test_bezier_derivatives_control_points_match_upstream_main_example() {
208        let control_points = [
209            (10.0, 1.0),
210            (6.409_890_128_576_997, 1.0),
211            (-2.538_591_035_287_97, -0.461_408_964_712_031),
212            (0.0, -3.0),
213        ];
214        let derivatives = bezier_derivatives_control_points(control_points, 2);
215        let expected_first = [
216            (-10.770_329_614_269_009, 0.0),
217            (-26.845_443_491_594_9, -4.384_226_894_136_093),
218            (7.615_773_105_863_909, -7.615_773_105_863_908),
219        ];
220        let expected_second = [
221            (-32.150_227_754_651_78, -8.768_453_788_272_19),
222            (68.922_433_194_917_62, -6.463_092_423_455_629),
223        ];
224
225        assert_eq!(derivatives.len(), 3);
226        assert_eq!(derivatives[1].len(), expected_first.len());
227        assert_eq!(derivatives[2].len(), expected_second.len());
228
229        for (actual, expected) in derivatives[1]
230            .iter()
231            .copied()
232            .zip(expected_first.iter().copied())
233        {
234            assert_point_close(actual, expected, 1e-12);
235        }
236        for (actual, expected) in derivatives[2]
237            .iter()
238            .copied()
239            .zip(expected_second.iter().copied())
240        {
241            assert_point_close(actual, expected, 1e-12);
242        }
243    }
244
245    #[test]
246    fn test_calc_4points_bezier_path_matches_upstream_main2_offset_sweep() {
247        let start = (10.0, 1.0, std::f64::consts::PI);
248        let end = (0.0, -3.0, -45.0_f64.to_radians());
249        let cases = [
250            (
251                1.0,
252                [
253                    (0usize, 10.0, 1.0),
254                    (25, 2.761_187_127_865_844, 1.452_632_145_801_054),
255                    (50, -1.957_892_182_702_414, 1.854_166_206_916_617),
256                    (75, -3.139_220_823_564_846, 0.770_058_618_915_284),
257                    (99, 0.0, -3.0),
258                ],
259                (
260                    44.990_387_229_033_885,
261                    88.471_152_619_864_35,
262                    -7_004.767_925_192_914,
263                    2_313.162_758_091_821,
264                ),
265            ),
266            (
267                2.0,
268                [
269                    (0usize, 10.0, 1.0),
270                    (25, 5.585_091_352_758_839, 0.908_115_188_430_895),
271                    (50, 1.483_176_409_032_695, 0.411_932_103_611_869),
272                    (75, -0.830_539_259_488_13, -0.819_342_229_624_641),
273                    (99, 0.0, -3.0),
274                ],
275                (
276                    272.495_193_614_516_95,
277                    -5.764_423_690_067_827,
278                    4_097.949_336_726_745,
279                    -3_378.285_301_224_811_5,
280                ),
281            ),
282            (
283                3.0,
284                [
285                    (0usize, 10.0, 1.0),
286                    (25, 6.526_392_761_056_504, 0.726_609_535_974_175),
287                    (50, 2.630_199_272_944_397, -0.068_812_597_489_713),
288                    (75, -0.060_978_738_129_225, -1.349_142_512_471_283),
289                    (99, 0.0, -3.0),
290                ],
291                (
292                    348.330_129_076_344_6,
293                    -37.176_282_460_045_215,
294                    7_798.855_090_699_961,
295                    -5_275.434_654_330_354,
296                ),
297            ),
298            (
299                4.0,
300                [
301                    (0usize, 10.0, 1.0),
302                    (25, 6.997_043_465_205_337, 0.635_856_709_745_815),
303                    (50, 3.203_710_704_900_249, -0.309_184_948_040_505),
304                    (75, 0.323_801_522_550_227, -1.614_042_653_894_604),
305                    (99, 0.0, -3.0),
306                ],
307                (
308                    386.247_596_807_258_45,
309                    -52.882_211_845_033_93,
310                    9_649.307_967_686_569,
311                    -6_224.009_330_883_126_5,
312                ),
313            ),
314        ];
315
316        for (offset, expected_samples, expected_fingerprint) in cases {
317            let (path, _) = calc_4points_bezier_path(start, end, offset);
318            assert_eq!(path.len(), 100);
319            for (index, x, y) in expected_samples {
320                assert_point_close(path[index], (x, y), 1e-12);
321            }
322            assert_path_fingerprint_close(path_fingerprint(&path), expected_fingerprint, 1e-9);
323        }
324    }
325}