1use axiolid_contracts::GeomError;
37use axiolid_core::{Point3, Scalar};
38use axiolid_curve::{BSplineCurve3, KnotSpec};
39use axiolid_numeric::{constrained_least_squares, least_squares, Matrix, NumericError};
40use axiolid_surface::BSplineSurface;
41
42use crate::fit::{basis_at, chord_parameters, collapse, span_of};
43
44pub type FitResult<T> = Result<T, GeomError>;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Parameterisation {
50 Uniform,
52 ChordLength,
55 Centripetal,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub struct CurveFitOptions {
64 pub degree: u16,
66 pub control_point_count: usize,
70 pub parameterisation: Parameterisation,
72 pub interpolate_endpoints: bool,
76 pub smoothing: Scalar,
80 pub parameter_correction_iterations: usize,
85}
86
87impl Default for CurveFitOptions {
88 fn default() -> Self {
89 Self {
90 degree: 3,
91 control_point_count: 0,
92 parameterisation: Parameterisation::ChordLength,
93 interpolate_endpoints: false,
94 smoothing: 0.0,
95 parameter_correction_iterations: 0,
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq)]
102pub struct CurveFit3 {
103 pub curve: BSplineCurve3,
105 pub max_deviation: Scalar,
108 pub rms_deviation: Scalar,
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub struct CurveFitTolerance {
115 pub degree: u16,
117 pub parameterisation: Parameterisation,
119 pub interpolate_endpoints: bool,
121 pub max_deviation: Scalar,
124 pub max_control_points: usize,
126}
127
128fn validate_finite(points: &[Point3]) -> FitResult<()> {
129 if !points.iter().all(|p| p.is_finite()) {
130 return Err(GeomError::InvalidInput(
131 "fitting points must be finite".to_owned(),
132 ));
133 }
134 Ok(())
135}
136
137fn validate_degree(degree: u16) -> FitResult<usize> {
138 if degree == 0 {
139 return Err(GeomError::InvalidInput(
140 "fitting degree must be at least 1".to_owned(),
141 ));
142 }
143 Ok(usize::from(degree))
144}
145
146pub(crate) fn parameters_for(points: &[Point3], mode: Parameterisation) -> FitResult<Vec<Scalar>> {
152 match mode {
153 Parameterisation::Uniform => {
154 let last = points.len() - 1;
155 Ok((0..points.len())
156 .map(|k| k as Scalar / last as Scalar)
157 .collect())
158 }
159 Parameterisation::ChordLength => chord_parameters(points),
160 Parameterisation::Centripetal => centripetal_parameters(points),
161 }
162}
163
164fn centripetal_parameters(points: &[Point3]) -> FitResult<Vec<Scalar>> {
165 let mut distances = Vec::with_capacity(points.len());
166 distances.push(0.0);
167 let mut total = 0.0;
168 for pair in points.windows(2) {
169 let step = (pair[1] - pair[0]).length();
170 if step <= 0.0 {
171 return Err(GeomError::Degenerate(
172 "consecutive fitting points coincide, so centripetal spacing is undefined"
173 .to_owned(),
174 ));
175 }
176 total += step.sqrt();
177 distances.push(total);
178 }
179 Ok(distances.into_iter().map(|d| d / total).collect())
180}
181
182pub(crate) fn approximation_knots(
187 parameters: &[Scalar],
188 degree: usize,
189 control_count: usize,
190) -> Vec<Scalar> {
191 let m = parameters.len() - 1;
192 let n = control_count - 1;
193 let d = (m + 1) as Scalar / (n - degree + 1) as Scalar;
194 let mut knots = vec![0.0; degree + 1];
195 for j in 1..=(n - degree) {
196 let temp = j as Scalar * d;
197 let i = temp.floor() as usize;
198 let alpha = temp - i as Scalar;
199 let value = if i == 0 {
200 parameters[0]
201 } else {
202 (1.0 - alpha) * parameters[i - 1] + alpha * parameters[i]
203 };
204 knots.push(value);
205 }
206 knots.extend(core::iter::repeat_n(1.0, degree + 1));
207 knots
208}
209
210fn design_matrix(
213 parameters: &[Scalar],
214 expanded_knots: &[Scalar],
215 degree: usize,
216 control_count: usize,
217) -> Matrix {
218 let n = control_count - 1;
219 let mut dense = vec![0.0; parameters.len() * control_count];
220 for (row, &t) in parameters.iter().enumerate() {
221 let span = span_of(expanded_knots, n, degree, t);
222 let basis = basis_at(span, t, degree, expanded_knots);
223 for (offset, value) in basis.iter().enumerate() {
224 dense[row * control_count + span - degree + offset] = *value;
225 }
226 }
227 Matrix::from_fn(parameters.len(), control_count, |i, j| {
228 dense[i * control_count + j]
229 })
230}
231
232fn evaluate(
235 control_points: &[Point3],
236 expanded_knots: &[Scalar],
237 degree: usize,
238 t: Scalar,
239) -> Point3 {
240 let n = control_points.len() - 1;
241 let span = span_of(expanded_knots, n, degree, t);
242 let basis = basis_at(span, t, degree, expanded_knots);
243 let mut acc = Point3::new(0.0, 0.0, 0.0);
244 for (offset, value) in basis.iter().enumerate() {
245 let cp = control_points[span - degree + offset];
246 acc = Point3::new(
247 acc.x + value * cp.x,
248 acc.y + value * cp.y,
249 acc.z + value * cp.z,
250 );
251 }
252 acc
253}
254
255fn evaluate_derivative(
259 control_points: &[Point3],
260 expanded_knots: &[Scalar],
261 degree: usize,
262 t: Scalar,
263) -> axiolid_core::Vec3 {
264 if degree == 0 {
265 return axiolid_core::Vec3::new(0.0, 0.0, 0.0);
266 }
267 let n = control_points.len() - 1;
268 let span = span_of(expanded_knots, n, degree, t);
269 let basis = basis_at(span, t, degree - 1, expanded_knots);
270 let mut acc = axiolid_core::Vec3::new(0.0, 0.0, 0.0);
271 let p = degree as Scalar;
272 for (offset, value) in basis.iter().enumerate() {
273 let i = span - degree + offset;
274 let denom = expanded_knots[i + degree + 1] - expanded_knots[i + 1];
275 if denom == 0.0 {
276 continue;
277 }
278 let coeff = p / denom * value;
279 acc += (control_points[i + 1] - control_points[i]) * coeff;
280 }
281 acc
282}
283
284fn from_numeric(err: NumericError) -> GeomError {
285 match err {
286 NumericError::RankDeficient {
287 name,
288 rank,
289 required,
290 } => GeomError::Degenerate(format!(
291 "{name} has numerical rank {rank}, needs {required} for a unique least-squares fit"
292 )),
293 other => GeomError::Degenerate(other.to_string()),
294 }
295}
296
297#[allow(clippy::too_many_arguments)]
300fn solve_coordinate(
301 design: &Matrix,
302 smoothing_rows: Option<&Matrix>,
303 values: &[Scalar],
304 control_count: usize,
305 interpolate_endpoints: bool,
306 first: Scalar,
307 last: Scalar,
308) -> FitResult<Vec<Scalar>> {
309 let (a, b): (Matrix, Vec<Scalar>) = if let Some(extra) = smoothing_rows {
310 let rows = design.rows() + extra.rows();
311 let a = Matrix::from_fn(rows, control_count, |i, j| {
312 if i < design.rows() {
313 design[(i, j)]
314 } else {
315 extra[(i - design.rows(), j)]
316 }
317 });
318 let mut b = values.to_vec();
319 b.resize(rows, 0.0);
320 (a, b)
321 } else {
322 (design.clone(), values.to_vec())
323 };
324
325 if interpolate_endpoints {
326 let mut c = Matrix::zeros(2, control_count);
327 c[(0, 0)] = 1.0;
328 c[(1, control_count - 1)] = 1.0;
329 let d = [first, last];
330 let solution = constrained_least_squares(&a, &b, &c, &d).map_err(from_numeric)?;
331 Ok(solution.x)
332 } else {
333 let solution = least_squares(&a, &b).map_err(from_numeric)?;
334 Ok(solution.x)
335 }
336}
337
338fn smoothing_rows(control_count: usize, weight: Scalar) -> Option<Matrix> {
341 if weight <= 0.0 || control_count < 3 {
342 return None;
343 }
344 let scale = weight.sqrt();
345 let rows = control_count - 2;
346 Some(Matrix::from_fn(rows, control_count, |i, j| {
347 if j == i {
348 scale
349 } else if j == i + 1 {
350 -2.0 * scale
351 } else if j == i + 2 {
352 scale
353 } else {
354 0.0
355 }
356 }))
357}
358
359fn assemble_curve(
360 control_points: Vec<Point3>,
361 expanded_knots: &[Scalar],
362 degree: usize,
363) -> FitResult<BSplineCurve3> {
364 let (knots, multiplicities) = collapse(expanded_knots);
365 Ok(BSplineCurve3 {
366 degree: u16::try_from(degree)
367 .map_err(|_| GeomError::InvalidInput("degree overflows".to_owned()))?,
368 control_points,
369 knots,
370 multiplicities,
371 weights: None,
372 knot_spec: KnotSpec::Unspecified,
373 closed: false,
374 self_intersect: None,
375 })
376}
377
378fn deviation(
379 points: &[Point3],
380 curve: &[Point3],
381 knots: &[Scalar],
382 degree: usize,
383 t: &[Scalar],
384) -> (Scalar, Scalar) {
385 let mut max = 0.0;
386 let mut sum_sq = 0.0;
387 for (point, &tk) in points.iter().zip(t) {
388 let fitted = evaluate(curve, knots, degree, tk);
389 let d = (fitted - *point).length();
390 max = Scalar::max(max, d);
391 sum_sq += d * d;
392 }
393 let rms = (sum_sq / points.len() as Scalar).sqrt();
394 (max, rms)
395}
396
397pub fn fit_curve3(points: &[Point3], options: &CurveFitOptions) -> FitResult<CurveFit3> {
409 if points.len() < 2 {
410 return Err(GeomError::InvalidInput(
411 "fitting needs at least two points".to_owned(),
412 ));
413 }
414 validate_finite(points)?;
415 let degree = validate_degree(options.degree)?;
416 let control_count = options.control_point_count;
417 if control_count < degree + 1 || control_count > points.len() {
418 return Err(GeomError::InvalidInput(format!(
419 "control point count {control_count} must be between {} and {} for degree {degree} \
420 and {} points",
421 degree + 1,
422 points.len(),
423 points.len()
424 )));
425 }
426 if !options.smoothing.is_finite() || options.smoothing < 0.0 {
427 return Err(GeomError::InvalidInput(
428 "smoothing weight must be finite and non-negative".to_owned(),
429 ));
430 }
431
432 let mut parameters = parameters_for(points, options.parameterisation)?;
433 let mut expanded = approximation_knots(¶meters, degree, control_count);
434 let mut design = design_matrix(¶meters, &expanded, degree, control_count);
435 let extra = smoothing_rows(control_count, options.smoothing);
436
437 let xs: Vec<Scalar> = points.iter().map(|p| p.x).collect();
438 let ys: Vec<Scalar> = points.iter().map(|p| p.y).collect();
439 let zs: Vec<Scalar> = points.iter().map(|p| p.z).collect();
440
441 let mut control_points = solve_xyz(
442 &design,
443 extra.as_ref(),
444 &xs,
445 &ys,
446 &zs,
447 control_count,
448 options.interpolate_endpoints,
449 points[0],
450 points[points.len() - 1],
451 )?;
452
453 for _ in 0..options.parameter_correction_iterations {
454 parameters = correct_parameters(points, &control_points, &expanded, degree, ¶meters);
455 expanded = approximation_knots(¶meters, degree, control_count);
456 design = design_matrix(¶meters, &expanded, degree, control_count);
457 let extra = smoothing_rows(control_count, options.smoothing);
458 control_points = solve_xyz(
459 &design,
460 extra.as_ref(),
461 &xs,
462 &ys,
463 &zs,
464 control_count,
465 options.interpolate_endpoints,
466 points[0],
467 points[points.len() - 1],
468 )?;
469 }
470
471 let (max_deviation, rms_deviation) =
472 deviation(points, &control_points, &expanded, degree, ¶meters);
473 let curve = assemble_curve(control_points, &expanded, degree)?;
474 Ok(CurveFit3 {
475 curve,
476 max_deviation,
477 rms_deviation,
478 })
479}
480
481#[allow(clippy::too_many_arguments)]
482fn solve_xyz(
483 design: &Matrix,
484 extra: Option<&Matrix>,
485 xs: &[Scalar],
486 ys: &[Scalar],
487 zs: &[Scalar],
488 control_count: usize,
489 interpolate_endpoints: bool,
490 first: Point3,
491 last: Point3,
492) -> FitResult<Vec<Point3>> {
493 let x = solve_coordinate(
494 design,
495 extra,
496 xs,
497 control_count,
498 interpolate_endpoints,
499 first.x,
500 last.x,
501 )?;
502 let y = solve_coordinate(
503 design,
504 extra,
505 ys,
506 control_count,
507 interpolate_endpoints,
508 first.y,
509 last.y,
510 )?;
511 let z = solve_coordinate(
512 design,
513 extra,
514 zs,
515 control_count,
516 interpolate_endpoints,
517 first.z,
518 last.z,
519 )?;
520 Ok((0..control_count)
521 .map(|i| Point3::new(x[i], y[i], z[i]))
522 .collect())
523}
524
525fn correct_parameters(
529 points: &[Point3],
530 control_points: &[Point3],
531 expanded_knots: &[Scalar],
532 degree: usize,
533 parameters: &[Scalar],
534) -> Vec<Scalar> {
535 let last = parameters.len() - 1;
536 let mut out = Vec::with_capacity(points.len());
537 for (k, (point, &t)) in points.iter().zip(parameters).enumerate() {
538 if k == 0 || k == last {
539 out.push(t);
540 continue;
541 }
542 let c = evaluate(control_points, expanded_knots, degree, t);
543 let d = evaluate_derivative(control_points, expanded_knots, degree, t);
544 let speed_sq = d.x * d.x + d.y * d.y + d.z * d.z;
545 let corrected = if speed_sq > 0.0 {
546 let diff = c - *point;
547 let num = diff.x * d.x + diff.y * d.y + diff.z * d.z;
548 (t - num / speed_sq).clamp(0.0, 1.0)
549 } else {
550 t
551 };
552 out.push(corrected);
553 }
554 for i in 1..out.len() {
558 if out[i] <= out[i - 1] {
559 out[i] = parameters[i];
560 }
561 }
562 out
563}
564
565pub fn fit_curve3_to_tolerance(
575 points: &[Point3],
576 tolerance: &CurveFitTolerance,
577) -> FitResult<CurveFit3> {
578 if !tolerance.max_deviation.is_finite() || tolerance.max_deviation < 0.0 {
579 return Err(GeomError::InvalidInput(
580 "tolerance must be finite and non-negative".to_owned(),
581 ));
582 }
583 let degree = validate_degree(tolerance.degree)?;
584 let mut control_count = degree + 1;
585 loop {
586 if control_count > tolerance.max_control_points || control_count > points.len() {
587 return Err(GeomError::BudgetExceeded {
588 resource: "control points",
589 });
590 }
591 let options = CurveFitOptions {
592 degree: tolerance.degree,
593 control_point_count: control_count,
594 parameterisation: tolerance.parameterisation,
595 interpolate_endpoints: tolerance.interpolate_endpoints,
596 smoothing: 0.0,
597 parameter_correction_iterations: 0,
598 };
599 let fit = fit_curve3(points, &options)?;
600 if fit.max_deviation <= tolerance.max_deviation {
601 return Ok(fit);
602 }
603 control_count += 1;
607 }
608}
609
610#[derive(Debug, Clone, PartialEq)]
612pub struct SurfaceFitOptions {
613 pub u_degree: u16,
615 pub v_degree: u16,
617 pub u_control_count: usize,
619 pub v_control_count: usize,
621 pub parameterisation: Parameterisation,
623}
624
625#[derive(Debug, Clone, PartialEq)]
627pub struct SurfaceFit {
628 pub surface: BSplineSurface,
630 pub max_deviation: Scalar,
633 pub rms_deviation: Scalar,
635}
636
637pub fn fit_surface_grid(
659 points: &[Vec<Point3>],
660 options: &SurfaceFitOptions,
661) -> FitResult<SurfaceFit> {
662 let rows = points.len();
663 if rows < 2 {
664 return Err(GeomError::InvalidInput(
665 "surface fitting needs at least two rows".to_owned(),
666 ));
667 }
668 let cols = points[0].len();
669 if cols < 2 {
670 return Err(GeomError::InvalidInput(
671 "surface fitting needs at least two columns".to_owned(),
672 ));
673 }
674 for (i, row) in points.iter().enumerate() {
675 if row.len() != cols {
676 return Err(GeomError::InvalidInput(format!(
677 "row {i} has {} points but row 0 has {cols}; the grid must be rectangular",
678 row.len()
679 )));
680 }
681 }
682 let u_degree = validate_degree(options.u_degree)?;
683 let v_degree = validate_degree(options.v_degree)?;
684 if options.u_control_count < u_degree + 1 || options.u_control_count > rows {
685 return Err(GeomError::InvalidInput(format!(
686 "u control point count {} must be between {} and {rows}",
687 options.u_control_count,
688 u_degree + 1
689 )));
690 }
691 if options.v_control_count < v_degree + 1 || options.v_control_count > cols {
692 return Err(GeomError::InvalidInput(format!(
693 "v control point count {} must be between {} and {cols}",
694 options.v_control_count,
695 v_degree + 1
696 )));
697 }
698
699 let mut u_params = vec![0.0; rows];
704 for row in points {
705 let per_row = parameters_for(row, options.parameterisation)?;
706 for (acc, value) in u_params.iter_mut().zip(&per_row) {
707 *acc += value / cols as Scalar;
708 }
709 }
710 u_params[0] = 0.0;
711 *u_params.last_mut().expect("rows checked non-empty") = 1.0;
712
713 let mut v_params = vec![0.0; cols];
714 for j in 0..cols {
715 let column: Vec<Point3> = points.iter().map(|row| row[j]).collect();
716 let per_col = parameters_for(&column, options.parameterisation)?;
717 for (acc, value) in v_params.iter_mut().zip(&per_col) {
718 *acc += value / rows as Scalar;
719 }
720 }
721 v_params[0] = 0.0;
722 *v_params.last_mut().expect("columns checked non-empty") = 1.0;
723
724 let u_knots = approximation_knots(&u_params, u_degree, options.u_control_count);
725 let v_knots = approximation_knots(&v_params, v_degree, options.v_control_count);
726
727 let u_design = design_matrix(&u_params, &u_knots, u_degree, options.u_control_count);
730 let mut intermediate: Vec<Vec<Point3>> =
731 vec![Vec::with_capacity(cols); options.u_control_count];
732 for j in 0..cols {
733 let column: Vec<Point3> = points.iter().map(|row| row[j]).collect();
734 let xs: Vec<Scalar> = column.iter().map(|p| p.x).collect();
735 let ys: Vec<Scalar> = column.iter().map(|p| p.y).collect();
736 let zs: Vec<Scalar> = column.iter().map(|p| p.z).collect();
737 let solved = solve_xyz(
738 &u_design,
739 None,
740 &xs,
741 &ys,
742 &zs,
743 options.u_control_count,
744 false,
745 column[0],
746 column[rows - 1],
747 )?;
748 for (row, point) in intermediate.iter_mut().zip(solved) {
749 row.push(point);
750 }
751 }
752
753 let v_design = design_matrix(&v_params, &v_knots, v_degree, options.v_control_count);
755 let mut net: Vec<Vec<Point3>> = Vec::with_capacity(options.u_control_count);
756 for row in &intermediate {
757 let xs: Vec<Scalar> = row.iter().map(|p| p.x).collect();
758 let ys: Vec<Scalar> = row.iter().map(|p| p.y).collect();
759 let zs: Vec<Scalar> = row.iter().map(|p| p.z).collect();
760 let solved = solve_xyz(
761 &v_design,
762 None,
763 &xs,
764 &ys,
765 &zs,
766 options.v_control_count,
767 false,
768 row[0],
769 row[cols - 1],
770 )?;
771 net.push(solved);
772 }
773
774 let mut max_deviation: Scalar = 0.0;
777 let mut sum_sq = 0.0;
778 for (i, row) in points.iter().enumerate() {
779 for (j, point) in row.iter().enumerate() {
780 let fitted = evaluate_surface(
781 &net,
782 &u_knots,
783 u_degree,
784 &v_knots,
785 v_degree,
786 u_params[i],
787 v_params[j],
788 );
789 let d = (fitted - *point).length();
790 max_deviation = Scalar::max(max_deviation, d);
791 sum_sq += d * d;
792 }
793 }
794 let rms_deviation = (sum_sq / (rows * cols) as Scalar).sqrt();
795
796 let (u_knots_distinct, u_multiplicities) = collapse(&u_knots);
797 let (v_knots_distinct, v_multiplicities) = collapse(&v_knots);
798 let surface = BSplineSurface {
799 u_degree: options.u_degree,
800 v_degree: options.v_degree,
801 control_points: net,
802 u_knots: u_knots_distinct,
803 u_multiplicities,
804 v_knots: v_knots_distinct,
805 v_multiplicities,
806 weights: None,
807 u_closed: false,
808 v_closed: false,
809 knot_spec: KnotSpec::Unspecified,
810 self_intersect: None,
811 };
812 Ok(SurfaceFit {
813 surface,
814 max_deviation,
815 rms_deviation,
816 })
817}
818
819fn evaluate_surface(
822 net: &[Vec<Point3>],
823 u_knots: &[Scalar],
824 u_degree: usize,
825 v_knots: &[Scalar],
826 v_degree: usize,
827 u: Scalar,
828 v: Scalar,
829) -> Point3 {
830 let rows = net.len();
831 let cols = net[0].len();
832 let u_span = span_of(u_knots, rows - 1, u_degree, u);
833 let u_basis = basis_at(u_span, u, u_degree, u_knots);
834 let v_span = span_of(v_knots, cols - 1, v_degree, v);
835 let v_basis = basis_at(v_span, v, v_degree, v_knots);
836 let mut acc = Point3::new(0.0, 0.0, 0.0);
837 for (ui, &uw) in u_basis.iter().enumerate() {
838 let row = u_span - u_degree + ui;
839 for (vi, &vw) in v_basis.iter().enumerate() {
840 let col = v_span - v_degree + vi;
841 let weight = uw * vw;
842 let cp = net[row][col];
843 acc = Point3::new(
844 acc.x + weight * cp.x,
845 acc.y + weight * cp.y,
846 acc.z + weight * cp.z,
847 );
848 }
849 }
850 acc
851}