1#![allow(dead_code, clippy::needless_range_loop, clippy::type_complexity)]
2
3#[derive(Debug, Clone)]
13pub struct BSplinePath {
14 pub path: Vec<(f64, f64)>,
16 pub heading: Vec<f64>,
18 pub curvature: Vec<f64>,
20}
21
22pub fn generate_bspline_path(
34 control_x: &[f64],
35 control_y: &[f64],
36 n_path_points: usize,
37 degree: usize,
38) -> Option<BSplinePath> {
39 let n = control_x.len();
40 if n != control_y.len() || n < 2 || n_path_points == 0 {
41 return None;
42 }
43 let degree = degree.min(n - 1);
44 if degree == 0 {
45 return None;
46 }
47
48 let knots = generate_clamped_knot_vector(n, degree);
49
50 let t_min = knots[degree];
51 let t_max = knots[n]; let mut path = Vec::with_capacity(n_path_points);
54 let mut heading = Vec::with_capacity(n_path_points);
55 let mut curvature = Vec::with_capacity(n_path_points);
56
57 for i in 0..n_path_points {
58 let t = if n_path_points == 1 {
59 t_min
60 } else {
61 t_min + (t_max - t_min) * (i as f64) / ((n_path_points - 1) as f64)
62 };
63
64 let t = t.min(t_max - 1e-10);
66
67 let (x, y) = eval_bspline(t, control_x, control_y, degree, &knots);
68 let (dx, dy) = eval_bspline_derivative(t, control_x, control_y, degree, &knots, 1);
69 let (ddx, ddy) = eval_bspline_derivative(t, control_x, control_y, degree, &knots, 2);
70
71 path.push((x, y));
72 heading.push(dy.atan2(dx));
73
74 let denom = (dx * dx + dy * dy).powf(2.0 / 3.0);
75 let k = if denom.abs() > 1e-15 {
76 (ddy * dx - ddx * dy) / denom
77 } else {
78 0.0
79 };
80 curvature.push(k);
81 }
82
83 Some(BSplinePath {
84 path,
85 heading,
86 curvature,
87 })
88}
89
90pub fn interpolate_bspline_path(
104 x: &[f64],
105 y: &[f64],
106 n_path_points: usize,
107 degree: usize,
108) -> Option<BSplinePath> {
109 let n = x.len();
110 if n != y.len() || n < 2 || n_path_points == 0 {
111 return None;
112 }
113 let degree = degree.min(n - 1);
114 if degree == 0 {
115 return None;
116 }
117
118 let distances = calc_normalised_cumulative_distances(x, y);
120
121 let ctrl_x = fit_bspline_interpolation(&distances, x, degree)?;
123 let ctrl_y = fit_bspline_interpolation(&distances, y, degree)?;
124
125 generate_bspline_path(&ctrl_x, &ctrl_y, n_path_points, degree)
126}
127
128fn generate_clamped_knot_vector(n: usize, degree: usize) -> Vec<f64> {
137 let m = n + degree + 1;
138 let mut knots = Vec::with_capacity(m);
139 for i in 0..m {
140 if i <= degree {
141 knots.push(0.0);
142 } else if i >= n {
143 knots.push((n - degree) as f64);
144 } else {
145 knots.push((i - degree) as f64);
146 }
147 }
148 knots
149}
150
151fn eval_bspline(t: f64, cx: &[f64], cy: &[f64], degree: usize, knots: &[f64]) -> (f64, f64) {
153 let n = cx.len();
154 let mut x = 0.0;
155 let mut y = 0.0;
156 for i in 0..n {
157 let b = bspline_basis(i, degree, t, knots);
158 x += b * cx[i];
159 y += b * cy[i];
160 }
161 (x, y)
162}
163
164fn eval_bspline_derivative(
166 t: f64,
167 cx: &[f64],
168 cy: &[f64],
169 degree: usize,
170 knots: &[f64],
171 order: usize,
172) -> (f64, f64) {
173 if order == 0 {
174 return eval_bspline(t, cx, cy, degree, knots);
175 }
176 if order > degree {
177 return (0.0, 0.0);
178 }
179
180 let mut dx: Vec<f64> = cx.to_vec();
182 let mut dy: Vec<f64> = cy.to_vec();
183 let mut current_knots = knots.to_vec();
184 let mut current_degree = degree;
185
186 for _ in 0..order {
187 let nn = dx.len();
188 if nn <= 1 {
189 return (0.0, 0.0);
190 }
191 let mut new_dx = Vec::with_capacity(nn - 1);
192 let mut new_dy = Vec::with_capacity(nn - 1);
193 for i in 0..nn - 1 {
194 let denom = current_knots[i + current_degree + 1] - current_knots[i + 1];
195 let factor = if denom.abs() > 1e-15 {
196 current_degree as f64 / denom
197 } else {
198 0.0
199 };
200 new_dx.push(factor * (dx[i + 1] - dx[i]));
201 new_dy.push(factor * (dy[i + 1] - dy[i]));
202 }
203 dx = new_dx;
204 dy = new_dy;
205 current_knots = current_knots[1..current_knots.len() - 1].to_vec();
207 current_degree -= 1;
208 }
209
210 eval_bspline(t, &dx, &dy, current_degree, ¤t_knots)
211}
212
213fn bspline_basis(i: usize, degree: usize, t: f64, knots: &[f64]) -> f64 {
215 if degree == 0 {
216 return if knots[i] <= t && t < knots[i + 1] {
217 1.0
218 } else {
219 0.0
220 };
221 }
222
223 let mut left = 0.0;
224 let denom_l = knots[i + degree] - knots[i];
225 if denom_l.abs() > 1e-15 {
226 left = (t - knots[i]) / denom_l * bspline_basis(i, degree - 1, t, knots);
227 }
228
229 let mut right = 0.0;
230 let denom_r = knots[i + degree + 1] - knots[i + 1];
231 if denom_r.abs() > 1e-15 {
232 right = (knots[i + degree + 1] - t) / denom_r * bspline_basis(i + 1, degree - 1, t, knots);
233 }
234
235 left + right
236}
237
238fn calc_normalised_cumulative_distances(x: &[f64], y: &[f64]) -> Vec<f64> {
240 let n = x.len();
241 let mut dists = Vec::with_capacity(n);
242 dists.push(0.0);
243 for i in 1..n {
244 let dx = x[i] - x[i - 1];
245 let dy = y[i] - y[i - 1];
246 dists.push(dists[i - 1] + (dx * dx + dy * dy).sqrt());
247 }
248 let total = *dists.last().unwrap();
249 if total > 1e-15 {
250 for d in &mut dists {
251 *d /= total;
252 }
253 }
254 dists
255}
256
257fn fit_bspline_interpolation(params: &[f64], values: &[f64], degree: usize) -> Option<Vec<f64>> {
262 let n = params.len();
263
264 let knots = build_interpolation_knot_vector(params, n, degree);
268
269 let mut mat = nalgebra::DMatrix::zeros(n, n);
271 for row in 0..n {
272 let t = if row == n - 1 {
273 params[row] - 1e-10 } else {
275 params[row]
276 };
277 for col in 0..n {
278 mat[(row, col)] = bspline_basis(col, degree, t, &knots);
279 }
280 }
281
282 let rhs = nalgebra::DVector::from_column_slice(values);
283 let decomp = mat.lu();
285 let solution = decomp.solve(&rhs)?;
286
287 Some(solution.as_slice().to_vec())
288}
289
290fn build_interpolation_knot_vector(params: &[f64], n: usize, degree: usize) -> Vec<f64> {
292 let m = n + degree + 1;
293 let mut knots = vec![0.0; m];
294
295 let last_val = *params.last().unwrap();
298 for i in (m - degree - 1)..m {
299 knots[i] = last_val;
300 }
301
302 for j in 1..(n - degree) {
304 let mut sum = 0.0;
305 for i in j..(j + degree) {
306 sum += params[i];
307 }
308 knots[j + degree] = sum / degree as f64;
309 }
310
311 knots
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
319 (a - b).abs() < tol
320 }
321
322 #[test]
323 fn test_clamped_knot_vector() {
324 let knots = generate_clamped_knot_vector(5, 3);
325 assert_eq!(knots.len(), 9);
327 assert_eq!(&knots[..4], &[0.0, 0.0, 0.0, 0.0]);
329 assert_eq!(&knots[5..], &[2.0, 2.0, 2.0, 2.0]);
331 assert_eq!(knots[4], 1.0);
332 }
333
334 #[test]
335 fn test_basis_partition_of_unity() {
336 let n = 5;
337 let degree = 3;
338 let knots = generate_clamped_knot_vector(n, degree);
339
340 for k in 0..20 {
342 let t = (k as f64) * 0.1;
343 if t >= (n - degree) as f64 {
344 continue;
345 }
346 let sum: f64 = (0..n).map(|i| bspline_basis(i, degree, t, &knots)).sum();
347 assert!(
348 approx_eq(sum, 1.0, 1e-12),
349 "Partition of unity failed at t={t}: sum={sum}"
350 );
351 }
352 }
353
354 #[test]
355 fn test_generate_bspline_path_basic() {
356 let cx = vec![-1.0, 3.0, 4.0, 2.0, 1.0];
357 let cy = vec![0.0, -3.0, 1.0, 1.0, 3.0];
358 let result = generate_bspline_path(&cx, &cy, 50, 3);
359 assert!(result.is_some());
360 let bsp = result.unwrap();
361 assert_eq!(bsp.path.len(), 50);
362 assert_eq!(bsp.heading.len(), 50);
363 assert_eq!(bsp.curvature.len(), 50);
364 }
365
366 #[test]
367 fn test_path_starts_and_ends_at_control_endpoints() {
368 let cx = vec![-1.0, 3.0, 4.0, 2.0, 1.0];
369 let cy = vec![0.0, -3.0, 1.0, 1.0, 3.0];
370 let bsp = generate_bspline_path(&cx, &cy, 100, 3).unwrap();
371
372 let first = bsp.path[0];
375 let last = bsp.path[bsp.path.len() - 1];
376 assert!(approx_eq(first.0, cx[0], 1e-9), "Start x mismatch");
377 assert!(approx_eq(first.1, cy[0], 1e-9), "Start y mismatch");
378 assert!(
379 approx_eq(last.0, *cx.last().unwrap(), 1e-9),
380 "End x mismatch"
381 );
382 assert!(
383 approx_eq(last.1, *cy.last().unwrap(), 1e-9),
384 "End y mismatch"
385 );
386 }
387
388 #[test]
389 fn test_different_degrees() {
390 let cx = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0];
391 let cy = vec![0.0, 2.0, -1.0, 3.0, 0.0, 1.0];
392
393 for degree in 1..=5 {
394 let result = generate_bspline_path(&cx, &cy, 30, degree);
395 assert!(result.is_some(), "Failed for degree={degree}");
396 let bsp = result.unwrap();
397 assert_eq!(bsp.path.len(), 30);
398
399 let first = bsp.path[0];
401 let last = bsp.path[bsp.path.len() - 1];
402 assert!(approx_eq(first.0, cx[0], 1e-9), "degree={degree} start x");
403 assert!(approx_eq(first.1, cy[0], 1e-9), "degree={degree} start y");
404 assert!(
405 approx_eq(last.0, *cx.last().unwrap(), 1e-9),
406 "degree={degree} end x"
407 );
408 assert!(
409 approx_eq(last.1, *cy.last().unwrap(), 1e-9),
410 "degree={degree} end y"
411 );
412 }
413 }
414
415 #[test]
416 fn test_degree_clamped_to_valid_range() {
417 let cx = vec![0.0, 1.0, 2.0];
419 let cy = vec![0.0, 1.0, 0.0];
420 let result = generate_bspline_path(&cx, &cy, 20, 10);
421 assert!(result.is_some());
422 }
423
424 #[test]
425 fn test_invalid_inputs() {
426 assert!(generate_bspline_path(&[], &[], 10, 3).is_none());
427 assert!(generate_bspline_path(&[1.0], &[2.0], 10, 3).is_none());
428 assert!(generate_bspline_path(&[1.0, 2.0], &[1.0], 10, 3).is_none());
429 assert!(generate_bspline_path(&[1.0, 2.0], &[1.0, 2.0], 0, 3).is_none());
430 }
431
432 #[test]
433 fn test_linear_degree1_is_straight() {
434 let cx = vec![0.0, 1.0, 2.0, 3.0];
436 let cy = vec![0.0, 1.0, 2.0, 3.0];
437 let bsp = generate_bspline_path(&cx, &cy, 50, 1).unwrap();
438
439 for &(px, py) in &bsp.path {
440 assert!(
441 approx_eq(px, py, 1e-9),
442 "Point ({px}, {py}) deviates from y=x"
443 );
444 }
445 }
446
447 #[test]
448 fn test_heading_is_consistent_with_path() {
449 let cx = vec![0.0, 2.0, 4.0, 6.0];
450 let cy = vec![0.0, 3.0, 1.0, 4.0];
451 let bsp = generate_bspline_path(&cx, &cy, 100, 3).unwrap();
452
453 for i in 1..bsp.path.len() - 1 {
455 let dx = bsp.path[i + 1].0 - bsp.path[i - 1].0;
456 let dy = bsp.path[i + 1].1 - bsp.path[i - 1].1;
457 let fd_heading = dy.atan2(dx);
458 let diff = (bsp.heading[i] - fd_heading).abs();
459 assert!(
461 diff < 0.3 || (diff - std::f64::consts::TAU).abs() < 0.3,
462 "Heading mismatch at i={i}: analytic={}, fd={fd_heading}",
463 bsp.heading[i]
464 );
465 }
466 }
467
468 #[test]
469 fn test_interpolate_bspline_path_passes_through_waypoints() {
470 let wx = vec![-1.0, 3.0, 4.0, 2.0, 1.0];
471 let wy = vec![0.0, -3.0, 1.0, 1.0, 3.0];
472 let bsp = interpolate_bspline_path(&wx, &wy, 200, 3).unwrap();
473
474 let first = bsp.path[0];
476 let last = bsp.path[bsp.path.len() - 1];
477 assert!(approx_eq(first.0, wx[0], 1e-6), "Start x");
478 assert!(approx_eq(first.1, wy[0], 1e-6), "Start y");
479 assert!(approx_eq(last.0, *wx.last().unwrap(), 1e-6), "End x");
480 assert!(approx_eq(last.1, *wy.last().unwrap(), 1e-6), "End y");
481 }
482
483 #[test]
484 fn test_interpolate_bspline_path_invalid() {
485 assert!(interpolate_bspline_path(&[], &[], 10, 3).is_none());
486 assert!(interpolate_bspline_path(&[1.0], &[2.0], 10, 3).is_none());
487 }
488
489 #[test]
490 fn test_single_output_point() {
491 let cx = vec![0.0, 1.0, 2.0, 3.0];
492 let cy = vec![0.0, 1.0, 0.0, 1.0];
493 let bsp = generate_bspline_path(&cx, &cy, 1, 3).unwrap();
494 assert_eq!(bsp.path.len(), 1);
495 assert!(approx_eq(bsp.path[0].0, 0.0, 1e-9));
497 assert!(approx_eq(bsp.path[0].1, 0.0, 1e-9));
498 }
499
500 #[test]
501 fn test_two_control_points() {
502 let cx = vec![0.0, 5.0];
503 let cy = vec![0.0, 3.0];
504 let bsp = generate_bspline_path(&cx, &cy, 10, 1).unwrap();
505 assert_eq!(bsp.path.len(), 10);
506 assert!(approx_eq(bsp.path[0].0, 0.0, 1e-9));
508 assert!(approx_eq(bsp.path[0].1, 0.0, 1e-9));
509 assert!(approx_eq(bsp.path[9].0, 5.0, 1e-9));
510 assert!(approx_eq(bsp.path[9].1, 3.0, 1e-9));
511 }
512
513 #[test]
514 fn test_curvature_zero_for_straight_line() {
515 let cx = vec![0.0, 1.0, 2.0, 3.0, 4.0];
516 let cy = vec![0.0, 0.0, 0.0, 0.0, 0.0];
517 let bsp = generate_bspline_path(&cx, &cy, 50, 3).unwrap();
518 for (i, &k) in bsp.curvature.iter().enumerate() {
519 assert!(
520 k.abs() < 1e-6,
521 "Curvature should be ~0 for straight line, got {k} at index {i}"
522 );
523 }
524 }
525}