1use crate::{KnotVector, NurbsCurve, Vec3, Vec4};
2
3#[derive(Clone, Debug)]
4pub struct PolylineFit {
5 pub curve: NurbsCurve,
6 pub parameters: Vec<f64>,
7 pub kept: Vec<Vec3>,
8}
9
10pub fn solve_dense(mut matrix: Vec<Vec<f64>>, mut rhs: Vec<f64>) -> Result<Vec<f64>, String> {
11 let count = rhs.len();
12 if count == 0 || matrix.len() != count || matrix.iter().any(|row| row.len() != count) {
13 return Err("solve_dense: matrix must be square and match RHS".into());
14 }
15 let scale = matrix
16 .iter()
17 .flatten()
18 .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
19 if scale == 0.0 {
20 return Err("solve_dense: singular matrix".into());
21 }
22 for column in 0..count {
23 let pivot = (column..count)
24 .max_by(|&a, &b| matrix[a][column].abs().total_cmp(&matrix[b][column].abs()))
25 .unwrap();
26 if matrix[pivot][column].abs() <= 1e-13 * scale {
27 return Err("solve_dense: singular matrix".into());
28 }
29 matrix.swap(column, pivot);
30 rhs.swap(column, pivot);
31 let diagonal = matrix[column][column];
32 for row in column + 1..count {
33 let factor = matrix[row][column] / diagonal;
34 matrix[row][column] = 0.0;
35 for entry in column + 1..count {
36 matrix[row][entry] -= factor * matrix[column][entry];
37 }
38 rhs[row] -= factor * rhs[column];
39 }
40 }
41 let mut result = vec![0.0; count];
42 for row in (0..count).rev() {
43 let remainder: f64 = (row + 1..count)
44 .map(|column| matrix[row][column] * result[column])
45 .sum();
46 result[row] = (rhs[row] - remainder) / matrix[row][row];
47 }
48 Ok(result)
49}
50
51pub fn solve_small<const N: usize>(
55 mut matrix: [[f64; N]; N],
56 mut rhs: [f64; N],
57 count: usize,
58) -> Result<[f64; N], String> {
59 if count == 0 || count > N {
60 return Err("solve_small: invalid system size".into());
61 }
62 let scale = matrix
63 .iter()
64 .take(count)
65 .flat_map(|row| row.iter().take(count))
66 .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
67 if scale == 0.0 {
68 return Err("solve_small: singular matrix".into());
69 }
70 for column in 0..count {
71 let pivot = (column..count)
72 .max_by(|&a, &b| matrix[a][column].abs().total_cmp(&matrix[b][column].abs()))
73 .unwrap();
74 if matrix[pivot][column].abs() <= 1e-13 * scale {
75 return Err("solve_small: singular matrix".into());
76 }
77 matrix.swap(column, pivot);
78 rhs.swap(column, pivot);
79 let diagonal = matrix[column][column];
80 for row in column + 1..count {
81 let factor = matrix[row][column] / diagonal;
82 matrix[row][column] = 0.0;
83 for entry in column + 1..count {
84 matrix[row][entry] -= factor * matrix[column][entry];
85 }
86 rhs[row] -= factor * rhs[column];
87 }
88 }
89 let mut result = [0.0; N];
90 for row in (0..count).rev() {
91 let remainder: f64 = (row + 1..count)
92 .map(|column| matrix[row][column] * result[column])
93 .sum();
94 result[row] = (rhs[row] - remainder) / matrix[row][row];
95 }
96 Ok(result)
97}
98
99pub fn solve_collocation(
110 matrix: &[Vec<f64>],
111 rhs: &[f64],
112 degree: usize,
113) -> Result<Vec<f64>, String> {
114 let count = rhs.len();
115 let bandwidth = degree.max(1).min(count.saturating_sub(1).max(1));
116 if let Ok(solution) = solve_banded(matrix, rhs, bandwidth) {
117 let scale = rhs
118 .iter()
119 .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
120 let tolerance = 1e-9 * scale.max(1.0);
121 let mut residual = 0.0_f64;
122 for row in 0..count {
123 let lo = row.saturating_sub(bandwidth);
124 let hi = (row + bandwidth).min(count - 1);
125 let mut accumulated = 0.0;
126 for col in lo..=hi {
127 accumulated += matrix[row][col] * solution[col];
128 }
129 residual = residual.max((accumulated - rhs[row]).abs());
130 }
131 if residual <= tolerance {
132 return Ok(solution);
133 }
134 }
135 solve_dense(matrix.to_vec(), rhs.to_vec())
136}
137
138pub fn solve_banded(
139 matrix: &[Vec<f64>],
140 rhs: &[f64],
141 bandwidth: usize,
142) -> Result<Vec<f64>, String> {
143 let count = rhs.len();
144 if count == 0 || matrix.len() != count || matrix.iter().any(|row| row.len() != count) {
145 return Err("solve_banded: matrix must be square and match RHS".into());
146 }
147 let mut matrix = matrix.to_vec();
148 let mut rhs = rhs.to_vec();
149 for column in 0..count {
150 let pivot = matrix[column][column];
151 if pivot.abs() <= 1e-300 {
152 return Err("solve_banded: singular matrix".into());
153 }
154 let maximum_row = (column + bandwidth).min(count - 1);
155 for row in column + 1..=maximum_row {
156 let factor = matrix[row][column] / pivot;
157 if factor == 0.0 {
158 continue;
159 }
160 let maximum_column = (column + bandwidth).min(count - 1);
161 for entry in column..=maximum_column {
162 matrix[row][entry] -= factor * matrix[column][entry];
163 }
164 rhs[row] -= factor * rhs[column];
165 }
166 }
167 let mut result = vec![0.0; count];
168 for row in (0..count).rev() {
169 let mut value = rhs[row];
170 let maximum_column = (row + bandwidth).min(count - 1);
171 for column in row + 1..=maximum_column {
172 value -= matrix[row][column] * result[column];
173 }
174 if matrix[row][row].abs() <= 1e-300 {
175 return Err("solve_banded: singular matrix".into());
176 }
177 result[row] = value / matrix[row][row];
178 }
179 Ok(result)
180}
181
182pub fn interpolate_homogeneous(
189 points: &[Vec4],
190 degree: usize,
191 parameters: &[f64],
192) -> Result<NurbsCurve, String> {
193 if points.len() < 2 || parameters.len() != points.len() {
194 return Err(
195 "interpolate_homogeneous: points and parameters must have matching length >= 2".into(),
196 );
197 }
198 if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
199 return Err("interpolate_homogeneous: parameters must be strictly increasing".into());
200 }
201 let n = points.len() - 1;
202 let degree = degree.min(n);
203 let m = n + degree + 1;
204 let mut knots = vec![0.0; m + 1];
205 for knot in &mut knots[m - degree..=m] {
206 *knot = 1.0;
207 }
208 for j in 1..=n.saturating_sub(degree) {
209 knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
210 }
211 let knot_vector = KnotVector::new(knots.clone(), degree)?;
212 let mut matrix = vec![vec![0.0; n + 1]; n + 1];
213 for (row, ¶meter) in parameters.iter().enumerate() {
214 let span = knot_vector.find_span(parameter);
215 let basis = knot_vector.basis_functions(span, parameter);
216 for (offset, value) in basis.into_iter().enumerate() {
217 matrix[row][span - degree + offset] = value;
218 }
219 }
220 let solve_axis = |axis: fn(&Vec4) -> f64| {
221 solve_collocation(
222 &matrix,
223 &points.iter().map(axis).collect::<Vec<_>>(),
224 degree,
225 )
226 };
227 let xs = solve_axis(|point| point.x)?;
228 let ys = solve_axis(|point| point.y)?;
229 let zs = solve_axis(|point| point.z)?;
230 let ws = solve_axis(|point| point.w)?;
231 NurbsCurve::new(
232 degree,
233 knots,
234 (0..=n)
235 .map(|index| Vec4 {
236 x: xs[index],
237 y: ys[index],
238 z: zs[index],
239 w: ws[index],
240 })
241 .collect(),
242 )
243}
244
245pub fn interpolate_curve(
246 points: &[Vec3],
247 degree: usize,
248 parameters: &[f64],
249) -> Result<NurbsCurve, String> {
250 if points.len() < 2 || parameters.len() != points.len() {
251 return Err(
252 "interpolate_curve: points and parameters must have matching length >= 2".into(),
253 );
254 }
255 if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
256 return Err("interpolate_curve: parameters must be strictly increasing".into());
257 }
258 let n = points.len() - 1;
259 let degree = degree.min(n);
260 let m = n + degree + 1;
261 let mut knots = vec![0.0; m + 1];
262 for knot in &mut knots[m - degree..=m] {
263 *knot = 1.0;
264 }
265 for j in 1..=n.saturating_sub(degree) {
266 knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
267 }
268 let knot_vector = KnotVector::new(knots.clone(), degree)?;
269 let mut matrix = vec![vec![0.0; n + 1]; n + 1];
270 for (row, ¶meter) in parameters.iter().enumerate() {
271 let span = knot_vector.find_span(parameter);
272 let basis = knot_vector.basis_functions(span, parameter);
273 for (offset, value) in basis.into_iter().enumerate() {
274 matrix[row][span - degree + offset] = value;
275 }
276 }
277 let solve_axis = |axis: fn(Vec3) -> f64| {
278 solve_collocation(
279 &matrix,
280 &points.iter().copied().map(axis).collect::<Vec<_>>(),
281 degree,
282 )
283 };
284 let xs = solve_axis(|point| point.x)?;
285 let ys = solve_axis(|point| point.y)?;
286 let zs = solve_axis(|point| point.z)?;
287 NurbsCurve::new(
288 degree,
289 knots,
290 (0..=n)
291 .map(|index| Vec4::from_point(Vec3::new(xs[index], ys[index], zs[index]), 1.0))
292 .collect(),
293 )
294}
295
296pub fn interpolate_curve_with_end_tangents(
303 points: &[Vec3],
304 parameters: &[f64],
305 start_tangent: Vec3,
306 end_tangent: Vec3,
307) -> Result<NurbsCurve, String> {
308 if points.len() < 2 || parameters.len() != points.len() {
309 return Err(
310 "interpolate_curve_with_end_tangents: points and parameters must match, >= 2".into(),
311 );
312 }
313 if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
314 return Err("interpolate_curve_with_end_tangents: parameters must increase".into());
315 }
316 let degree = 3usize;
317 let n = points.len() - 1;
318 let control_count = n + 3;
319 let t0 = parameters[0];
320 let t1 = parameters[n];
321 let mut knots = vec![t0; degree + 1];
324 for j in 0..n.saturating_sub(1) {
325 let window = ¶meters[j + 1..(j + degree).min(n) + 1];
326 knots.push(window.iter().sum::<f64>() / window.len() as f64);
327 }
328 knots.extend(std::iter::repeat(t1).take(degree + 1));
329 if knots.len() != control_count + degree + 1 {
330 return Err(format!(
331 "interpolate_curve_with_end_tangents: internal knot count {} for {} controls",
332 knots.len(),
333 control_count
334 ));
335 }
336 let knot_vector = KnotVector::new(knots.clone(), degree)?;
337 let mut matrix = vec![vec![0.0; control_count]; control_count];
338 let mut rhs_points = vec![Vec3::default(); control_count];
339 matrix[0][0] = 1.0;
343 rhs_points[0] = points[0];
344 let start_span = knots[degree + 1] - t0;
345 matrix[1][0] = -(degree as f64) / start_span;
346 matrix[1][1] = (degree as f64) / start_span;
347 rhs_points[1] = start_tangent;
348 for (index, ¶meter) in parameters.iter().enumerate().take(n).skip(1) {
349 let row = index + 1;
350 let span = knot_vector.find_span(parameter);
351 let basis = knot_vector.basis_functions(span, parameter);
352 for (offset, value) in basis.into_iter().enumerate() {
353 matrix[row][span - degree + offset] = value;
354 }
355 rhs_points[row] = points[index];
356 }
357 let end_span = t1 - knots[control_count - 1];
358 matrix[control_count - 2][control_count - 2] = -(degree as f64) / end_span;
359 matrix[control_count - 2][control_count - 1] = (degree as f64) / end_span;
360 rhs_points[control_count - 2] = end_tangent;
361 matrix[control_count - 1][control_count - 1] = 1.0;
362 rhs_points[control_count - 1] = points[n];
363 let solve_axis = |axis: fn(Vec3) -> f64| {
364 solve_dense(
365 matrix.clone(),
366 rhs_points.iter().copied().map(axis).collect::<Vec<_>>(),
367 )
368 };
369 let xs = solve_axis(|point| point.x)?;
370 let ys = solve_axis(|point| point.y)?;
371 let zs = solve_axis(|point| point.z)?;
372 NurbsCurve::new(
373 degree,
374 knots,
375 (0..control_count)
376 .map(|index| Vec4::from_point(Vec3::new(xs[index], ys[index], zs[index]), 1.0))
377 .collect(),
378 )
379}
380
381pub fn interpolate_curve_thinned(
382 points: &[Vec3],
383 degree: usize,
384 maximum_points: usize,
385) -> Result<NurbsCurve, String> {
386 if maximum_points < 2 {
387 return Err("interpolate_curve_thinned: maximum point count must be at least 2".into());
388 }
389 if points.len() <= maximum_points {
390 let parameters = chord_parameters(points);
391 return interpolate_curve(points, degree, ¶meters);
392 }
393 let step = (points.len() - 1) as f64 / (maximum_points - 1) as f64;
394 let thinned = (0..maximum_points)
395 .map(|index| points[(index as f64 * step).round() as usize])
396 .collect::<Vec<_>>();
397 let parameters = chord_parameters(&thinned);
398 interpolate_curve(&thinned, degree, ¶meters)
399}
400
401fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
402 if points.len() < 2 {
403 return vec![0.0; points.len()];
404 }
405 let mut parameters = vec![0.0; points.len()];
406 for index in 1..points.len() {
407 parameters[index] = parameters[index - 1] + points[index].sub(points[index - 1]).length();
408 }
409 let length = parameters[points.len() - 1];
410 if length <= 1e-15 {
411 for (index, parameter) in parameters.iter_mut().enumerate() {
412 *parameter = index as f64 / (points.len() - 1) as f64;
413 }
414 } else {
415 for parameter in &mut parameters {
416 *parameter /= length;
417 }
418 }
419 parameters
420}
421
422pub fn simplify_polyline(points: &[Vec3], tolerance: f64) -> Vec<Vec3> {
423 if points.len() <= 2 {
424 return points.to_vec();
425 }
426 let mut keep = vec![false; points.len()];
427 keep[0] = true;
428 keep[points.len() - 1] = true;
429 let mut stack = vec![(0usize, points.len() - 1)];
430 while let Some((start, end)) = stack.pop() {
431 if end - start < 2 {
432 continue;
433 }
434 let a = points[start];
435 let direction = points[end].sub(a);
436 let length_squared = direction.length_squared().max(1e-300);
437 let mut worst = None;
438 let mut worst_distance = tolerance;
439 for (index, point) in points.iter().enumerate().take(end).skip(start + 1) {
440 let fraction = point.sub(a).dot(direction) / length_squared;
441 let fraction = fraction.clamp(0.0, 1.0);
442 let distance = point.sub(a.add(direction.scale(fraction))).length();
443 if distance > worst_distance {
444 worst_distance = distance;
445 worst = Some(index);
446 }
447 }
448 if let Some(index) = worst {
449 keep[index] = true;
450 stack.push((start, index));
451 stack.push((index, end));
452 }
453 }
454 points
455 .iter()
456 .copied()
457 .zip(keep)
458 .filter_map(|(point, keep)| keep.then_some(point))
459 .collect()
460}
461
462pub fn interpolate_curve_local(
463 points: &[Vec3],
464 parameters: &[f64],
465 tension: f64,
466) -> Result<NurbsCurve, String> {
467 if points.len() != parameters.len() || points.len() < 2 {
468 return Err(
469 "interpolate_curve_local: points and parameters must have matching length >= 2".into(),
470 );
471 }
472 if points.len() == 2 {
473 return interpolate_curve(points, 1, parameters);
474 }
475 let count = points.len();
476 let mut tangents = vec![Vec3::default(); count];
477 tangents[0] = points[1]
478 .sub(points[0])
479 .scale(1.0 / (parameters[1] - parameters[0]));
480 tangents[count - 1] = points[count - 1]
481 .sub(points[count - 2])
482 .scale(1.0 / (parameters[count - 1] - parameters[count - 2]));
483 for index in 1..count - 1 {
484 let previous_interval = parameters[index] - parameters[index - 1];
485 let next_interval = parameters[index + 1] - parameters[index];
486 let total = previous_interval + next_interval;
487 let previous_secant = points[index]
488 .sub(points[index - 1])
489 .scale(1.0 / previous_interval);
490 let next_secant = points[index + 1]
491 .sub(points[index])
492 .scale(1.0 / next_interval);
493 let mut tangent =
494 points[index - 1]
495 .scale(-next_interval / (previous_interval * total))
496 .add(points[index].scale(
497 (next_interval - previous_interval) / (previous_interval * next_interval),
498 ))
499 .add(points[index + 1].scale(previous_interval / (next_interval * total)));
500 if previous_secant.dot(next_secant) <= 0.0
501 || tangent.dot(previous_secant) <= 0.0
502 || tangent.dot(next_secant) <= 0.0
503 {
504 tangent = Vec3::default();
505 } else {
506 let maximum = 3.0 * previous_secant.length().min(next_secant.length());
507 if tangent.length() > maximum {
508 tangent = tangent.normalized()?.scale(maximum);
509 }
510 }
511 tangents[index] = tangent;
512 }
513 if tension != 1.0 {
514 for tangent in &mut tangents {
515 *tangent = tangent.scale(tension);
516 }
517 }
518 let mut control_points = vec![Vec4::from_point(points[0], 1.0)];
519 for index in 0..count - 1 {
520 let interval = parameters[index + 1] - parameters[index];
521 control_points.extend([
522 Vec4::from_point(
523 points[index].add(tangents[index].scale(interval / 3.0)),
524 1.0,
525 ),
526 Vec4::from_point(
527 points[index + 1].sub(tangents[index + 1].scale(interval / 3.0)),
528 1.0,
529 ),
530 Vec4::from_point(points[index + 1], 1.0),
531 ]);
532 }
533 let mut knots = vec![parameters[0]; 4];
534 for parameter in ¶meters[1..count - 1] {
535 knots.extend([*parameter; 3]);
536 }
537 knots.extend([parameters[count - 1]; 4]);
538 NurbsCurve::new(3, knots, control_points)
539}
540
541pub fn fit_polyline(
542 points: &[Vec3],
543 tolerance: f64,
544 maximum_points: usize,
545 local_interpolation: bool,
546) -> Result<PolylineFit, String> {
547 let mut kept = simplify_polyline(points, tolerance);
548 if kept.len() > 2 {
549 let total: f64 = kept
550 .windows(2)
551 .map(|pair| pair[1].sub(pair[0]).length())
552 .sum();
553 let floor = (tolerance * 0.01).max(total * 1e-4);
554 let first = kept[0];
555 let last = kept[kept.len() - 1];
556 let mut conditioned = vec![first];
557 for point in &kept[1..kept.len() - 1] {
558 if point.sub(first).length() > floor
559 && point.sub(last).length() > floor
560 && point.sub(*conditioned.last().unwrap()).length() > floor
561 {
562 conditioned.push(*point);
563 }
564 }
565 conditioned.push(last);
566 kept = conditioned;
567 } else {
568 let mut distinct = Vec::new();
569 for point in kept {
570 if distinct
571 .last()
572 .is_none_or(|previous: &Vec3| point.sub(*previous).length() > tolerance * 0.01)
573 {
574 distinct.push(point);
575 }
576 }
577 kept = distinct;
578 }
579 if kept.len() < 2 {
580 return Err("fit_polyline: degenerate polyline".into());
581 }
582 let maximum_points = maximum_points.max(2);
583 if kept.len() > maximum_points {
584 let step = (kept.len() - 1) as f64 / (maximum_points - 1) as f64;
585 kept = (0..maximum_points)
586 .map(|index| kept[(index as f64 * step).round() as usize])
587 .collect();
588 }
589 let total: f64 = kept
590 .windows(2)
591 .map(|pair| pair[1].sub(pair[0]).length())
592 .sum();
593 if total <= 0.0 {
594 return Err("fit_polyline: degenerate polyline".into());
595 }
596 let mut parameters = vec![0.0; kept.len()];
597 let mut accumulated = 0.0;
598 for index in 1..kept.len() {
599 accumulated += kept[index].sub(kept[index - 1]).length();
600 parameters[index] = accumulated / total;
601 }
602 *parameters.last_mut().unwrap() = 1.0;
603 let curve = if local_interpolation {
604 interpolate_curve_local(&kept, ¶meters, 1.0)?
605 } else {
606 interpolate_curve(&kept, 3usize.min(kept.len() - 1), ¶meters)?
607 };
608 Ok(PolylineFit {
609 curve,
610 parameters,
611 kept,
612 })
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn global_interpolation_passes_through_supplied_parameters() {
621 let points = [
622 Vec3::new(0.0, 0.0, 0.0),
623 Vec3::new(1.0, 2.0, 0.5),
624 Vec3::new(3.0, -1.0, 1.0),
625 Vec3::new(5.0, 0.0, 2.0),
626 ];
627 let parameters = [0.0, 0.2, 0.75, 1.0];
628 let curve = interpolate_curve(&points, 3, ¶meters).unwrap();
629 for (point, parameter) in points.iter().zip(parameters) {
630 assert!(curve.evaluate(parameter).unwrap().sub(*point).length() < 1e-10);
631 }
632 }
633
634 #[test]
635 fn polyline_fit_matches_retained_samples() {
636 let points = (0..=40)
637 .map(|index| {
638 let x = index as f64 / 10.0;
639 Vec3::new(x, x.sin(), 0.1 * x * x)
640 })
641 .collect::<Vec<_>>();
642 let fit = fit_polyline(&points, 1e-4, 20, false).unwrap();
643 assert!(fit.kept.len() <= 20);
644 for (point, parameter) in fit.kept.iter().zip(&fit.parameters) {
645 assert!(fit.curve.evaluate(*parameter).unwrap().sub(*point).length() < 1e-9);
646 }
647 }
648
649 #[test]
650 fn local_fit_has_bounded_spans() {
651 let points = [
652 Vec3::new(0.0, 0.0, 0.0),
653 Vec3::new(1.0, 0.0, 0.0),
654 Vec3::new(1.1, 1.0, 0.0),
655 Vec3::new(2.0, 1.0, 0.0),
656 ];
657 let fit = fit_polyline(&points, 1e-8, 20, true).unwrap();
658 for (point, parameter) in fit.kept.iter().zip(&fit.parameters) {
659 assert!(fit.curve.evaluate(*parameter).unwrap().sub(*point).length() < 1e-9);
660 }
661 }
662
663 #[test]
664 fn banded_solver_matches_known_tridiagonal_solution() {
665 let matrix = vec![
666 vec![2.0, -1.0, 0.0, 0.0],
667 vec![-1.0, 2.0, -1.0, 0.0],
668 vec![0.0, -1.0, 2.0, -1.0],
669 vec![0.0, 0.0, -1.0, 2.0],
670 ];
671 let solution = solve_banded(&matrix, &[0.0, 0.0, 0.0, 5.0], 1).unwrap();
672 for (actual, expected) in solution.iter().zip([1.0, 2.0, 3.0, 4.0]) {
673 assert!((actual - expected).abs() < 1e-12);
674 }
675 }
676
677 #[test]
678 fn thinned_interpolation_caps_control_points_and_keeps_endpoints() {
679 let points = (0..100)
680 .map(|index| Vec3::new(index as f64, (index as f64 * 0.1).sin(), 0.0))
681 .collect::<Vec<_>>();
682 let curve = interpolate_curve_thinned(&points, 3, 12).unwrap();
683 assert_eq!(curve.control_points.len(), 12);
684 let [start, end] = curve.domain().unwrap();
685 assert!(curve.evaluate(start).unwrap().sub(points[0]).length() < 1e-12);
686 assert!(
687 curve
688 .evaluate(end)
689 .unwrap()
690 .sub(points[points.len() - 1])
691 .length()
692 < 1e-12
693 );
694 }
695}
696
697#[cfg(test)]
698mod end_tangent_tests {
699 use super::*;
700
701 #[test]
702 fn end_tangent_interpolation_reproduces_points_and_tangents_exactly() {
703 let points = [
704 Vec3::new(0.0, 0.0, 0.0),
705 Vec3::new(1.0, 2.0, 0.5),
706 Vec3::new(3.0, 2.5, 1.0),
707 Vec3::new(5.0, 1.0, 2.0),
708 ];
709 let parameters = [0.0, 0.3, 0.65, 1.0];
710 let start = Vec3::new(0.0, 5.0, 1.0);
711 let end = Vec3::new(4.0, -3.0, 2.0);
712 let curve = interpolate_curve_with_end_tangents(&points, ¶meters, start, end).unwrap();
713 for (point, parameter) in points.iter().zip(¶meters) {
714 let value = curve.evaluate(*parameter).unwrap();
715 assert!(value.sub(*point).length() < 1e-9, "point at {parameter}");
716 }
717 let d0 = curve.derivatives(0.0, 1).unwrap()[1];
718 let d1 = curve.derivatives(1.0, 1).unwrap()[1];
719 assert!(d0.sub(start).length() < 1e-9, "start tangent {d0:?}");
720 assert!(d1.sub(end).length() < 1e-9, "end tangent {d1:?}");
721 }
722
723 #[test]
724 fn two_point_end_tangent_interpolation_is_a_cubic_hermite() {
725 let points = [Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)];
726 let parameters = [0.0, 1.0];
727 let start = Vec3::new(0.0, 3.0, 0.0);
728 let end = Vec3::new(0.0, -3.0, 0.0);
729 let curve = interpolate_curve_with_end_tangents(&points, ¶meters, start, end).unwrap();
730 let expected = Vec3::new(1.0, 3.0 * 0.125 - 3.0 * -0.125, 0.0);
732 let mid = curve.evaluate(0.5).unwrap();
733 assert!(mid.sub(expected).length() < 1e-9, "{mid:?} vs {expected:?}");
734 }
735}
736
737fn raw_basis(knots: &[f64], degree: usize, span: usize, parameter: f64) -> Vec<f64> {
741 let mut basis = vec![0.0; degree + 1];
742 let mut left = vec![0.0; degree + 1];
743 let mut right = vec![0.0; degree + 1];
744 basis[0] = 1.0;
745 for j in 1..=degree {
746 left[j] = parameter - knots[span + 1 - j];
747 right[j] = knots[span + j] - parameter;
748 let mut saved = 0.0;
749 for r in 0..j {
750 let denominator = right[r + 1] + left[j - r];
751 let temp = if denominator.abs() > 0.0 {
752 basis[r] / denominator
753 } else {
754 0.0
755 };
756 basis[r] = saved + right[r + 1] * temp;
757 saved = left[j - r] * temp;
758 }
759 basis[j] = saved;
760 }
761 basis
762}
763
764fn raw_insert_knot(knots: &mut Vec<f64>, controls: &mut Vec<Vec3>, degree: usize, parameter: f64) {
766 let span = knots
770 .iter()
771 .rposition(|&knot| knot <= parameter + 1e-14)
772 .unwrap()
773 .min(controls.len() - 1);
774 let mut fresh = Vec::with_capacity(controls.len() + 1);
775 fresh.extend_from_slice(&controls[..=span - degree]);
776 for i in span - degree + 1..=span {
777 let denominator = knots[i + degree] - knots[i];
778 let alpha = if denominator.abs() > 0.0 {
779 (parameter - knots[i]) / denominator
780 } else {
781 0.0
782 };
783 fresh.push(
784 controls[i - 1]
785 .scale(1.0 - alpha)
786 .add(controls[i].scale(alpha)),
787 );
788 }
789 fresh.extend_from_slice(&controls[span..]);
790 *controls = fresh;
791 knots.insert(span + 1, parameter);
792}
793
794pub fn interpolate_curve_closed(points: &[Vec3], parameters: &[f64]) -> Result<NurbsCurve, String> {
802 let degree = 3usize;
803 let station_count = points.len();
804 if station_count < 4 {
805 return Err("interpolate_curve_closed: need at least 4 stations".into());
806 }
807 if parameters.len() != station_count + 1 {
808 return Err(
809 "interpolate_curve_closed: parameters must have one more entry than points".into(),
810 );
811 }
812 if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
813 return Err("interpolate_curve_closed: parameters must increase".into());
814 }
815 let period = parameters[station_count] - parameters[0];
816 let cyclic = |j: i64| -> f64 {
819 let s = station_count as i64;
820 let wrap = j.div_euclid(s);
821 parameters[j.rem_euclid(s) as usize] + wrap as f64 * period
822 };
823 let raw_knots: Vec<f64> = (-3..=(station_count as i64 + 3)).map(cyclic).collect();
824 let mut matrix = vec![vec![0.0; station_count]; station_count];
827 for i in 0..station_count {
828 let span = i + 3;
829 let basis = raw_basis(&raw_knots, degree, span, parameters[i]);
830 for (offset, value) in basis.iter().enumerate() {
831 let index = (i as i64 + offset as i64 - 3).rem_euclid(station_count as i64) as usize;
834 matrix[i][index] += value;
835 }
836 }
837 let solve_axis = |axis: fn(Vec3) -> f64| {
838 solve_dense(
839 matrix.clone(),
840 points.iter().copied().map(axis).collect::<Vec<_>>(),
841 )
842 };
843 let xs = solve_axis(|point| point.x)?;
844 let ys = solve_axis(|point| point.y)?;
845 let zs = solve_axis(|point| point.z)?;
846 let cyclic_controls: Vec<Vec3> = (0..station_count)
847 .map(|index| Vec3::new(xs[index], ys[index], zs[index]))
848 .collect();
849 let mut window_controls: Vec<Vec3> = (-3..(station_count as i64))
851 .map(|j| cyclic_controls[j.rem_euclid(station_count as i64) as usize])
852 .collect();
853 let mut window_knots = raw_knots.clone();
854 for _ in 0..degree {
857 raw_insert_knot(
858 &mut window_knots,
859 &mut window_controls,
860 degree,
861 parameters[0],
862 );
863 }
864 for _ in 0..degree {
865 raw_insert_knot(
866 &mut window_knots,
867 &mut window_controls,
868 degree,
869 parameters[station_count],
870 );
871 }
872 let first = window_knots
876 .iter()
877 .position(|&knot| (knot - parameters[0]).abs() < 1e-12)
878 .ok_or("interpolate_curve_closed: clamp lost the start knot")?;
879 let last = window_knots
880 .iter()
881 .rposition(|&knot| (knot - parameters[station_count]).abs() < 1e-12)
882 .ok_or("interpolate_curve_closed: clamp lost the end knot")?;
883 let clamped_knots: Vec<f64> = window_knots[first..=last].to_vec();
884 let control_count = clamped_knots.len() - degree - 1;
885 let clamped_controls: Vec<Vec4> = window_controls[first..first + control_count]
886 .iter()
887 .map(|point| Vec4::from_point(*point, 1.0))
888 .collect();
889 NurbsCurve::new(degree, clamped_knots, clamped_controls)
890}
891
892#[cfg(test)]
893mod closed_interpolation_tests {
894 use super::*;
895
896 #[test]
897 fn closed_interpolation_hits_stations_and_closes_c2() {
898 let count = 8usize;
899 let radius = 5.0;
900 let points: Vec<Vec3> = (0..count)
901 .map(|index| {
902 let angle = std::f64::consts::TAU * index as f64 / count as f64;
903 Vec3::new(radius * angle.cos(), radius * angle.sin(), 1.0)
904 })
905 .collect();
906 let parameters: Vec<f64> = (0..=count).map(|index| index as f64).collect();
907 let curve = interpolate_curve_closed(&points, ¶meters).unwrap();
908 for (index, point) in points.iter().enumerate() {
909 let value = curve.evaluate(parameters[index]).unwrap();
910 assert!(
911 value.sub(*point).length() < 1e-9,
912 "station {index}: {value:?} vs {point:?}"
913 );
914 }
915 let start = curve.derivatives(parameters[0], 2).unwrap();
917 let end = curve.derivatives(parameters[count], 2).unwrap();
918 assert!(start[0].sub(end[0]).length() < 1e-9, "C0 seam");
919 assert!(start[1].sub(end[1]).length() < 1e-9, "C1 seam");
920 assert!(start[2].sub(end[2]).length() < 1e-6, "C2 seam");
921 for sample in 0..64 {
923 let t = parameters[0] + (parameters[count] - parameters[0]) * sample as f64 / 64.0;
924 let value = curve.evaluate(t).unwrap();
925 let planar = (value.x * value.x + value.y * value.y).sqrt();
926 assert!(
927 (planar - radius).abs() < 0.05,
928 "radial sag at {t}: {planar}"
929 );
930 assert!((value.z - 1.0).abs() < 1e-9);
931 }
932 }
933
934 #[test]
935 fn closed_interpolation_handles_nonuniform_parameters() {
936 let points = [
937 Vec3::new(4.0, 0.0, 0.0),
938 Vec3::new(1.0, 3.0, 0.5),
939 Vec3::new(-3.0, 2.0, 1.0),
940 Vec3::new(-4.0, -1.0, 0.5),
941 Vec3::new(0.0, -3.5, 0.0),
942 ];
943 let parameters = [0.0, 0.8, 2.1, 3.0, 4.4, 5.5];
944 let curve = interpolate_curve_closed(&points, ¶meters).unwrap();
945 for (index, point) in points.iter().enumerate() {
946 let value = curve.evaluate(parameters[index]).unwrap();
947 assert!(value.sub(*point).length() < 1e-9, "station {index}");
948 }
949 let start = curve.derivatives(0.0, 2).unwrap();
950 let end = curve.derivatives(5.5, 2).unwrap();
951 assert!(start[1].sub(end[1]).length() < 1e-9, "C1 seam");
952 assert!(start[2].sub(end[2]).length() < 1e-6, "C2 seam");
953 }
954}