Skip to main content

oxiphysics_geometry/spline_geometry/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::f64::consts::PI;
6
7use super::types::{BSplineCurve, BezierCurve};
8
9/// Add two 3-vectors.
10#[inline]
11pub fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
12    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
13}
14/// Subtract two 3-vectors.
15#[inline]
16pub fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
18}
19/// Scale a 3-vector by a scalar.
20#[inline]
21pub fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
22    [a[0] * s, a[1] * s, a[2] * s]
23}
24/// Dot product of two 3-vectors.
25#[inline]
26pub fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
27    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
28}
29/// Cross product of two 3-vectors.
30#[inline]
31pub fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
32    [
33        a[1] * b[2] - a[2] * b[1],
34        a[2] * b[0] - a[0] * b[2],
35        a[0] * b[1] - a[1] * b[0],
36    ]
37}
38/// Euclidean norm of a 3-vector.
39#[inline]
40pub fn vec3_norm(a: [f64; 3]) -> f64 {
41    (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
42}
43/// Normalize a 3-vector (returns zero vector for degenerate input).
44#[inline]
45pub fn vec3_normalize(a: [f64; 3]) -> [f64; 3] {
46    let n = vec3_norm(a);
47    if n < 1e-300 {
48        [0.0, 0.0, 0.0]
49    } else {
50        vec3_scale(a, 1.0 / n)
51    }
52}
53/// Linear interpolation between two 3-vectors.
54#[inline]
55pub fn vec3_lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
56    vec3_add(vec3_scale(a, 1.0 - t), vec3_scale(b, t))
57}
58/// Evaluate all non-zero B-spline basis functions of degree `p` at parameter `t`
59/// using the Cox–de Boor recurrence.
60///
61/// `knots` is the full knot vector. `span` is the knot span index such that
62/// `knots[span] <= t < knots[span+1]`.
63///
64/// Returns an array of `p+1` values: `N[0..=p]` corresponding to basis functions
65/// `N_{span-p,p}` through `N_{span,p}`.
66pub fn bspline_basis(span: usize, p: usize, t: f64, knots: &[f64]) -> Vec<f64> {
67    let mut n = vec![0.0f64; p + 1];
68    let mut left = vec![0.0f64; p + 1];
69    let mut right = vec![0.0f64; p + 1];
70    n[0] = 1.0;
71    for j in 1..=p {
72        left[j] = t - knots[span + 1 - j];
73        right[j] = knots[span + j] - t;
74        let mut saved = 0.0f64;
75        for r in 0..j {
76            let temp = n[r] / (right[r + 1] + left[j - r]);
77            n[r] = saved + right[r + 1] * temp;
78            saved = left[j - r] * temp;
79        }
80        n[j] = saved;
81    }
82    n
83}
84/// Find the knot span index for parameter `t` in knot vector `knots` with `n+1` control points
85/// and degree `p`, where `n = num_ctrl - 1`.
86pub fn find_knot_span(num_ctrl: usize, p: usize, t: f64, knots: &[f64]) -> usize {
87    let n = num_ctrl - 1;
88    if t >= knots[n + 1] {
89        return n;
90    }
91    if t <= knots[p] {
92        return p;
93    }
94    let mut low = p;
95    let mut high = n + 1;
96    let mut mid = (low + high) / 2;
97    while t < knots[mid] || t >= knots[mid + 1] {
98        if t < knots[mid] {
99            high = mid;
100        } else {
101            low = mid;
102        }
103        mid = (low + high) / 2;
104    }
105    mid
106}
107/// Generate a uniform clamped knot vector for `n+1` control points of degree `p`.
108///
109/// The result has `n + p + 2` knots: `p+1` zeros, `n-p` interior knots, `p+1` ones.
110pub fn uniform_clamped_knots(num_ctrl: usize, p: usize) -> Vec<f64> {
111    let n = num_ctrl - 1;
112    let m = n + p + 1;
113    let mut knots = vec![0.0f64; m + 1];
114    for (i, k) in knots.iter_mut().enumerate().take(m + 1) {
115        if i <= p {
116            *k = 0.0;
117        } else if i > n {
118            *k = 1.0;
119        } else {
120            *k = (i - p) as f64 / (n - p + 1) as f64;
121        }
122    }
123    knots
124}
125/// Fit a B-spline curve to a set of data points using least squares.
126///
127/// Uses a uniform clamped knot vector and solves the normal equations
128/// `A^T A c = A^T y` for each coordinate independently via Cholesky-like
129/// tridiagonal-band decomposition approximation (here: full dense solver for simplicity).
130pub fn fit_bspline_least_squares(
131    data: &[[f64; 3]],
132    num_ctrl: usize,
133    degree: usize,
134) -> BSplineCurve {
135    let m = data.len();
136    assert!(m >= num_ctrl, "need at least num_ctrl data points");
137    let knots = uniform_clamped_knots(num_ctrl, degree);
138    let mut params = vec![0.0f64; m];
139    for i in 1..m {
140        params[i] = params[i - 1] + vec3_norm(vec3_sub(data[i], data[i - 1]));
141    }
142    let total = params[m - 1];
143    if total > 0.0 {
144        for p in params.iter_mut() {
145            *p /= total;
146        }
147    }
148    let mut a_mat = vec![vec![0.0f64; num_ctrl]; m];
149    for (row, &t) in params.iter().enumerate() {
150        let span = find_knot_span(num_ctrl, degree, t, &knots);
151        let basis = bspline_basis(span, degree, t, &knots);
152        for j in 0..=degree {
153            a_mat[row][span - degree + j] = basis[j];
154        }
155    }
156    let mut ata = vec![vec![0.0f64; num_ctrl]; num_ctrl];
157    let mut atb = vec![[0.0f64; 3]; num_ctrl];
158    for i in 0..m {
159        for j in 0..num_ctrl {
160            for k in 0..num_ctrl {
161                ata[j][k] += a_mat[i][j] * a_mat[i][k];
162            }
163            for d in 0..3 {
164                atb[j][d] += a_mat[i][j] * data[i][d];
165            }
166        }
167    }
168    let n = num_ctrl;
169    let mut aug = vec![vec![0.0f64; n + 3]; n];
170    for i in 0..n {
171        for j in 0..n {
172            aug[i][j] = ata[i][j];
173        }
174        for d in 0..3 {
175            aug[i][n + d] = atb[i][d];
176        }
177    }
178    for col in 0..n {
179        let mut max_row = col;
180        let mut max_val = aug[col][col].abs();
181        for (row, _) in aug.iter().enumerate().take(n).skip(col + 1) {
182            if aug[row][col].abs() > max_val {
183                max_val = aug[row][col].abs();
184                max_row = row;
185            }
186        }
187        aug.swap(col, max_row);
188        let pivot = aug[col][col];
189        if pivot.abs() < 1e-12 {
190            continue;
191        }
192        for elem in aug[col].iter_mut().skip(col) {
193            *elem /= pivot;
194        }
195        for row in 0..n {
196            if row != col {
197                let factor = aug[row][col];
198                let col_row: Vec<f64> = aug[col][col..].to_vec();
199                for (j_off, elem) in aug[row][col..].iter_mut().enumerate() {
200                    *elem -= factor * col_row[j_off];
201                }
202            }
203        }
204    }
205    let ctrl: Vec<[f64; 3]> = (0..n)
206        .map(|i| [aug[i][n], aug[i][n + 1], aug[i][n + 2]])
207        .collect();
208    BSplineCurve::new(ctrl, degree, knots)
209}
210/// Generate a surface of revolution by rotating a profile curve around the Z-axis.
211///
212/// The profile is a set of 2D points `(r, z)` in the meridian plane.
213/// Returns a grid of 3D points `[x, y, z]` on the surface.
214pub fn surface_of_revolution(
215    profile: &[[f64; 2]],
216    num_angular_samples: usize,
217) -> Vec<Vec<[f64; 3]>> {
218    let m = profile.len();
219    let n = num_angular_samples;
220    let mut grid = vec![vec![[0.0f64; 3]; n]; m];
221    for (i, &[r, z]) in profile.iter().enumerate() {
222        for (j, cell) in grid[i].iter_mut().enumerate() {
223            let theta = 2.0 * PI * j as f64 / n as f64;
224            *cell = [r * theta.cos(), r * theta.sin(), z];
225        }
226    }
227    grid
228}
229/// Generate a lofted surface by interpolating between a set of cross-section curves.
230///
231/// Each cross-section must have the same number of points.
232pub fn lofted_surface(sections: &[Vec<[f64; 3]>], num_v_samples: usize) -> Vec<Vec<[f64; 3]>> {
233    let ns = sections.len();
234    assert!(ns >= 2, "need at least 2 sections");
235    let np = sections[0].len();
236    for s in sections.iter() {
237        assert_eq!(s.len(), np, "all sections must have equal point count");
238    }
239    let mut grid = vec![vec![[0.0f64; 3]; np]; num_v_samples];
240    for (vi, row) in grid.iter_mut().enumerate() {
241        let t = vi as f64 / (num_v_samples - 1).max(1) as f64;
242        let seg = ((t * (ns - 1) as f64).min((ns - 2) as f64)) as usize;
243        let local_t = t * (ns - 1) as f64 - seg as f64;
244        for (pi, cell) in row.iter_mut().enumerate() {
245            *cell = vec3_lerp(sections[seg][pi], sections[seg + 1][pi], local_t);
246        }
247    }
248    grid
249}
250/// Compute the curvature of a parametric curve at parameter `t`.
251///
252/// Curvature κ = |r' × r''| / |r'|³.
253pub fn curve_curvature(curve: &BezierCurve, t: f64) -> f64 {
254    let r_prime = curve.tangent(t);
255    let r_double = curve.second_derivative(t);
256    let cross = vec3_cross(r_prime, r_double);
257    let cross_norm = vec3_norm(cross);
258    let prime_norm = vec3_norm(r_prime);
259    if prime_norm < 1e-12 {
260        0.0
261    } else {
262        cross_norm / (prime_norm * prime_norm * prime_norm)
263    }
264}
265/// Compute the torsion of a parametric curve using finite differences.
266///
267/// Torsion τ = (r' × r'') · r''' / |r' × r''|².
268/// Uses centered finite differences with step `h` to estimate `r'''`.
269pub fn curve_torsion(curve: &BezierCurve, t: f64, h: f64) -> f64 {
270    let r_prime = curve.tangent(t);
271    let r_double = curve.second_derivative(t);
272    let r_double_plus = {
273        let tp = (t + h).min(1.0);
274        curve.second_derivative(tp)
275    };
276    let r_double_minus = {
277        let tm = (t - h).max(0.0);
278        curve.second_derivative(tm)
279    };
280    let r_triple = vec3_scale(vec3_sub(r_double_plus, r_double_minus), 1.0 / (2.0 * h));
281    let cross = vec3_cross(r_prime, r_double);
282    let denom = vec3_dot(cross, cross);
283    if denom < 1e-20 {
284        0.0
285    } else {
286        vec3_dot(cross, r_triple) / denom
287    }
288}
289/// Compute the Frenet-Serret frame `(T, N, B)` at parameter `t`.
290///
291/// Returns `(tangent, normal, binormal)` unit vectors.
292pub fn frenet_serret_frame(curve: &BezierCurve, t: f64) -> ([f64; 3], [f64; 3], [f64; 3]) {
293    let t_vec = vec3_normalize(curve.tangent(t));
294    let r_double = curve.second_derivative(t);
295    let n_raw = vec3_sub(r_double, vec3_scale(t_vec, vec3_dot(r_double, t_vec)));
296    let n_vec = vec3_normalize(n_raw);
297    let b_vec = vec3_cross(t_vec, n_vec);
298    (t_vec, n_vec, b_vec)
299}
300/// Compute the osculating circle radius (= 1/κ) at parameter `t`.
301pub fn osculating_radius(curve: &BezierCurve, t: f64) -> f64 {
302    let kappa = curve_curvature(curve, t);
303    if kappa < 1e-12 {
304        f64::INFINITY
305    } else {
306        1.0 / kappa
307    }
308}
309/// Compute chord-length parameters for a sequence of 3D points.
310///
311/// Returns normalized parameter values in `[0,1]`.
312pub fn chord_length_params(points: &[[f64; 3]]) -> Vec<f64> {
313    let n = points.len();
314    if n == 0 {
315        return vec![];
316    }
317    let mut params = vec![0.0f64; n];
318    for i in 1..n {
319        params[i] = params[i - 1] + vec3_norm(vec3_sub(points[i], points[i - 1]));
320    }
321    let total = params[n - 1];
322    if total > 0.0 {
323        for p in params.iter_mut() {
324            *p /= total;
325        }
326    }
327    params
328}
329/// Insert a knot `t_new` into a B-spline curve at the given span, returning the new curve.
330pub fn knot_insert(curve: &BSplineCurve, t_new: f64) -> BSplineCurve {
331    let n = curve.control_points.len();
332    let p = curve.degree;
333    let knots = &curve.knots;
334    let span = find_knot_span(n, p, t_new, knots);
335    let mut new_knots = Vec::with_capacity(knots.len() + 1);
336    new_knots.extend_from_slice(&knots[..span + 1]);
337    new_knots.push(t_new);
338    new_knots.extend_from_slice(&knots[span + 1..]);
339    let mut new_cp = Vec::with_capacity(n + 1);
340    for i in 0..=span - p {
341        new_cp.push(curve.control_points[i]);
342    }
343    for i in span - p + 1..=span {
344        let alpha = (t_new - knots[i]) / (knots[i + p] - knots[i]);
345        let prev = curve.control_points[i - 1];
346        let curr = curve.control_points[i];
347        new_cp.push(vec3_add(
348            vec3_scale(prev, 1.0 - alpha),
349            vec3_scale(curr, alpha),
350        ));
351    }
352    for i in span..n {
353        new_cp.push(curve.control_points[i]);
354    }
355    BSplineCurve::new(new_cp, p, new_knots)
356}
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::spline_geometry::BSplineSurface;
361    use crate::spline_geometry::BezierPatch;
362    use crate::spline_geometry::BlendingSurface;
363    use crate::spline_geometry::ContinuityOrder;
364    use crate::spline_geometry::CubicSpline;
365    use crate::spline_geometry::NurbsCurve;
366    use crate::spline_geometry::NurbsSurface;
367    use crate::spline_geometry::PeriodicBSpline;
368    use crate::spline_geometry::SweptSurface;
369    pub(super) const EPS: f64 = 1e-9;
370    #[test]
371    fn test_bspline_basis_partition_of_unity() {
372        let ctrl = [
373            [0.0, 0.0, 0.0],
374            [1.0, 0.0, 0.0],
375            [2.0, 1.0, 0.0],
376            [3.0, 0.0, 0.0],
377            [4.0, 0.0, 0.0],
378        ];
379        let degree = 3;
380        let knots = uniform_clamped_knots(ctrl.len(), degree);
381        for i in 1..10 {
382            let t = i as f64 / 10.0;
383            let span = find_knot_span(ctrl.len(), degree, t, &knots);
384            let basis = bspline_basis(span, degree, t, &knots);
385            let sum: f64 = basis.iter().sum();
386            assert!((sum - 1.0).abs() < 1e-10, "basis sum = {}", sum);
387        }
388    }
389    #[test]
390    fn test_bspline_basis_non_negative() {
391        let ctrl = [
392            [0.0, 0.0, 0.0],
393            [1.0, 0.0, 0.0],
394            [2.0, 1.0, 0.0],
395            [3.0, 0.0, 0.0],
396        ];
397        let degree = 2;
398        let knots = uniform_clamped_knots(ctrl.len(), degree);
399        for i in 0..=20 {
400            let t = i as f64 / 20.0;
401            let t_clamped = t.min(1.0 - 1e-12);
402            let span = find_knot_span(ctrl.len(), degree, t_clamped, &knots);
403            let basis = bspline_basis(span, degree, t_clamped, &knots);
404            for &b in &basis {
405                assert!(b >= -1e-12, "negative basis value: {}", b);
406            }
407        }
408    }
409    #[test]
410    fn test_uniform_clamped_knots_endpoints() {
411        let knots = uniform_clamped_knots(5, 3);
412        for &k in knots.iter().take(4) {
413            assert_eq!(k, 0.0);
414        }
415        let n = knots.len();
416        for &k in knots.iter().skip(n - 4) {
417            assert_eq!(k, 1.0);
418        }
419    }
420    #[test]
421    fn test_find_knot_span_boundary() {
422        let knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
423        let span = find_knot_span(3, 2, 0.5, &knots);
424        assert_eq!(span, 2);
425    }
426    #[test]
427    fn test_bspline_curve_endpoints() {
428        let ctrl = vec![
429            [0.0, 0.0, 0.0],
430            [1.0, 2.0, 0.0],
431            [3.0, 2.0, 0.0],
432            [4.0, 0.0, 0.0],
433        ];
434        let curve = BSplineCurve::with_clamped_knots(ctrl.clone(), 3);
435        let p0 = curve.eval(0.0);
436        let p1 = curve.eval(1.0 - 1e-12);
437        assert!((p0[0] - ctrl[0][0]).abs() < 1e-6);
438        assert!((p1[0] - ctrl[3][0]).abs() < 1e-4, "p1.x = {}", p1[0]);
439    }
440    #[test]
441    fn test_bspline_curve_linear_reproduces_line() {
442        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
443        let curve = BSplineCurve::with_clamped_knots(ctrl, 1);
444        for i in 0..=10 {
445            let t = i as f64 / 10.0;
446            let p = curve.eval(t.min(1.0 - 1e-12));
447            assert!((p[0] - p[1]).abs() < 1e-10);
448        }
449    }
450    #[test]
451    fn test_bspline_curve_arc_length_positive() {
452        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
453        let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
454        let len = curve.arc_length(100);
455        assert!(len > 0.0);
456    }
457    #[test]
458    fn test_bspline_curve_derivative_direction() {
459        let ctrl = vec![
460            [0.0, 0.0, 0.0],
461            [1.0, 0.0, 0.0],
462            [2.0, 0.0, 0.0],
463            [3.0, 0.0, 0.0],
464        ];
465        let curve = BSplineCurve::with_clamped_knots(ctrl, 3);
466        let d = curve.derivative(0.5);
467        assert!(d[0] > 0.0, "tangent x = {}", d[0]);
468        assert!(d[1].abs() < 1e-6);
469    }
470    #[test]
471    fn test_bspline_curve_sample_count() {
472        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
473        let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
474        let pts = curve.sample(11);
475        assert_eq!(pts.len(), 11);
476    }
477    #[test]
478    fn test_nurbs_unit_weights_matches_bspline() {
479        let pts = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
480        let weights = vec![1.0, 1.0, 1.0];
481        let nurbs = NurbsCurve::from_points_and_weights(pts.clone(), weights, 2);
482        let bsp = BSplineCurve::with_clamped_knots(pts, 2);
483        for i in 1..10 {
484            let t = i as f64 / 10.0;
485            let pn = nurbs.eval(t);
486            let pb = bsp.eval(t);
487            for k in 0..3 {
488                assert!(
489                    (pn[k] - pb[k]).abs() < 1e-9,
490                    "mismatch at dim {}: {} vs {}",
491                    k,
492                    pn[k],
493                    pb[k]
494                );
495            }
496        }
497    }
498    #[test]
499    fn test_nurbs_circle_radius() {
500        let r = 3.0;
501        let circle = NurbsCurve::circle(r);
502        for i in 0..12 {
503            let t = i as f64 / 12.0;
504            let p = circle.eval(t);
505            let dist = (p[0] * p[0] + p[1] * p[1]).sqrt();
506            assert!(
507                (dist - r).abs() < 1e-6,
508                "circle radius error: {} vs {}",
509                dist,
510                r
511            );
512        }
513    }
514    #[test]
515    fn test_nurbs_sample_count() {
516        let pts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
517        let nurbs = NurbsCurve::from_points_and_weights(pts, vec![1.0, 1.0, 1.0], 2);
518        assert_eq!(nurbs.sample(7).len(), 7);
519    }
520    #[test]
521    fn test_bezier_endpoints() {
522        let ctrl = vec![
523            [0.0, 0.0, 0.0],
524            [1.0, 2.0, 0.0],
525            [3.0, 2.0, 0.0],
526            [4.0, 0.0, 0.0],
527        ];
528        let curve = BezierCurve::new(ctrl.clone());
529        let p0 = curve.eval(0.0);
530        let p1 = curve.eval(1.0);
531        for k in 0..3 {
532            assert!((p0[k] - ctrl[0][k]).abs() < EPS);
533            assert!((p1[k] - ctrl[3][k]).abs() < EPS);
534        }
535    }
536    #[test]
537    fn test_bezier_convex_hull_property() {
538        let ctrl = vec![
539            [0.0, 0.0, 0.0],
540            [1.0, 2.0, 0.0],
541            [3.0, 2.0, 0.0],
542            [4.0, 0.0, 0.0],
543        ];
544        let curve = BezierCurve::new(ctrl.clone());
545        let x_min = ctrl.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
546        let x_max = ctrl.iter().map(|p| p[0]).fold(f64::NEG_INFINITY, f64::max);
547        let y_min = ctrl.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min);
548        let y_max = ctrl.iter().map(|p| p[1]).fold(f64::NEG_INFINITY, f64::max);
549        for i in 0..=20 {
550            let t = i as f64 / 20.0;
551            let p = curve.eval(t);
552            assert!(p[0] >= x_min - 1e-9 && p[0] <= x_max + 1e-9);
553            assert!(p[1] >= y_min - 1e-9 && p[1] <= y_max + 1e-9);
554        }
555    }
556    #[test]
557    fn test_bezier_linear_is_interpolating() {
558        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0]];
559        let curve = BezierCurve::new(ctrl);
560        let mid = curve.eval(0.5);
561        assert!((mid[0] - 0.5).abs() < EPS);
562        assert!((mid[1] - 0.5).abs() < EPS);
563    }
564    #[test]
565    fn test_bezier_split_reconstructs() {
566        let ctrl = vec![
567            [0.0, 0.0, 0.0],
568            [1.0, 2.0, 0.0],
569            [3.0, 2.0, 0.0],
570            [4.0, 0.0, 0.0],
571        ];
572        let curve = BezierCurve::new(ctrl);
573        let (left, right) = curve.split(0.5);
574        let orig = BezierCurve::new(curve.control_points.clone()).eval(0.5);
575        let lp = left.eval(1.0);
576        let rp = right.eval(0.0);
577        for k in 0..3 {
578            assert!((orig[k] - lp[k]).abs() < 1e-9);
579            assert!((orig[k] - rp[k]).abs() < 1e-9);
580        }
581    }
582    #[test]
583    fn test_bezier_degree_elevate_same_shape() {
584        let ctrl = vec![[0.0, 0.0, 0.0], [2.0, 4.0, 0.0], [4.0, 0.0, 0.0]];
585        let curve = BezierCurve::new(ctrl);
586        let elevated = curve.degree_elevate();
587        assert_eq!(elevated.control_points.len(), 4);
588        for i in 0..=10 {
589            let t = i as f64 / 10.0;
590            let p_orig = curve.eval(t);
591            let p_elev = elevated.eval(t);
592            for k in 0..3 {
593                assert!(
594                    (p_orig[k] - p_elev[k]).abs() < 1e-9,
595                    "degree elevation changed shape at t={}: {} vs {}",
596                    t,
597                    p_orig[k],
598                    p_elev[k]
599                );
600            }
601        }
602    }
603    #[test]
604    fn test_bezier_arc_length_positive() {
605        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 1.0, 0.0], [2.0, 0.0, 0.0]];
606        let curve = BezierCurve::new(ctrl);
607        assert!(curve.arc_length(100) > 0.0);
608    }
609    #[test]
610    fn test_natural_spline_interpolates_data() {
611        let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
612        let y = vec![0.0, 1.0, 0.0, -1.0, 0.0];
613        let spline = CubicSpline::natural(x.clone(), y.clone());
614        for i in 0..x.len() {
615            let val = spline.eval(x[i]);
616            assert!(
617                (val - y[i]).abs() < 1e-8,
618                "spline({}) = {} vs {}",
619                x[i],
620                val,
621                y[i]
622            );
623        }
624    }
625    #[test]
626    fn test_clamped_spline_interpolates_data() {
627        let x = vec![0.0, 1.0, 2.0, 3.0];
628        let y = vec![0.0, 1.0, 4.0, 9.0];
629        let spline = CubicSpline::clamped(x.clone(), y.clone(), 1.0, 6.0);
630        for i in 0..x.len() {
631            let val = spline.eval(x[i]);
632            assert!((val - y[i]).abs() < 1e-8);
633        }
634    }
635    #[test]
636    fn test_natural_spline_second_deriv_at_endpoints_zero() {
637        let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
638        let y = vec![1.0, 2.0, 1.5, 3.0, 2.0];
639        let spline = CubicSpline::natural(x.clone(), y.clone());
640        let d2_left = spline.eval_second_derivative(x[0]);
641        let d2_right = spline.eval_second_derivative(*x.last().unwrap());
642        assert!(d2_left.abs() < 1e-8, "d2 at left = {}", d2_left);
643        assert!(d2_right.abs() < 1e-8, "d2 at right = {}", d2_right);
644    }
645    #[test]
646    fn test_spline_eval_derivative_finite_diff() {
647        let x = vec![0.0, 1.0, 2.0, 3.0];
648        let y = vec![0.0, 1.0, 0.0, 1.0];
649        let spline = CubicSpline::natural(x, y);
650        let t = 1.5;
651        let h = 1e-5;
652        let fd = (spline.eval(t + h) - spline.eval(t - h)) / (2.0 * h);
653        let exact = spline.eval_derivative(t);
654        assert!((fd - exact).abs() < 1e-4, "fd={} exact={}", fd, exact);
655    }
656    #[test]
657    fn test_cubic_spline_linear_data() {
658        let x = vec![0.0, 1.0, 2.0, 3.0];
659        let y = vec![0.0, 2.0, 4.0, 6.0];
660        let spline = CubicSpline::natural(x, y);
661        let val = spline.eval(1.5);
662        assert!((val - 3.0).abs() < 1e-8);
663    }
664    #[test]
665    fn test_bspline_fit_interpolates_when_num_ctrl_eq_data() {
666        let data: Vec<[f64; 3]> = (0..5)
667            .map(|i| {
668                let t = i as f64 / 4.0;
669                [t, t * t, 0.0]
670            })
671            .collect();
672        let curve = fit_bspline_least_squares(&data, 5, 3);
673        assert_eq!(curve.control_points.len(), 5);
674    }
675    #[test]
676    fn test_bspline_fit_approximates_data() {
677        let data: Vec<[f64; 3]> = (0..10)
678            .map(|i| {
679                let t = i as f64 / 9.0;
680                [t, (2.0 * PI * t).sin(), 0.0]
681            })
682            .collect();
683        let curve = fit_bspline_least_squares(&data, 6, 3);
684        let pt_mid = curve.eval(0.5);
685        assert!(pt_mid[0].abs() < 2.0, "mid x = {}", pt_mid[0]);
686    }
687    #[test]
688    fn test_surface_of_revolution_cylinder() {
689        let profile = vec![[1.0, 0.0], [1.0, 1.0]];
690        let grid = surface_of_revolution(&profile, 8);
691        assert_eq!(grid.len(), 2);
692        assert_eq!(grid[0].len(), 8);
693        for row in &grid {
694            for p in row {
695                let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
696                assert!((r - 1.0).abs() < 1e-9);
697            }
698        }
699    }
700    #[test]
701    fn test_surface_of_revolution_point_count() {
702        let profile: Vec<[f64; 2]> = (0..5).map(|i| [i as f64, i as f64]).collect();
703        let grid = surface_of_revolution(&profile, 12);
704        assert_eq!(grid.len(), 5);
705        for row in &grid {
706            assert_eq!(row.len(), 12);
707        }
708    }
709    #[test]
710    fn test_swept_surface_grid_shape() {
711        let spine: Vec<[f64; 3]> = (0..5).map(|i| [i as f64, 0.0, 0.0]).collect();
712        let profile = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]];
713        let sw = SweptSurface::new(spine, profile);
714        let grid = sw.compute();
715        assert_eq!(grid.len(), 5);
716        assert_eq!(grid[0].len(), 3);
717    }
718    #[test]
719    fn test_swept_surface_spine_on_surface() {
720        let spine: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
721        let profile = vec![[0.0, 0.0]];
722        let sw = SweptSurface::new(spine.clone(), profile);
723        let grid = sw.compute();
724        for (i, sp) in spine.iter().enumerate() {
725            for k in 0..3 {
726                assert!((grid[i][0][k] - sp[k]).abs() < 1e-9);
727            }
728        }
729    }
730    #[test]
731    fn test_lofted_surface_shape() {
732        let s0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
733        let s1 = vec![[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [2.0, 0.0, 1.0]];
734        let grid = lofted_surface(&[s0, s1], 5);
735        assert_eq!(grid.len(), 5);
736        assert_eq!(grid[0].len(), 3);
737    }
738    #[test]
739    fn test_lofted_surface_endpoints() {
740        let s0 = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
741        let s1 = vec![[0.0, 0.0, 2.0], [1.0, 0.0, 2.0]];
742        let grid = lofted_surface(&[s0.clone(), s1.clone()], 3);
743        for k in 0..3 {
744            assert!((grid[0][0][k] - s0[0][k]).abs() < 1e-9);
745            assert!((grid[2][0][k] - s1[0][k]).abs() < 1e-9);
746        }
747    }
748    #[test]
749    fn test_blending_surface_g0_endpoints() {
750        let c0 = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
751        let c1 = BezierCurve::new(vec![[0.0, 1.0, 0.0], [1.0, 1.0, 0.0]]);
752        let blend = BlendingSurface::new(c0.clone(), c1.clone(), 1.0, 1.0, ContinuityOrder::G0);
753        let p0 = blend.eval(0.0, 0.5);
754        let p1 = blend.eval(1.0, 0.5);
755        let ep0 = c0.eval(0.5);
756        let ep1 = c1.eval(0.5);
757        for k in 0..3 {
758            assert!((p0[k] - ep0[k]).abs() < 1e-9);
759            assert!((p1[k] - ep1[k]).abs() < 1e-9);
760        }
761    }
762    #[test]
763    fn test_blending_surface_g1_continuity() {
764        let c0 = BezierCurve::new(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]]);
765        let c1 = BezierCurve::new(vec![[0.0, 2.0, 0.0], [1.0, 2.0, 0.0], [2.0, 2.0, 0.0]]);
766        let blend = BlendingSurface::new(c0, c1, 0.5, 0.5, ContinuityOrder::G1);
767        let grid = blend.sample_grid(5, 5);
768        assert_eq!(grid.len(), 5);
769        assert_eq!(grid[0].len(), 5);
770    }
771    #[test]
772    fn test_blending_surface_g2_sample() {
773        let c0 = BezierCurve::new(vec![
774            [0.0, 0.0, 0.0],
775            [1.0, 0.0, 0.0],
776            [2.0, 0.0, 0.0],
777            [3.0, 0.0, 0.0],
778        ]);
779        let c1 = BezierCurve::new(vec![
780            [0.0, 3.0, 0.0],
781            [1.0, 3.0, 0.0],
782            [2.0, 3.0, 0.0],
783            [3.0, 3.0, 0.0],
784        ]);
785        let blend = BlendingSurface::new(c0, c1, 0.3, 0.3, ContinuityOrder::G2);
786        let p = blend.eval(0.5, 0.5);
787        assert!(p[1] > 0.0 && p[1] < 3.0);
788    }
789    #[test]
790    fn test_curvature_line_is_zero() {
791        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
792        let curve = BezierCurve::new(ctrl);
793        let kappa = curve_curvature(&curve, 0.5);
794        assert!(kappa < 1e-8, "curvature of line = {}", kappa);
795    }
796    #[test]
797    fn test_curvature_circle_approx() {
798        let r = 2.0;
799        let ctrl = vec![[r, 0.0, 0.0], [r, r, 0.0], [0.0, r, 0.0]];
800        let curve = BezierCurve::new(ctrl);
801        let kappa = curve_curvature(&curve, 0.5);
802        assert!(kappa > 0.0);
803    }
804    #[test]
805    fn test_frenet_serret_orthonormal() {
806        let ctrl = vec![
807            [0.0, 0.0, 0.0],
808            [1.0, 1.0, 0.0],
809            [2.0, 0.0, 1.0],
810            [3.0, 1.0, 1.0],
811        ];
812        let curve = BezierCurve::new(ctrl);
813        let (t, n, b) = frenet_serret_frame(&curve, 0.5);
814        assert!((vec3_norm(t) - 1.0).abs() < 1e-6);
815        let _ = n;
816        let _ = b;
817    }
818    #[test]
819    fn test_osculating_radius_line_is_infinity() {
820        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
821        let curve = BezierCurve::new(ctrl);
822        let r = osculating_radius(&curve, 0.5);
823        assert!(r == f64::INFINITY || r > 1e10);
824    }
825    #[test]
826    fn test_bspline_surface_eval_shape() {
827        let net: Vec<Vec<[f64; 3]>> = (0..3)
828            .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
829            .collect();
830        let surf = BSplineSurface::with_clamped_knots(net, 2, 2);
831        let p = surf.eval(0.5, 0.5);
832        assert!(p[0] >= 0.0 && p[0] <= 2.0);
833        assert!(p[1] >= 0.0 && p[1] <= 2.0);
834    }
835    #[test]
836    fn test_bspline_surface_normal_nonzero() {
837        let net: Vec<Vec<[f64; 3]>> = (0..3)
838            .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
839            .collect();
840        let surf = BSplineSurface::with_clamped_knots(net, 2, 2);
841        let n = surf.normal(0.5, 0.5);
842        let norm = vec3_norm(n);
843        assert!((norm - 1.0).abs() < 0.1, "normal norm = {}", norm);
844    }
845    #[test]
846    fn test_bspline_surface_sample_grid_dimensions() {
847        let net: Vec<Vec<[f64; 3]>> = (0..4)
848            .map(|i| (0..4).map(|j| [i as f64, j as f64, 0.0]).collect())
849            .collect();
850        let surf = BSplineSurface::with_clamped_knots(net, 3, 3);
851        let grid = surf.sample_grid(5, 7);
852        assert_eq!(grid.len(), 5);
853        assert_eq!(grid[0].len(), 7);
854    }
855    #[test]
856    fn test_nurbs_surface_eval() {
857        let net: Vec<Vec<[f64; 4]>> = (0..3)
858            .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0, 1.0]).collect())
859            .collect();
860        let ku = uniform_clamped_knots(3, 2);
861        let kv = uniform_clamped_knots(3, 2);
862        let surf = NurbsSurface::new(net, 2, 2, ku, kv);
863        let p = surf.eval(0.5, 0.5);
864        assert!(p[0].is_finite() && p[1].is_finite());
865    }
866    #[test]
867    fn test_periodic_bspline_returns_finite() {
868        let ctrl = vec![
869            [0.0, 0.0, 0.0],
870            [1.0, 0.0, 0.0],
871            [1.0, 1.0, 0.0],
872            [0.0, 1.0, 0.0],
873        ];
874        let curve = PeriodicBSpline::new(ctrl, 3);
875        let p = curve.eval(0.5);
876        for &pk in p.iter() {
877            assert!(pk.is_finite());
878        }
879    }
880    #[test]
881    fn test_chord_length_params_bounds() {
882        let pts = vec![
883            [0.0, 0.0, 0.0],
884            [1.0, 0.0, 0.0],
885            [2.0, 1.0, 0.0],
886            [3.0, 0.0, 0.0],
887        ];
888        let params = chord_length_params(&pts);
889        assert_eq!(params.len(), 4);
890        assert!((params[0] - 0.0).abs() < EPS);
891        assert!((params[3] - 1.0).abs() < EPS);
892        for i in 0..params.len() - 1 {
893            assert!(params[i] <= params[i + 1]);
894        }
895    }
896    #[test]
897    fn test_knot_insert_preserves_shape() {
898        let ctrl = vec![
899            [0.0, 0.0, 0.0],
900            [1.0, 1.0, 0.0],
901            [2.0, 0.0, 0.0],
902            [3.0, 1.0, 0.0],
903        ];
904        let curve = BSplineCurve::with_clamped_knots(ctrl, 3);
905        let refined = knot_insert(&curve, 0.5);
906        for i in 1..9 {
907            let t = i as f64 / 9.0;
908            let p_orig = curve.eval(t);
909            let p_ref = refined.eval(t);
910            for k in 0..3 {
911                assert!(
912                    (p_orig[k] - p_ref[k]).abs() < 1e-8,
913                    "knot insert changed shape at t={}: {} vs {}",
914                    t,
915                    p_orig[k],
916                    p_ref[k]
917                );
918            }
919        }
920    }
921    #[test]
922    fn test_knot_insert_increases_knot_count() {
923        let ctrl = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
924        let curve = BSplineCurve::with_clamped_knots(ctrl, 2);
925        let n_before = curve.knots.len();
926        let refined = knot_insert(&curve, 0.4);
927        assert_eq!(refined.knots.len(), n_before + 1);
928    }
929    #[test]
930    fn test_bezier_patch_corner_interpolation() {
931        let grid: Vec<Vec<[f64; 3]>> = (0..3)
932            .map(|i| (0..3).map(|j| [i as f64, j as f64, 0.0]).collect())
933            .collect();
934        let patch = BezierPatch::new(grid.clone());
935        let p00 = patch.eval(0.0, 0.0);
936        let p11 = patch.eval(1.0, 1.0);
937        assert!((p00[0] - grid[0][0][0]).abs() < EPS);
938        assert!((p11[0] - grid[2][2][0]).abs() < EPS);
939    }
940    #[test]
941    fn test_bezier_patch_sample_grid_shape() {
942        let grid: Vec<Vec<[f64; 3]>> = (0..4)
943            .map(|i| (0..4).map(|j| [i as f64, j as f64, 0.0]).collect())
944            .collect();
945        let patch = BezierPatch::new(grid);
946        let sampled = patch.sample_grid(6, 8);
947        assert_eq!(sampled.len(), 6);
948        assert_eq!(sampled[0].len(), 8);
949    }
950    #[test]
951    fn test_vec3_cross_orthogonal() {
952        let a = [1.0, 0.0, 0.0];
953        let b = [0.0, 1.0, 0.0];
954        let c = vec3_cross(a, b);
955        assert!((c[0]).abs() < EPS);
956        assert!((c[1]).abs() < EPS);
957        assert!((c[2] - 1.0).abs() < EPS);
958    }
959    #[test]
960    fn test_vec3_normalize_unit() {
961        let a = [3.0, 4.0, 0.0];
962        let n = vec3_normalize(a);
963        assert!((vec3_norm(n) - 1.0).abs() < 1e-9);
964    }
965    #[test]
966    fn test_vec3_lerp_midpoint() {
967        let a = [0.0, 0.0, 0.0];
968        let b = [2.0, 4.0, 6.0];
969        let mid = vec3_lerp(a, b, 0.5);
970        assert!((mid[0] - 1.0).abs() < EPS);
971        assert!((mid[1] - 2.0).abs() < EPS);
972        assert!((mid[2] - 3.0).abs() < EPS);
973    }
974}