1#![allow(dead_code, clippy::needless_range_loop, clippy::type_complexity)]
2
3pub 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
63pub 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}