1#![allow(
4 clippy::many_single_char_names,
5 clippy::similar_names,
6 clippy::suboptimal_flops,
7 clippy::needless_range_loop,
8 clippy::cast_precision_loss,
9 clippy::option_if_let_else,
10 clippy::let_and_return,
11 clippy::doc_markdown,
12 clippy::manual_slice_fill,
13 clippy::missing_const_for_fn
14)]
15
16use crate::MathError;
17use crate::nurbs::basis::basis_funs;
18use crate::nurbs::curve::NurbsCurve;
19use crate::vec::Point3;
20
21pub fn interpolate(points: &[Point3], degree: usize) -> Result<NurbsCurve, MathError> {
43 let n = points.len();
44 if n < 2 {
45 return Err(MathError::EmptyInput);
46 }
47
48 let p = degree.min(n - 1);
49
50 let params = chord_length_params(points);
51 let knots = build_interpolation_knots(¶ms, p, n);
52 let control_points = solve_interpolation(points, ¶ms, &knots, p)?;
53
54 let weights = vec![1.0; n];
55 NurbsCurve::new(p, knots, control_points, weights)
56}
57
58pub fn approximate(
69 points: &[Point3],
70 degree: usize,
71 num_control_points: usize,
72) -> Result<NurbsCurve, MathError> {
73 let n = points.len();
74 if n < 2 {
75 return Err(MathError::EmptyInput);
76 }
77 if num_control_points < degree + 1 {
78 return Err(MathError::InvalidKnotVector {
79 expected: degree + 1,
80 got: num_control_points,
81 });
82 }
83 if num_control_points > n {
84 return Err(MathError::InvalidKnotVector {
85 expected: n,
86 got: num_control_points,
87 });
88 }
89
90 if num_control_points == n {
91 return interpolate(points, degree);
92 }
93
94 let p = degree.min(num_control_points - 1);
95 let m = num_control_points;
96
97 let params = chord_length_params(points);
98 let knots = build_approximation_knots(¶ms, p, m, n);
99
100 let control_points = solve_approximation(points, ¶ms, &knots, p, m)?;
103
104 let weights = vec![1.0; m];
105 NurbsCurve::new(p, knots, control_points, weights)
106}
107
108pub(crate) fn chord_length_params(points: &[Point3]) -> Vec<f64> {
115 let n = points.len();
116 if n <= 1 {
117 return vec![0.0; n];
118 }
119
120 let mut dists = Vec::with_capacity(n);
121 dists.push(0.0);
122 for i in 1..n {
123 let d = (points[i] - points[i - 1]).length();
124 dists.push(dists[i - 1] + d);
125 }
126
127 let total = dists[n - 1];
128 if total < 1e-15 {
129 #[allow(clippy::cast_precision_loss)]
131 return (0..n).map(|i| (i as f64) / ((n - 1) as f64)).collect();
132 }
133
134 dists.iter().map(|d| d / total).collect()
135}
136
137#[allow(clippy::cast_precision_loss)]
146fn build_interpolation_knots(params: &[f64], p: usize, n: usize) -> Vec<f64> {
147 let num_knots = n + p + 1;
148 let mut knots = Vec::with_capacity(num_knots);
149
150 knots.extend(std::iter::repeat_n(0.0, p + 1));
152
153 for j in 1..n - p {
155 #[allow(clippy::cast_precision_loss)]
156 let avg = params[j..j + p].iter().sum::<f64>() / (p as f64);
157 knots.push(avg);
158 }
159
160 knots.extend(std::iter::repeat_n(1.0, p + 1));
162
163 knots
164}
165
166#[allow(
168 clippy::many_single_char_names,
169 clippy::cast_precision_loss,
170 clippy::cast_possible_truncation,
171 clippy::cast_sign_loss
172)]
173pub(crate) fn build_approximation_knots(params: &[f64], p: usize, m: usize, n: usize) -> Vec<f64> {
174 let num_knots = m + p + 1;
175 let mut knots = Vec::with_capacity(num_knots);
176
177 knots.extend(std::iter::repeat_n(0.0, p + 1));
178
179 let num_interior = m - p - 1;
181 if num_interior > 0 {
182 #[allow(clippy::cast_precision_loss)]
183 let d = (n as f64) / ((num_interior + 1) as f64);
184 for j in 1..=num_interior {
185 #[allow(
186 clippy::cast_precision_loss,
187 clippy::cast_possible_truncation,
188 clippy::cast_sign_loss
189 )]
190 let i = (j as f64 * d) as usize;
191 let alpha = (j as f64).mul_add(d, -(i as f64));
192 let knot =
193 (1.0 - alpha).mul_add(params[i.min(n - 1)], alpha * params[(i + 1).min(n - 1)]);
194 knots.push(knot);
195 }
196 }
197
198 knots.extend(std::iter::repeat_n(1.0, p + 1));
199
200 knots
201}
202
203#[allow(clippy::cast_precision_loss, clippy::needless_range_loop)]
210fn solve_interpolation(
211 points: &[Point3],
212 params: &[f64],
213 knots: &[f64],
214 degree: usize,
215) -> Result<Vec<Point3>, MathError> {
216 let n = points.len();
217
218 let mut matrix = vec![vec![0.0; n]; n];
219 for (i, &t) in params.iter().enumerate() {
220 let span = find_span(t, degree, knots, n);
221 let basis = basis_funs(span, t, degree, knots);
222 for (k, &b) in basis.iter().enumerate() {
223 let col = span - degree + k;
224 if col < n {
225 matrix[i][col] = b;
226 }
227 }
228 }
229
230 let mut rhs_x: Vec<f64> = points.iter().map(|p| p.x()).collect();
231 let mut rhs_y: Vec<f64> = points.iter().map(|p| p.y()).collect();
232 let mut rhs_z: Vec<f64> = points.iter().map(|p| p.z()).collect();
233
234 gauss_solve(&mut matrix, &mut rhs_x)?;
235 let mut matrix2 = vec![vec![0.0; n]; n];
237 for (i, &t) in params.iter().enumerate() {
238 let span = find_span(t, degree, knots, n);
239 let basis = basis_funs(span, t, degree, knots);
240 for (k, &b) in basis.iter().enumerate() {
241 let col = span - degree + k;
242 if col < n {
243 matrix2[i][col] = b;
244 }
245 }
246 }
247 gauss_solve(&mut matrix2, &mut rhs_y)?;
248
249 let mut matrix3 = vec![vec![0.0; n]; n];
250 for (i, &t) in params.iter().enumerate() {
251 let span = find_span(t, degree, knots, n);
252 let basis = basis_funs(span, t, degree, knots);
253 for (k, &b) in basis.iter().enumerate() {
254 let col = span - degree + k;
255 if col < n {
256 matrix3[i][col] = b;
257 }
258 }
259 }
260 gauss_solve(&mut matrix3, &mut rhs_z)?;
261
262 Ok(rhs_x
263 .iter()
264 .zip(rhs_y.iter())
265 .zip(rhs_z.iter())
266 .map(|((&x, &y), &z)| Point3::new(x, y, z))
267 .collect())
268}
269
270#[allow(
272 clippy::cast_precision_loss,
273 clippy::many_single_char_names,
274 clippy::needless_range_loop
275)]
276fn solve_approximation(
277 points: &[Point3],
278 params: &[f64],
279 knots: &[f64],
280 degree: usize,
281 m: usize,
282) -> Result<Vec<Point3>, MathError> {
283 let n = points.len();
284
285 let mut mat_n = vec![vec![0.0; m]; n];
286 for (i, &t) in params.iter().enumerate() {
287 let span = find_span(t, degree, knots, m);
288 let basis = basis_funs(span, t, degree, knots);
289 for (k, &b) in basis.iter().enumerate() {
290 let col = span - degree + k;
291 if col < m {
292 mat_n[i][col] = b;
293 }
294 }
295 }
296
297 let mut ntn = vec![vec![0.0; m]; m];
299 let mut ntq_x = vec![0.0; m];
300 let mut ntq_y = vec![0.0; m];
301 let mut ntq_z = vec![0.0; m];
302
303 for i in 0..m {
304 for j in 0..m {
305 for k in 0..n {
306 ntn[i][j] += mat_n[k][i] * mat_n[k][j];
307 }
308 }
309 for k in 0..n {
310 ntq_x[i] += mat_n[k][i] * points[k].x();
311 ntq_y[i] += mat_n[k][i] * points[k].y();
312 ntq_z[i] += mat_n[k][i] * points[k].z();
313 }
314 }
315
316 for j in 0..m {
319 ntn[0][j] = 0.0;
320 ntn[m - 1][j] = 0.0;
321 ntn[j][0] = 0.0;
322 ntn[j][m - 1] = 0.0;
323 }
324 ntn[0][0] = 1.0;
325 ntn[m - 1][m - 1] = 1.0;
326 ntq_x[0] = points[0].x();
327 ntq_y[0] = points[0].y();
328 ntq_z[0] = points[0].z();
329 ntq_x[m - 1] = points[n - 1].x();
330 ntq_y[m - 1] = points[n - 1].y();
331 ntq_z[m - 1] = points[n - 1].z();
332
333 gauss_solve(&mut ntn.clone(), &mut ntq_x)?;
334 gauss_solve(&mut ntn.clone(), &mut ntq_y)?;
335 gauss_solve(&mut ntn, &mut ntq_z)?;
336
337 Ok(ntq_x
338 .iter()
339 .zip(ntq_y.iter())
340 .zip(ntq_z.iter())
341 .map(|((&x, &y), &z)| Point3::new(x, y, z))
342 .collect())
343}
344
345pub(crate) fn find_span(t: f64, degree: usize, knots: &[f64], n: usize) -> usize {
349 if t >= knots[n] {
350 return n - 1;
351 }
352 if t <= knots[degree] {
353 return degree;
354 }
355
356 let mut low = degree;
357 let mut high = n;
358 let mut mid = low.midpoint(high);
359
360 while t < knots[mid] || t >= knots[mid + 1] {
361 if t < knots[mid] {
362 high = mid;
363 } else {
364 low = mid;
365 }
366 mid = low.midpoint(high);
367 }
368
369 mid
370}
371
372#[allow(clippy::needless_range_loop)]
374fn gauss_solve(a: &mut [Vec<f64>], b: &mut [f64]) -> Result<(), MathError> {
375 let n = b.len();
376
377 for k in 0..n {
379 let mut max_val = a[k][k].abs();
380 let mut max_row = k;
381 for i in (k + 1)..n {
382 if a[i][k].abs() > max_val {
383 max_val = a[i][k].abs();
384 max_row = i;
385 }
386 }
387
388 if max_val < 1e-15 {
389 return Err(MathError::SingularMatrix);
390 }
391
392 if max_row != k {
393 a.swap(k, max_row);
394 b.swap(k, max_row);
395 }
396
397 for i in (k + 1)..n {
398 let factor = a[i][k] / a[k][k];
399 for j in (k + 1)..n {
400 a[i][j] -= factor * a[k][j];
401 }
402 a[i][k] = 0.0;
403 b[i] -= factor * b[k];
404 }
405 }
406
407 for i in (0..n).rev() {
409 let mut sum = b[i];
410 for j in (i + 1)..n {
411 sum -= a[i][j] * b[j];
412 }
413 b[i] = sum / a[i][i];
414 }
415
416 Ok(())
417}
418
419fn compute_lspia_step_size(basis_data: &[(usize, Vec<f64>)], degree: usize, m: usize) -> f64 {
427 let mut v = vec![1.0f64; m];
429 let norm = (m as f64).sqrt();
430 for val in &mut v {
431 *val /= norm;
432 }
433
434 for _ in 0..20 {
435 let nv: Vec<f64> = basis_data
437 .iter()
438 .map(|(span, n_vals)| {
439 let mut sum = 0.0f64;
440 for (k, &bk) in n_vals.iter().enumerate() {
441 let j = span - degree + k;
442 if j < m {
443 sum += bk * v[j];
444 }
445 }
446 sum
447 })
448 .collect();
449 let mut w = vec![0.0f64; m];
450 for (i, (span, n_vals)) in basis_data.iter().enumerate() {
451 for (k, &bk) in n_vals.iter().enumerate() {
452 let j = span - degree + k;
453 if j < m {
454 w[j] += bk * nv[i];
455 }
456 }
457 }
458
459 let mag = w.iter().map(|x| x * x).sum::<f64>().sqrt();
460 if mag < 1e-30 {
461 return 1.0;
462 }
463 for val in &mut v {
464 *val = 0.0;
465 }
466 for (j, &wj) in w.iter().enumerate() {
467 v[j] = wj / mag;
468 }
469 }
470
471 let nv: Vec<f64> = basis_data
473 .iter()
474 .map(|(span, n_vals)| {
475 let mut sum = 0.0f64;
476 for (k, &bk) in n_vals.iter().enumerate() {
477 let j = span - degree + k;
478 if j < m {
479 sum += bk * v[j];
480 }
481 }
482 sum
483 })
484 .collect();
485 let lambda_max = nv.iter().map(|x| x * x).sum::<f64>();
486
487 if lambda_max < 1e-30 {
488 1.0
489 } else {
490 1.0 / lambda_max
492 }
493}
494
495fn compute_lspia_step_size_weighted(
500 basis_data: &[(usize, Vec<f64>)],
501 point_weights: &[f64],
502 degree: usize,
503 m: usize,
504) -> f64 {
505 let mut v = vec![1.0f64; m];
506 let norm = (m as f64).sqrt();
507 for val in &mut v {
508 *val /= norm;
509 }
510
511 for _ in 0..20 {
512 let nv: Vec<f64> = basis_data
513 .iter()
514 .map(|(span, n_vals)| {
515 let mut sum = 0.0f64;
516 for (k, &bk) in n_vals.iter().enumerate() {
517 let j = span - degree + k;
518 if j < m {
519 sum += bk * v[j];
520 }
521 }
522 sum
523 })
524 .collect();
525 let mut w = vec![0.0f64; m];
526 for (i, (span, n_vals)) in basis_data.iter().enumerate() {
527 let pw = point_weights[i];
528 for (k, &bk) in n_vals.iter().enumerate() {
529 let j = span - degree + k;
530 if j < m {
531 w[j] += pw * bk * nv[i];
532 }
533 }
534 }
535
536 let mag = w.iter().map(|x| x * x).sum::<f64>().sqrt();
537 if mag < 1e-30 {
538 return 1.0;
539 }
540 for val in &mut v {
541 *val = 0.0;
542 }
543 for (j, &wj) in w.iter().enumerate() {
544 v[j] = wj / mag;
545 }
546 }
547
548 let nv: Vec<f64> = basis_data
549 .iter()
550 .map(|(span, n_vals)| {
551 let mut sum = 0.0f64;
552 for (k, &bk) in n_vals.iter().enumerate() {
553 let j = span - degree + k;
554 if j < m {
555 sum += bk * v[j];
556 }
557 }
558 sum
559 })
560 .collect();
561 let lambda_max: f64 = nv
562 .iter()
563 .zip(point_weights.iter())
564 .map(|(&x, &pw)| pw * x * x)
565 .sum();
566
567 if lambda_max < 1e-30 {
568 1.0
569 } else {
570 1.0 / lambda_max
571 }
572}
573
574#[allow(clippy::cast_precision_loss)]
606pub fn approximate_lspia(
607 points: &[Point3],
608 degree: usize,
609 num_control_points: usize,
610 tolerance: f64,
611 max_iterations: usize,
612) -> Result<NurbsCurve, MathError> {
613 let n = points.len();
614 if n < 2 {
615 return Err(MathError::EmptyInput);
616 }
617
618 let p = degree.min(n - 1);
619 let m = num_control_points.min(n).max(p + 1);
620
621 let params = chord_length_params(points);
622 let knots = build_approximation_knots(¶ms, p, m, n);
623
624 let mut control_points = Vec::with_capacity(m);
626 for i in 0..m {
627 let t = if m > 1 {
628 i as f64 / (m - 1) as f64
629 } else {
630 0.0
631 };
632 let mut best_idx = 0;
633 let mut best_dist = f64::INFINITY;
634 for (j, ¶m) in params.iter().enumerate() {
635 let d = (param - t).abs();
636 if d < best_dist {
637 best_dist = d;
638 best_idx = j;
639 }
640 }
641 control_points.push(points[best_idx]);
642 }
643
644 let weights = vec![1.0; m];
645
646 let mut basis_data: Vec<(usize, Vec<f64>)> = Vec::with_capacity(n);
647 for &u in ¶ms {
648 let span = find_span(u, p, &knots, m);
649 let n_vals = basis_funs(span, u, p, &knots);
650 basis_data.push((span, n_vals));
651 }
652
653 let mu = compute_lspia_step_size(&basis_data, p, m);
656
657 for iter in 0..max_iterations {
658 let curve = NurbsCurve::new(p, knots.clone(), control_points.clone(), weights.clone())?;
659
660 let mut max_err = 0.0f64;
661 let mut deltas = vec![(0.0f64, 0.0f64, 0.0f64); m];
662
663 for (i, &u) in params.iter().enumerate() {
664 let q = curve.evaluate(u);
665 let err_x = points[i].x() - q.x();
666 let err_y = points[i].y() - q.y();
667 let err_z = points[i].z() - q.z();
668 let err_mag = (err_x * err_x + err_y * err_y + err_z * err_z).sqrt();
669 max_err = max_err.max(err_mag);
670
671 let (span, n_vals) = &basis_data[i];
672 for (k, &nv) in n_vals.iter().enumerate() {
673 let j = span - p + k;
674 if j < m {
675 deltas[j].0 += nv * err_x;
676 deltas[j].1 += nv * err_y;
677 deltas[j].2 += nv * err_z;
678 }
679 }
680 }
681
682 if max_err < tolerance {
683 return NurbsCurve::new(p, knots, control_points, weights);
684 }
685
686 for j in 0..m {
688 control_points[j] = Point3::new(
689 mu.mul_add(deltas[j].0, control_points[j].x()),
690 mu.mul_add(deltas[j].1, control_points[j].y()),
691 mu.mul_add(deltas[j].2, control_points[j].z()),
692 );
693 }
694
695 if iter == max_iterations - 1 {
697 #[cfg(debug_assertions)]
700 if max_err > tolerance {
701 log::warn!(
702 "approximate_lspia: did not converge after {max_iterations} iterations \
703 (max_err={max_err:.2e}, tolerance={tolerance:.2e}, \
704 num_cps={m}, degree={p})"
705 );
706 }
707 return NurbsCurve::new(p, knots, control_points, weights);
708 }
709 }
710
711 NurbsCurve::new(p, knots, control_points, weights)
713}
714
715#[allow(clippy::cast_precision_loss, clippy::too_many_arguments, dead_code)]
726pub(crate) fn approximate_lspia_weighted(
727 points: &[Point3],
728 point_weights: &[f64],
729 degree: usize,
730 num_control_points: usize,
731 tolerance: f64,
732 max_iterations: usize,
733) -> Result<NurbsCurve, MathError> {
734 let n = points.len();
735 if n < 2 {
736 return Err(MathError::EmptyInput);
737 }
738 if points.len() != point_weights.len() {
739 return Err(MathError::InvalidWeights {
740 expected: points.len(),
741 got: point_weights.len(),
742 });
743 }
744
745 let p = degree.min(n - 1);
746 let m = num_control_points.min(n).max(p + 1);
747
748 let params = chord_length_params(points);
749 let knots = build_approximation_knots(¶ms, p, m, n);
750
751 let mut control_points = Vec::with_capacity(m);
753 for i in 0..m {
754 let t = if m > 1 {
755 i as f64 / (m - 1) as f64
756 } else {
757 0.0
758 };
759 let mut best_idx = 0;
760 let mut best_dist = f64::INFINITY;
761 for (j, ¶m) in params.iter().enumerate() {
762 let d = (param - t).abs();
763 if d < best_dist {
764 best_dist = d;
765 best_idx = j;
766 }
767 }
768 control_points.push(points[best_idx]);
769 }
770
771 let weights = vec![1.0; m];
772
773 let mut basis_data: Vec<(usize, Vec<f64>)> = Vec::with_capacity(n);
774 for &u in ¶ms {
775 let span = find_span(u, p, &knots, m);
776 let n_vals = basis_funs(span, u, p, &knots);
777 basis_data.push((span, n_vals));
778 }
779
780 let mu = compute_lspia_step_size_weighted(&basis_data, point_weights, p, m);
781
782 for iter in 0..max_iterations {
783 let curve = NurbsCurve::new(p, knots.clone(), control_points.clone(), weights.clone())?;
784
785 let mut max_err = 0.0f64;
786 let mut deltas = vec![(0.0f64, 0.0f64, 0.0f64); m];
787
788 for (i, &u) in params.iter().enumerate() {
789 let q = curve.evaluate(u);
790 let pw = point_weights[i];
791 let err_x = points[i].x() - q.x();
792 let err_y = points[i].y() - q.y();
793 let err_z = points[i].z() - q.z();
794 let err_mag = (err_x * err_x + err_y * err_y + err_z * err_z).sqrt();
795 max_err = max_err.max(err_mag);
796
797 let (span, n_vals) = &basis_data[i];
798 for (k, &nv) in n_vals.iter().enumerate() {
799 let j = span - p + k;
800 if j < m {
801 deltas[j].0 += pw * nv * err_x;
802 deltas[j].1 += pw * nv * err_y;
803 deltas[j].2 += pw * nv * err_z;
804 }
805 }
806 }
807
808 if max_err < tolerance {
809 return NurbsCurve::new(p, knots, control_points, weights);
810 }
811
812 for j in 0..m {
814 control_points[j] = Point3::new(
815 mu.mul_add(deltas[j].0, control_points[j].x()),
816 mu.mul_add(deltas[j].1, control_points[j].y()),
817 mu.mul_add(deltas[j].2, control_points[j].z()),
818 );
819 }
820
821 if iter == max_iterations - 1 {
822 return NurbsCurve::new(p, knots, control_points, weights);
823 }
824 }
825
826 NurbsCurve::new(p, knots, control_points, weights)
827}
828
829#[cfg(test)]
830mod tests {
831 #![allow(clippy::unwrap_used, clippy::cast_lossless, clippy::suboptimal_flops)]
832
833 use crate::tolerance::Tolerance;
834 use crate::vec::Point3;
835
836 use super::*;
837
838 #[test]
839 fn interpolate_two_points_is_line() {
840 let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)];
841 let curve = interpolate(&pts, 1).unwrap();
842
843 let tol = Tolerance::new();
844 let mid = curve.evaluate(0.5);
845 assert!(tol.approx_eq(mid.x(), 0.5));
846 assert!(tol.approx_eq(mid.y(), 0.0));
847 }
848
849 #[test]
850 fn interpolate_passes_through_points() {
851 let pts = vec![
852 Point3::new(0.0, 0.0, 0.0),
853 Point3::new(1.0, 1.0, 0.0),
854 Point3::new(2.0, 0.0, 0.0),
855 Point3::new(3.0, 1.0, 0.0),
856 ];
857 let curve = interpolate(&pts, 3).unwrap();
858
859 let tol = Tolerance::new();
860 let p0 = curve.evaluate(0.0);
861 let p1 = curve.evaluate(1.0);
862 assert!(tol.approx_eq(p0.x(), 0.0), "start x: {}", p0.x());
863 assert!(tol.approx_eq(p0.y(), 0.0), "start y: {}", p0.y());
864 assert!(tol.approx_eq(p1.x(), 3.0), "end x: {}", p1.x());
865 assert!(tol.approx_eq(p1.y(), 1.0), "end y: {}", p1.y());
866 }
867
868 #[test]
869 fn interpolate_3d_points() {
870 let pts = vec![
871 Point3::new(0.0, 0.0, 0.0),
872 Point3::new(1.0, 1.0, 1.0),
873 Point3::new(2.0, 0.0, 2.0),
874 ];
875 let curve = interpolate(&pts, 2).unwrap();
876
877 let tol = Tolerance::new();
878 let p0 = curve.evaluate(0.0);
879 let p1 = curve.evaluate(1.0);
880 assert!(tol.approx_eq(p0.x(), 0.0));
881 assert!(tol.approx_eq(p1.x(), 2.0));
882 assert!(tol.approx_eq(p1.z(), 2.0));
883 }
884
885 #[test]
886 fn interpolate_single_point_error() {
887 let pts = vec![Point3::new(0.0, 0.0, 0.0)];
888 assert!(interpolate(&pts, 3).is_err());
889 }
890
891 #[test]
892 fn approximate_fewer_control_points() {
893 let pts = vec![
894 Point3::new(0.0, 0.0, 0.0),
895 Point3::new(0.5, 0.8, 0.0),
896 Point3::new(1.0, 1.0, 0.0),
897 Point3::new(1.5, 0.8, 0.0),
898 Point3::new(2.0, 0.0, 0.0),
899 ];
900 let curve = approximate(&pts, 3, 4).unwrap();
901
902 let tol = Tolerance::new();
904 let p0 = curve.evaluate(0.0);
905 let p1 = curve.evaluate(1.0);
906 assert!(tol.approx_eq(p0.x(), 0.0), "start x: {}", p0.x());
907 assert!(tol.approx_eq(p1.x(), 2.0), "end x: {}", p1.x());
908 }
909
910 #[test]
911 fn approximate_equals_interpolate_when_same_count() {
912 let pts = vec![
913 Point3::new(0.0, 0.0, 0.0),
914 Point3::new(1.0, 1.0, 0.0),
915 Point3::new(2.0, 0.0, 0.0),
916 ];
917
918 let interp = interpolate(&pts, 2).unwrap();
919 let approx = approximate(&pts, 2, 3).unwrap();
920
921 let tol = Tolerance::new();
922 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
923 let pi = interp.evaluate(t);
924 let pa = approx.evaluate(t);
925 assert!(
926 tol.approx_eq(pi.x(), pa.x()) && tol.approx_eq(pi.y(), pa.y()),
927 "at t={t}: interp=({}, {}) approx=({}, {})",
928 pi.x(),
929 pi.y(),
930 pa.x(),
931 pa.y()
932 );
933 }
934 }
935
936 #[test]
939 #[allow(clippy::cast_precision_loss)]
940 fn lspia_fits_line() {
941 let points: Vec<Point3> = (0..10)
942 .map(|i| {
943 let t = i as f64 / 9.0;
944 Point3::new(t, 2.0f64.mul_add(t, 1.0), 0.0)
945 })
946 .collect();
947 let curve = approximate_lspia(&points, 3, 6, 1e-6, 100).unwrap();
948
949 let p0 = curve.evaluate(0.0);
951 let p1 = curve.evaluate(1.0);
952 assert!(
953 (p0.x() - 0.0).abs() < 0.01,
954 "start x: expected ~0.0, got {}",
955 p0.x()
956 );
957 assert!(
958 (p1.x() - 1.0).abs() < 0.01,
959 "end x: expected ~1.0, got {}",
960 p1.x()
961 );
962 }
963
964 #[test]
965 #[allow(clippy::cast_precision_loss)]
966 fn lspia_fits_circle() {
967 let n = 50;
968 let points: Vec<Point3> = (0..n)
969 .map(|i| {
970 let t = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
971 Point3::new(t.cos(), t.sin(), 0.0)
972 })
973 .collect();
974 let curve = approximate_lspia(&points, 3, 15, 1e-4, 200).unwrap();
975
976 for i in 0..10 {
977 let t = i as f64 / 9.0;
978 let p = curve.evaluate(t);
979 let r = (p.x() * p.x() + p.y() * p.y()).sqrt();
980 assert!((r - 1.0).abs() < 0.15, "radius at t={t} is {r}");
981 }
982 }
983
984 #[test]
985 #[allow(clippy::cast_precision_loss)]
986 fn lspia_fewer_cps_than_points() {
987 let points: Vec<Point3> = (0..100)
988 .map(|i| {
989 let t = i as f64 / 99.0;
990 Point3::new(t, (t * 6.0).sin(), 0.0)
991 })
992 .collect();
993 let curve = approximate_lspia(&points, 3, 20, 1e-3, 100).unwrap();
994 let p = curve.evaluate(0.5);
995 assert!(
996 (p.x() - 0.5).abs() < 0.1,
997 "midpoint x: expected ~0.5, got {}",
998 p.x()
999 );
1000 }
1001
1002 #[test]
1003 fn lspia_empty_input_returns_error() {
1004 let result = approximate_lspia(&[], 3, 5, 1e-6, 100);
1005 assert!(result.is_err());
1006 }
1007
1008 #[test]
1009 #[allow(clippy::cast_precision_loss)]
1010 fn lspia_weighted_emphasizes_region() {
1011 let points: Vec<Point3> = (0..20)
1012 .map(|i| {
1013 let t = i as f64 / 19.0;
1014 Point3::new(t, t * t, 0.0)
1015 })
1016 .collect();
1017 let uniform_weights = vec![1.0; 20];
1018 let curve = approximate_lspia_weighted(&points, &uniform_weights, 3, 8, 1e-5, 100).unwrap();
1019
1020 let p = curve.evaluate(0.5);
1021 assert!(
1022 (p.x() - 0.5).abs() < 0.15,
1023 "midpoint x: expected ~0.5, got {}",
1024 p.x()
1025 );
1026 }
1027
1028 #[test]
1029 fn lspia_weighted_mismatched_lengths_returns_error() {
1030 let points = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 0.0)];
1031 let weights = vec![1.0; 5];
1032 let result = approximate_lspia_weighted(&points, &weights, 1, 2, 1e-6, 10);
1033 assert!(result.is_err());
1034 }
1035}