Skip to main content

oxiphysics_geometry/bspline/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::{solve_3x_systems, solve_linear_system_row};
6
7/// A rational B-spline (NURBS) curve in 3D space.
8///
9/// C(t) = Σ w_i N_{i,p}(t) P_i / Σ w_i N_{i,p}(t)
10#[derive(Debug, Clone)]
11pub struct NurbsCurve {
12    /// Underlying B-spline basis.
13    pub basis: BsplineBasis,
14    /// Control points \[P_0, …, P_n\], each \[x, y, z\].
15    pub control_points: Vec<[f64; 3]>,
16    /// Weights w_i > 0.
17    pub weights: Vec<f64>,
18}
19impl NurbsCurve {
20    /// Create a new NURBS curve.
21    pub fn new(
22        degree: usize,
23        knot_vector: KnotVector,
24        control_points: Vec<[f64; 3]>,
25        weights: Vec<f64>,
26    ) -> Self {
27        let n_ctrl = control_points.len();
28        assert_eq!(
29            weights.len(),
30            n_ctrl,
31            "weights and control points must have the same length"
32        );
33        let basis = BsplineBasis::new(degree, knot_vector, n_ctrl);
34        Self {
35            basis,
36            control_points,
37            weights,
38        }
39    }
40    /// Create a NURBS circle arc from center, radius, start and end angles (radians).
41    ///
42    /// Uses the standard 9-point representation for a full circle (degree 2).
43    /// For arcs smaller than 2Ï€, a simplified quadratic NURBS is returned.
44    pub fn circle_arc(center: [f64; 3], radius: f64, start_angle: f64, end_angle: f64) -> Self {
45        let sweep = end_angle - start_angle;
46        let n_arcs = if sweep.abs() <= std::f64::consts::FRAC_PI_2 {
47            1
48        } else if sweep.abs() <= std::f64::consts::PI {
49            2
50        } else if sweep.abs() <= 3.0 * std::f64::consts::FRAC_PI_2 {
51            3
52        } else {
53            4
54        };
55        let n_pts = 2 * n_arcs + 1;
56        let mut ctrl_pts = Vec::with_capacity(n_pts);
57        let mut weights = Vec::with_capacity(n_pts);
58        let d_theta = sweep / n_arcs as f64;
59        let w1 = (d_theta / 2.0).cos();
60        let mut angle = start_angle;
61        ctrl_pts.push([
62            center[0] + radius * angle.cos(),
63            center[1] + radius * angle.sin(),
64            center[2],
65        ]);
66        weights.push(1.0);
67        for _i in 0..n_arcs {
68            let a_mid = angle + d_theta * 0.5;
69            let a_end = angle + d_theta;
70            ctrl_pts.push([
71                center[0] + radius / w1 * a_mid.cos(),
72                center[1] + radius / w1 * a_mid.sin(),
73                center[2],
74            ]);
75            weights.push(w1);
76            ctrl_pts.push([
77                center[0] + radius * a_end.cos(),
78                center[1] + radius * a_end.sin(),
79                center[2],
80            ]);
81            weights.push(1.0);
82            angle = a_end;
83        }
84        let mut knots = vec![0.0_f64; 3];
85        let knot_step = 1.0 / n_arcs as f64;
86        for i in 1..n_arcs {
87            knots.push(i as f64 * knot_step);
88            knots.push(i as f64 * knot_step);
89        }
90        knots.extend(std::iter::repeat_n(1.0_f64, 3));
91        let kv = KnotVector::new(knots);
92        Self::new(2, kv, ctrl_pts, weights)
93    }
94    /// Evaluate the NURBS curve at parameter t: returns \[x, y, z\].
95    pub fn eval(&self, t: f64) -> [f64; 3] {
96        let (span, nonzero) = self.basis.eval_nonzero(t);
97        let p = self.basis.degree;
98        let n_ctrl = self.control_points.len();
99        let mut num = [0.0_f64; 3];
100        let mut denom = 0.0_f64;
101        for (k, &n_val) in nonzero[..=p].iter().enumerate() {
102            let idx = span + k - p;
103            if idx >= n_ctrl {
104                continue;
105            }
106            let wn = self.weights[idx] * n_val;
107            denom += wn;
108            let cp = self.control_points[idx];
109            for d in 0..3 {
110                num[d] += wn * cp[d];
111            }
112        }
113        if denom.abs() < 1e-30 {
114            return self.control_points[0];
115        }
116        [num[0] / denom, num[1] / denom, num[2] / denom]
117    }
118    /// Evaluate the first derivative of the NURBS curve at t.
119    ///
120    /// Uses the quotient rule: C'(t) = (W'(t) P̃(t) - W(t)^2 P̃'(t)) / W(t)^2
121    pub fn eval_deriv(&self, t: f64) -> [f64; 3] {
122        let h = 1e-7;
123        let t0 = (t - h).max(self.basis.knot_vector.knots[0]);
124        let t1 = (t + h).min(*self.basis.knot_vector.knots.last().unwrap_or(&1.0));
125        let p0 = self.eval(t0);
126        let p1 = self.eval(t1);
127        let dt = t1 - t0;
128        [
129            (p1[0] - p0[0]) / dt,
130            (p1[1] - p0[1]) / dt,
131            (p1[2] - p0[2]) / dt,
132        ]
133    }
134    /// Update a control point and its weight.
135    pub fn update_control_point(&mut self, i: usize, point: [f64; 3], weight: f64) {
136        self.control_points[i] = point;
137        self.weights[i] = weight;
138    }
139    /// Compute arc length using Gaussian quadrature.
140    pub fn arc_length(&self, t_start: f64, t_end: f64, n_intervals: usize) -> f64 {
141        let h = (t_end - t_start) / n_intervals as f64;
142        let gp = [-0.906180, -0.538469, 0.0, 0.538469, 0.906180];
143        let gw = [0.236927, 0.478629, 0.568889, 0.478629, 0.236927];
144        let mut length = 0.0;
145        for i in 0..n_intervals {
146            let a = t_start + i as f64 * h;
147            let b = a + h;
148            let mid = (a + b) * 0.5;
149            let half = (b - a) * 0.5;
150            for (&xi, &wi) in gp.iter().zip(gw.iter()) {
151                let t = mid + half * xi;
152                let dt = self.eval_deriv(t);
153                let speed = (dt[0] * dt[0] + dt[1] * dt[1] + dt[2] * dt[2]).sqrt();
154                length += wi * half * speed;
155            }
156        }
157        length
158    }
159    /// Compute curvature using the B-spline curve's curvature formula (numerical).
160    pub fn curvature(&self, t: f64) -> f64 {
161        let h = 1e-5;
162        let t_min = self.basis.knot_vector.knots[0];
163        let t_max = *self.basis.knot_vector.knots.last().unwrap_or(&1.0);
164        let t0 = (t - h).max(t_min);
165        let t1 = (t + h).min(t_max);
166        let dt = t1 - t0;
167        if dt < 1e-14 {
168            return 0.0;
169        }
170        let d1 = self.eval_deriv(t);
171        let p0 = self.eval(t0);
172        let p1 = self.eval(t);
173        let p2 = self.eval(t1);
174        let d2 = [
175            (p2[0] - 2.0 * p1[0] + p0[0]) / (h * h),
176            (p2[1] - 2.0 * p1[1] + p0[1]) / (h * h),
177            (p2[2] - 2.0 * p1[2] + p0[2]) / (h * h),
178        ];
179        let cross = [
180            d1[1] * d2[2] - d1[2] * d2[1],
181            d1[2] * d2[0] - d1[0] * d2[2],
182            d1[0] * d2[1] - d1[1] * d2[0],
183        ];
184        let cross_mag = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
185        let d1_mag = (d1[0] * d1[0] + d1[1] * d1[1] + d1[2] * d1[2]).sqrt();
186        if d1_mag < 1e-14 {
187            0.0
188        } else {
189            cross_mag / d1_mag.powi(3)
190        }
191    }
192    /// Sample the NURBS curve at n_points uniformly spaced parameter values.
193    pub fn sample(&self, n_points: usize) -> Vec<[f64; 3]> {
194        let (t0, t1) = self.basis.knot_vector.domain();
195        (0..n_points)
196            .map(|i| {
197                let t = t0 + (t1 - t0) * i as f64 / (n_points - 1).max(1) as f64;
198                self.eval(t)
199            })
200            .collect()
201    }
202    /// Evaluate the B-spline basis function N_{i,p}(t) (Cox-de Boor recursion).
203    pub fn b_spline_basis(i: usize, p: usize, t: f64, knots: &[f64]) -> f64 {
204        if p == 0 {
205            let at_end = (t - knots[knots.len() - 1]).abs() < 1e-14
206                && (knots[i + 1] - knots[knots.len() - 1]).abs() < 1e-14;
207            return if (knots[i] <= t && t < knots[i + 1]) || at_end {
208                1.0
209            } else {
210                0.0
211            };
212        }
213        let left_denom = knots[i + p] - knots[i];
214        let left = if left_denom.abs() > 1e-14 {
215            (t - knots[i]) / left_denom * Self::b_spline_basis(i, p - 1, t, knots)
216        } else {
217            0.0
218        };
219        let right_denom = knots[i + p + 1] - knots[i + 1];
220        let right = if right_denom.abs() > 1e-14 {
221            (knots[i + p + 1] - t) / right_denom * Self::b_spline_basis(i + 1, p - 1, t, knots)
222        } else {
223            0.0
224        };
225        left + right
226    }
227    /// Create a NURBS curve from 3D control points, weights, and degree.
228    ///
229    /// Automatically creates a clamped uniform knot vector.
230    pub fn from_points_and_weights(
231        points: Vec<[f64; 3]>,
232        weights: Vec<f64>,
233        degree: usize,
234    ) -> Self {
235        let n = points.len();
236        let m = n + degree + 1;
237        let mut knots = vec![0.0_f64; degree + 1];
238        let interior = m - 2 * (degree + 1);
239        for i in 1..=interior {
240            knots.push(i as f64 / (interior + 1) as f64);
241        }
242        knots.extend(vec![1.0_f64; degree + 1]);
243        let kv = KnotVector::new(knots);
244        Self::new(degree, kv, points, weights)
245    }
246    /// Create a NURBS circle in the XY plane with the given radius (degree 2, 9 points).
247    pub fn circle(radius: f64) -> Self {
248        let w = std::f64::consts::FRAC_1_SQRT_2;
249        let r = radius;
250        let ctrl = vec![
251            [r, 0.0, 0.0],
252            [r, r, 0.0],
253            [0.0, r, 0.0],
254            [-r, r, 0.0],
255            [-r, 0.0, 0.0],
256            [-r, -r, 0.0],
257            [0.0, -r, 0.0],
258            [r, -r, 0.0],
259            [r, 0.0, 0.0],
260        ];
261        let weights = vec![1.0, w, 1.0, w, 1.0, w, 1.0, w, 1.0];
262        let knots = KnotVector::new(vec![
263            0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
264        ]);
265        Self::new(2, knots, ctrl, weights)
266    }
267    /// Compute arc-length reparametrization: return `n_output` uniformly-spaced
268    /// (by arc length) sample points.
269    pub fn compute_arc_length_reparametrize(
270        &self,
271        n_intervals: usize,
272        n_output: usize,
273    ) -> Vec<[f64; 3]> {
274        let (arc_lengths, params) = self.arc_length_table(n_intervals);
275        let total_len = *arc_lengths.last().unwrap_or(&0.0);
276        if total_len < 1e-14 || n_output == 0 {
277            return Vec::new();
278        }
279        let mut result = Vec::with_capacity(n_output);
280        for k in 0..n_output {
281            let target = total_len * k as f64 / (n_output - 1).max(1) as f64;
282            let idx = arc_lengths
283                .partition_point(|&s| s < target)
284                .min(arc_lengths.len() - 1);
285            let t = if idx == 0 {
286                params[0]
287            } else if (arc_lengths[idx] - arc_lengths[idx - 1]).abs() < 1e-14 {
288                params[idx]
289            } else {
290                let frac =
291                    (target - arc_lengths[idx - 1]) / (arc_lengths[idx] - arc_lengths[idx - 1]);
292                params[idx - 1] + frac * (params[idx] - params[idx - 1])
293            };
294            result.push(self.eval(t));
295        }
296        result
297    }
298    /// Build an arc-length look-up table with `n` samples.
299    ///
300    /// Returns `(cumulative_arc_lengths, corresponding_parameters)`.
301    pub fn arc_length_table(&self, n: usize) -> (Vec<f64>, Vec<f64>) {
302        let (t0, t1) = self.basis.knot_vector.domain();
303        let mut arc_lengths = Vec::with_capacity(n + 1);
304        let mut params = Vec::with_capacity(n + 1);
305        let mut cumulative = 0.0_f64;
306        let mut prev_pt = self.eval(t0);
307        arc_lengths.push(0.0);
308        params.push(t0);
309        for i in 1..=n {
310            let t = t0 + (t1 - t0) * i as f64 / n as f64;
311            let pt = self.eval(t);
312            let dx = pt[0] - prev_pt[0];
313            let dy = pt[1] - prev_pt[1];
314            let dz = pt[2] - prev_pt[2];
315            cumulative += (dx * dx + dy * dy + dz * dz).sqrt();
316            arc_lengths.push(cumulative);
317            params.push(t);
318            prev_pt = pt;
319        }
320        (arc_lengths, params)
321    }
322}
323/// B-spline basis functions and their derivatives.
324///
325/// Implements the Cox-de Boor recursion:
326///
327/// N_{i,0}(t) = 1 if t_i ≤ t < t_{i+1}, else 0
328///
329/// N_{i,p}(t) = (t − t_i)/(t_{i+p} − t_i) N_{i,p-1}(t) + (t_{i+p+1} − t)/(t_{i+p+1} − t_{i+1}) N_{i+1,p-1}(t)
330#[derive(Debug, Clone)]
331pub struct BsplineBasis {
332    /// Polynomial degree p.
333    pub degree: usize,
334    /// Knot vector.
335    pub knot_vector: KnotVector,
336    /// Number of control points (n+1).
337    pub n_ctrl: usize,
338}
339impl BsplineBasis {
340    /// Create a new B-spline basis.
341    pub fn new(degree: usize, knot_vector: KnotVector, n_ctrl: usize) -> Self {
342        Self {
343            degree,
344            knot_vector,
345            n_ctrl,
346        }
347    }
348    /// Create a basis with a uniform clamped knot vector.
349    pub fn clamped(degree: usize, n_ctrl: usize) -> Self {
350        let kv = KnotVector::clamped_uniform(n_ctrl, degree);
351        Self {
352            degree,
353            knot_vector: kv,
354            n_ctrl,
355        }
356    }
357    /// Evaluate all non-zero basis functions N_{i-p,p}(t), …, N_{i,p}(t) at parameter t.
358    ///
359    /// Returns an array of length p+1. Uses the triangular table algorithm.
360    pub fn eval_nonzero(&self, t: f64) -> (usize, Vec<f64>) {
361        let p = self.degree;
362        let knots = &self.knot_vector.knots;
363        let span = self.knot_vector.find_span(t, p, self.n_ctrl);
364        let mut n = vec![0.0_f64; p + 1];
365        let mut left = vec![0.0_f64; p + 1];
366        let mut right = vec![0.0_f64; p + 1];
367        n[0] = 1.0;
368        for j in 1..=p {
369            left[j] = t - knots[span + 1 - j];
370            right[j] = knots[span + j] - t;
371            let mut saved = 0.0;
372            for r in 0..j {
373                let temp = n[r] / (right[r + 1] + left[j - r]);
374                n[r] = saved + right[r + 1] * temp;
375                saved = left[j - r] * temp;
376            }
377            n[j] = saved;
378        }
379        (span, n)
380    }
381    /// Evaluate all basis functions N_{i,p}(t) for i = 0, …, n_ctrl-1.
382    ///
383    /// Returns a vector of length n_ctrl.
384    pub fn eval_all(&self, t: f64) -> Vec<f64> {
385        let (span, nonzero) = self.eval_nonzero(t);
386        let p = self.degree;
387        let mut result = vec![0.0_f64; self.n_ctrl];
388        for k in 0..=p {
389            if span + k >= p && span + k - p < self.n_ctrl {
390                result[span + k - p] = nonzero[k];
391            }
392        }
393        result
394    }
395    /// Evaluate basis function N_{i,p}(t).
396    pub fn eval(&self, i: usize, t: f64) -> f64 {
397        let all = self.eval_all(t);
398        if i < all.len() { all[i] } else { 0.0 }
399    }
400    /// Evaluate all non-zero basis functions and their derivatives up to order `deriv`.
401    ///
402    /// Returns a 2D array `ders[k][j]` where k is the derivative order (0..=deriv)
403    /// and j is the basis function index (0..=p) relative to the span.
404    pub fn eval_nonzero_derivs(&self, t: f64, deriv: usize) -> (usize, Vec<Vec<f64>>) {
405        let p = self.degree;
406        let knots = &self.knot_vector.knots;
407        let span = self.knot_vector.find_span(t, p, self.n_ctrl);
408        let d = deriv.min(p);
409        let mut ndu = vec![vec![0.0_f64; p + 1]; p + 1];
410        let mut left = vec![0.0_f64; p + 1];
411        let mut right = vec![0.0_f64; p + 1];
412        ndu[0][0] = 1.0;
413        for j in 1..=p {
414            left[j] = t - knots[span + 1 - j];
415            right[j] = knots[span + j] - t;
416            let mut saved = 0.0;
417            for r in 0..j {
418                ndu[j][r] = right[r + 1] + left[j - r];
419                let temp = ndu[r][j - 1] / ndu[j][r];
420                ndu[r][j] = saved + right[r + 1] * temp;
421                saved = left[j - r] * temp;
422            }
423            ndu[j][j] = saved;
424        }
425        let mut ders = vec![vec![0.0_f64; p + 1]; d + 1];
426        for j in 0..=p {
427            ders[0][j] = ndu[j][p];
428        }
429        let mut a = vec![vec![0.0_f64; p + 1]; 2];
430        for r in 0..=p {
431            let mut s1 = 0_usize;
432            let mut s2 = 1_usize;
433            a[0][0] = 1.0;
434            for k in 1..=d {
435                let mut nd = 0.0;
436                let rk = r as isize - k as isize;
437                let pk = p as isize - k as isize;
438                if r >= k {
439                    a[s2][0] = a[s1][0] / ndu[pk as usize + 1][rk as usize];
440                    nd = a[s2][0] * ndu[rk as usize][pk as usize];
441                }
442                let j1 = if rk >= -1 {
443                    1_usize
444                } else {
445                    (-(rk + 1)) as usize
446                };
447                let j2 = if (r as isize - 1) <= pk { k - 1 } else { p - r };
448                for j in j1..=j2 {
449                    let idx2 = rk + j as isize;
450                    if idx2 < 0 {
451                        continue;
452                    }
453                    let idx2 = idx2 as usize;
454                    a[s2][j] = (a[s1][j] - a[s1][j.saturating_sub(1)]) / ndu[pk as usize + 1][idx2];
455                    nd += a[s2][j] * ndu[idx2][pk as usize];
456                }
457                if r <= (p - k) {
458                    a[s2][k] = -a[s1][k - 1] / ndu[pk as usize + 1][r];
459                    nd += a[s2][k] * ndu[r][pk as usize];
460                }
461                ders[k][r] = nd;
462                std::mem::swap(&mut s1, &mut s2);
463            }
464        }
465        let mut rfact = p as f64;
466        for (k, ders_row) in ders[1..=d].iter_mut().enumerate().map(|(i, v)| (i + 1, v)) {
467            for val in ders_row[..=p].iter_mut() {
468                *val *= rfact;
469            }
470            if k < d {
471                rfact *= (p - k) as f64;
472            }
473        }
474        (span, ders)
475    }
476    /// Evaluate the k-th derivative of basis function N_{i,p}(t).
477    pub fn eval_deriv(&self, i: usize, t: f64, k: usize) -> f64 {
478        let p = self.degree;
479        let (span, ders) = self.eval_nonzero_derivs(t, k);
480        let local_idx = i as isize - span as isize;
481        if local_idx < 0 || local_idx > p as isize {
482            0.0
483        } else {
484            ders[k][local_idx as usize]
485        }
486    }
487    /// Greville abscissae (average knots) for control point parameterization.
488    ///
489    /// ξ_i = (t_{i+1} + … + t_{i+p}) / p
490    pub fn greville_abscissae(&self) -> Vec<f64> {
491        let p = self.degree;
492        let knots = &self.knot_vector.knots;
493        (0..self.n_ctrl)
494            .map(|i| {
495                if p == 0 {
496                    knots[i]
497                } else {
498                    knots[i + 1..i + p + 1].iter().sum::<f64>() / p as f64
499                }
500            })
501            .collect()
502    }
503}
504/// B-spline curve fitting to a set of 3D points.
505///
506/// Supports:
507/// - Chord-length parameterization
508/// - Centripetal parameterization
509/// - Knot selection by averaging
510/// - Least-squares fitting
511#[derive(Debug, Clone)]
512pub struct BsplineFitting {
513    /// Target degree.
514    pub degree: usize,
515    /// Number of control points.
516    pub n_ctrl: usize,
517    /// Fitted B-spline curve (after `fit()`).
518    pub curve: Option<BsplineCurve>,
519}
520impl BsplineFitting {
521    /// Create a new B-spline fitter.
522    pub fn new(degree: usize, n_ctrl: usize) -> Self {
523        Self {
524            degree,
525            n_ctrl,
526            curve: None,
527        }
528    }
529    /// Compute chord-length parameterization for a sequence of points.
530    ///
531    /// t_0 = 0, t_k = t_{k-1} + |P_k − P_{k-1}| / total_chord_length, t_n = 1.
532    pub fn chord_length_parameterization(points: &[[f64; 3]]) -> Vec<f64> {
533        let n = points.len();
534        if n == 0 {
535            return vec![];
536        }
537        if n == 1 {
538            return vec![0.0];
539        }
540        let mut d = vec![0.0_f64; n];
541        let mut total = 0.0_f64;
542        for i in 1..n {
543            let dx = points[i][0] - points[i - 1][0];
544            let dy = points[i][1] - points[i - 1][1];
545            let dz = points[i][2] - points[i - 1][2];
546            d[i] = (dx * dx + dy * dy + dz * dz).sqrt();
547            total += d[i];
548        }
549        if total < 1e-14 {
550            return (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
551        }
552        let mut params = vec![0.0_f64; n];
553        for i in 1..n - 1 {
554            params[i] = params[i - 1] + d[i] / total;
555        }
556        params[n - 1] = 1.0;
557        params
558    }
559    /// Compute centripetal parameterization (square root of chord length).
560    pub fn centripetal_parameterization(points: &[[f64; 3]]) -> Vec<f64> {
561        let n = points.len();
562        if n == 0 {
563            return vec![];
564        }
565        if n == 1 {
566            return vec![0.0];
567        }
568        let mut d = vec![0.0_f64; n];
569        let mut total = 0.0_f64;
570        for i in 1..n {
571            let dx = points[i][0] - points[i - 1][0];
572            let dy = points[i][1] - points[i - 1][1];
573            let dz = points[i][2] - points[i - 1][2];
574            d[i] = (dx * dx + dy * dy + dz * dz).sqrt().sqrt();
575            total += d[i];
576        }
577        if total < 1e-14 {
578            return (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
579        }
580        let mut params = vec![0.0_f64; n];
581        for i in 1..n - 1 {
582            params[i] = params[i - 1] + d[i] / total;
583        }
584        params[n - 1] = 1.0;
585        params
586    }
587    /// Select interior knots by averaging (Piegl-Tiller method).
588    ///
589    /// Produces n_ctrl − p − 1 interior knots.
590    pub fn select_knots_by_averaging(params: &[f64], n_ctrl: usize, degree: usize) -> KnotVector {
591        let n = params.len();
592        let p = degree;
593        let m = n_ctrl + p;
594        let mut knots = vec![0.0_f64; m + 1];
595        for val in knots[..=p].iter_mut() {
596            *val = 0.0;
597        }
598        for val in knots[(m - p)..=m].iter_mut() {
599            *val = 1.0;
600        }
601        let n_interior = n_ctrl - p - 1;
602        if n_interior > 0 {
603            let d = n as f64 / n_ctrl as f64;
604            for j in 1..=n_interior {
605                let i_float = j as f64 * d;
606                let i_floor = i_float as usize;
607                let alpha = i_float - i_floor as f64;
608                let t_avg = if i_floor + 1 < n {
609                    params[i_floor] * (1.0 - alpha) + params[i_floor + 1] * alpha
610                } else {
611                    params[n - 1]
612                };
613                knots[p + j] = t_avg.clamp(0.0, 1.0);
614            }
615        }
616        for i in 1..knots.len() {
617            if knots[i] < knots[i - 1] {
618                knots[i] = knots[i - 1];
619            }
620        }
621        KnotVector::new(knots)
622    }
623    /// Perform least-squares B-spline fitting to a set of points.
624    ///
625    /// Uses chord-length parameterization and averaging knot selection.
626    /// Solves the normal equations Q^T Q P = Q^T D.
627    pub fn fit(&mut self, points: &[[f64; 3]]) {
628        let n_pts = points.len();
629        let n_ctrl = self.n_ctrl;
630        let p = self.degree;
631        if n_pts < 2 || n_ctrl < p + 1 {
632            self.curve = Some(BsplineCurve::clamped(
633                1,
634                vec![points[0], *points.last().unwrap_or(&points[0])],
635            ));
636            return;
637        }
638        let params = Self::chord_length_parameterization(points);
639        let kv = Self::select_knots_by_averaging(&params, n_ctrl, p);
640        let basis = BsplineBasis::new(p, kv.clone(), n_ctrl);
641        if n_pts == n_ctrl {
642            let mut a = vec![vec![0.0_f64; n_ctrl]; n_ctrl];
643            for (row, &t) in params.iter().enumerate() {
644                let row_vals = basis.eval_all(t);
645                a[row][..n_ctrl].copy_from_slice(&row_vals[..n_ctrl]);
646            }
647            let ctrl_pts: Vec<[f64; 3]> = (0..n_ctrl)
648                .map(|i| solve_linear_system_row(&a, points, i, n_ctrl))
649                .collect();
650            self.curve = Some(BsplineCurve::new(p, kv, ctrl_pts));
651        } else {
652            let mut n_mat = vec![vec![0.0_f64; n_ctrl]; n_pts];
653            for (row, &t) in params.iter().enumerate() {
654                let row_vals = basis.eval_all(t);
655                n_mat[row][..n_ctrl].copy_from_slice(&row_vals[..n_ctrl]);
656            }
657            let mut ata = vec![vec![0.0_f64; n_ctrl]; n_ctrl];
658            let mut atd = vec![[0.0_f64; 3]; n_ctrl];
659            for row in 0..n_pts {
660                for col_j in 0..n_ctrl {
661                    for col_k in 0..n_ctrl {
662                        ata[col_j][col_k] += n_mat[row][col_j] * n_mat[row][col_k];
663                    }
664                    for d in 0..3 {
665                        atd[col_j][d] += n_mat[row][col_j] * points[row][d];
666                    }
667                }
668            }
669            let ctrl_pts: Vec<[f64; 3]> = solve_3x_systems(&ata, &atd, n_ctrl);
670            self.curve = Some(BsplineCurve::new(p, kv, ctrl_pts));
671        }
672    }
673    /// Return the fitted residual (sum of squared distances from points to curve).
674    pub fn residual(&self, points: &[[f64; 3]]) -> f64 {
675        let curve = match &self.curve {
676            Some(c) => c,
677            None => return f64::INFINITY,
678        };
679        let params = Self::chord_length_parameterization(points);
680        params
681            .iter()
682            .zip(points.iter())
683            .map(|(&t, &p)| {
684                let c = curve.eval(t);
685                let dx = c[0] - p[0];
686                let dy = c[1] - p[1];
687                let dz = c[2] - p[2];
688                dx * dx + dy * dy + dz * dz
689            })
690            .sum()
691    }
692}
693/// A B-spline curve in 3D space.
694///
695/// C(t) = Σ_{i=0}^{n} N_{i,p}(t) * P_i
696///
697/// where P_i are control points and N_{i,p}(t) are B-spline basis functions.
698#[derive(Debug, Clone)]
699pub struct BsplineCurve {
700    /// B-spline basis.
701    pub basis: BsplineBasis,
702    /// Control points \[P_0, …, P_n\], each \[x, y, z\].
703    pub control_points: Vec<[f64; 3]>,
704}
705impl BsplineCurve {
706    /// Create a new B-spline curve.
707    pub fn new(degree: usize, knot_vector: KnotVector, control_points: Vec<[f64; 3]>) -> Self {
708        let n_ctrl = control_points.len();
709        let basis = BsplineBasis::new(degree, knot_vector, n_ctrl);
710        Self {
711            basis,
712            control_points,
713        }
714    }
715    /// Create a B-spline curve with a clamped uniform knot vector.
716    pub fn clamped(degree: usize, control_points: Vec<[f64; 3]>) -> Self {
717        let n_ctrl = control_points.len();
718        let basis = BsplineBasis::clamped(degree, n_ctrl);
719        Self {
720            basis,
721            control_points,
722        }
723    }
724    /// Evaluate the curve at parameter t: returns \[x, y, z\].
725    pub fn eval(&self, t: f64) -> [f64; 3] {
726        let (span, nonzero) = self.basis.eval_nonzero(t);
727        let p = self.basis.degree;
728        let mut point = [0.0_f64; 3];
729        for (k, &n_val) in nonzero[..=p].iter().enumerate() {
730            let idx = span + k - p;
731            if idx < self.control_points.len() {
732                let cp = self.control_points[idx];
733                for (pt, &cp_d) in point.iter_mut().zip(cp.iter()) {
734                    *pt += n_val * cp_d;
735                }
736            }
737        }
738        point
739    }
740    /// Evaluate the k-th derivative of the curve at parameter t.
741    ///
742    /// Returns \[dx^k/dt^k, dy^k/dt^k, dz^k/dt^k\].
743    pub fn eval_deriv(&self, t: f64, k: usize) -> [f64; 3] {
744        let p = self.basis.degree;
745        let (span, ders) = self.basis.eval_nonzero_derivs(t, k);
746        let n_ctrl = self.control_points.len();
747        let mut result = [0.0_f64; 3];
748        for (j, &d_val) in ders[k][..=p].iter().enumerate() {
749            let idx = span + j - p;
750            if idx < n_ctrl {
751                let cp = self.control_points[idx];
752                for (res_d, &cp_d) in result.iter_mut().zip(cp.iter()) {
753                    *res_d += d_val * cp_d;
754                }
755            }
756        }
757        result
758    }
759    /// Compute arc length from t_start to t_end using Gaussian quadrature (n_gauss points per interval).
760    pub fn arc_length(&self, t_start: f64, t_end: f64, n_intervals: usize) -> f64 {
761        let h = (t_end - t_start) / n_intervals as f64;
762        let gp = [-0.906180, -0.538469, 0.0, 0.538469, 0.906180];
763        let gw = [0.236927, 0.478629, 0.568889, 0.478629, 0.236927];
764        let mut length = 0.0;
765        for i in 0..n_intervals {
766            let a = t_start + i as f64 * h;
767            let b = a + h;
768            let mid = (a + b) * 0.5;
769            let half = (b - a) * 0.5;
770            for (&xi, &wi) in gp.iter().zip(gw.iter()) {
771                let t = mid + half * xi;
772                let dt = self.eval_deriv(t, 1);
773                let speed = (dt[0] * dt[0] + dt[1] * dt[1] + dt[2] * dt[2]).sqrt();
774                length += wi * half * speed;
775            }
776        }
777        length
778    }
779    /// Compute the unit tangent vector T(t) = C'(t)/|C'(t)|.
780    pub fn tangent(&self, t: f64) -> [f64; 3] {
781        let d1 = self.eval_deriv(t, 1);
782        let mag = (d1[0] * d1[0] + d1[1] * d1[1] + d1[2] * d1[2]).sqrt();
783        if mag < 1e-14 {
784            [0.0, 0.0, 0.0]
785        } else {
786            [d1[0] / mag, d1[1] / mag, d1[2] / mag]
787        }
788    }
789    /// Compute curvature κ(t) = |C' × C''| / |C'|³.
790    pub fn curvature(&self, t: f64) -> f64 {
791        let d1 = self.eval_deriv(t, 1);
792        let d2 = self.eval_deriv(t, 2);
793        let cross = [
794            d1[1] * d2[2] - d1[2] * d2[1],
795            d1[2] * d2[0] - d1[0] * d2[2],
796            d1[0] * d2[1] - d1[1] * d2[0],
797        ];
798        let cross_mag = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
799        let d1_mag = (d1[0] * d1[0] + d1[1] * d1[1] + d1[2] * d1[2]).sqrt();
800        if d1_mag < 1e-14 {
801            0.0
802        } else {
803            cross_mag / d1_mag.powi(3)
804        }
805    }
806    /// Compute torsion τ(t) = (C' × C'')·C''' / |C' × C''|².
807    pub fn torsion(&self, t: f64) -> f64 {
808        let d1 = self.eval_deriv(t, 1);
809        let d2 = self.eval_deriv(t, 2);
810        let d3 = self.eval_deriv(t, 3);
811        let cross = [
812            d1[1] * d2[2] - d1[2] * d2[1],
813            d1[2] * d2[0] - d1[0] * d2[2],
814            d1[0] * d2[1] - d1[1] * d2[0],
815        ];
816        let cross_mag2 = cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2];
817        if cross_mag2 < 1e-28 {
818            return 0.0;
819        }
820        let dot_d3 = cross[0] * d3[0] + cross[1] * d3[1] + cross[2] * d3[2];
821        dot_d3 / cross_mag2
822    }
823    /// Compute the principal normal vector N(t) = T'(t)/|T'(t)|.
824    pub fn principal_normal(&self, t: f64) -> [f64; 3] {
825        let kappa = self.curvature(t);
826        if kappa < 1e-14 {
827            return [0.0, 0.0, 0.0];
828        }
829        let d1 = self.eval_deriv(t, 1);
830        let d2 = self.eval_deriv(t, 2);
831        let d1_mag = (d1[0] * d1[0] + d1[1] * d1[1] + d1[2] * d1[2]).sqrt();
832        let d1_mag3 = d1_mag.powi(3);
833        let d1_dot_d2 = d1[0] * d2[0] + d1[1] * d2[1] + d1[2] * d2[2];
834        let d1_mag2 = d1_mag * d1_mag;
835        let mut normal = [0.0_f64; 3];
836        for i in 0..3 {
837            normal[i] = (d2[i] * d1_mag2 - d1[i] * d1_dot_d2) / (kappa * d1_mag3);
838        }
839        normal
840    }
841    /// Evaluate the curve at n_points uniformly spaced parameter values.
842    ///
843    /// Returns a vector of \[x, y, z\] points.
844    pub fn sample(&self, n_points: usize) -> Vec<[f64; 3]> {
845        let (t0, t1) = self.basis.knot_vector.domain();
846        (0..n_points)
847            .map(|i| {
848                let t = t0 + (t1 - t0) * i as f64 / (n_points - 1).max(1) as f64;
849                self.eval(t)
850            })
851            .collect()
852    }
853    /// Compute the bounding box of the curve (sampled at n_samples points).
854    ///
855    /// Returns (\[x_min, y_min, z_min\], \[x_max, y_max, z_max\]).
856    pub fn bounding_box(&self, n_samples: usize) -> ([f64; 3], [f64; 3]) {
857        let pts = self.sample(n_samples);
858        let mut lo = pts[0];
859        let mut hi = pts[0];
860        for p in &pts {
861            for d in 0..3 {
862                lo[d] = lo[d].min(p[d]);
863                hi[d] = hi[d].max(p[d]);
864            }
865        }
866        (lo, hi)
867    }
868}
869/// B-spline knot vector with associated utilities.
870///
871/// A knot vector is a non-decreasing sequence of real numbers
872/// T = \[t_0, t_1, …, t_{n+p+1}\] where n+1 is the number of basis functions
873/// and p is the polynomial degree.
874#[derive(Debug, Clone)]
875pub struct KnotVector {
876    /// Knot values (non-decreasing).
877    pub knots: Vec<f64>,
878}
879impl KnotVector {
880    /// Construct a knot vector from a slice.
881    ///
882    /// # Panics
883    ///
884    /// Panics if the knots are not non-decreasing.
885    pub fn new(knots: Vec<f64>) -> Self {
886        for i in 1..knots.len() {
887            assert!(
888                knots[i] >= knots[i - 1],
889                "knot vector must be non-decreasing"
890            );
891        }
892        Self { knots }
893    }
894    /// Construct a uniform (clamped) knot vector for n+1 control points and degree p.
895    ///
896    /// The first and last knots are repeated p+1 times (clamped), and the interior
897    /// knots are uniformly spaced.
898    pub fn clamped_uniform(n_ctrl: usize, degree: usize) -> Self {
899        let n = n_ctrl - 1;
900        let p = degree;
901        let m = n + p + 1;
902        let mut knots = vec![0.0_f64; m + 1];
903        for val in knots[..=p].iter_mut() {
904            *val = 0.0;
905        }
906        for val in knots[(m - p)..=m].iter_mut() {
907            *val = 1.0;
908        }
909        let n_interior = m - 2 * p - 1;
910        for j in 1..=n_interior {
911            knots[p + j] = j as f64 / (n_interior + 1) as f64;
912        }
913        Self { knots }
914    }
915    /// Find the knot span index i such that t ∈ \[t_i, t_{i+1}).
916    ///
917    /// Uses binary search. For t at the end of the domain, returns the last
918    /// non-zero-length span.
919    pub fn find_span(&self, t: f64, degree: usize, n_ctrl: usize) -> usize {
920        let n = n_ctrl - 1;
921        let p = degree;
922        let knots = &self.knots;
923        if t >= knots[n + 1] {
924            let mut i = n;
925            while i > p && (knots[i] - knots[i + 1]).abs() < 1e-14 {
926                i -= 1;
927            }
928            return i;
929        }
930        if t <= knots[p] {
931            return p;
932        }
933        let mut lo = p;
934        let mut hi = n + 1;
935        let mut mid = (lo + hi) / 2;
936        while t < knots[mid] || t >= knots[mid + 1] {
937            if t < knots[mid] {
938                hi = mid;
939            } else {
940                lo = mid;
941            }
942            mid = (lo + hi) / 2;
943        }
944        mid
945    }
946    /// Number of knots in the vector.
947    pub fn len(&self) -> usize {
948        self.knots.len()
949    }
950    /// Whether the knot vector is empty.
951    pub fn is_empty(&self) -> bool {
952        self.knots.is_empty()
953    }
954    /// Return the domain \[a, b\] of the knot vector (a = first knot, b = last knot).
955    pub fn domain(&self) -> (f64, f64) {
956        (
957            *self.knots.first().unwrap_or(&0.0),
958            *self.knots.last().unwrap_or(&1.0),
959        )
960    }
961}