Skip to main content

oxiphysics_geometry/spline_geometry/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6use std::f64::consts::PI;
7
8/// Generate a swept surface by moving a profile curve along a spine curve.
9///
10/// The profile is defined in a local frame; this function sweeps it along
11/// sample points of the spine, generating a grid of 3D points.
12pub struct SweptSurface {
13    /// Spine curve points.
14    pub spine: Vec<[f64; 3]>,
15    /// Profile curve (2D in local frame, stored as \[u, v\]).
16    pub profile: Vec<[f64; 2]>,
17}
18impl SweptSurface {
19    /// Create a new swept surface.
20    pub fn new(spine: Vec<[f64; 3]>, profile: Vec<[f64; 2]>) -> Self {
21        Self { spine, profile }
22    }
23    /// Compute the swept surface grid.
24    ///
25    /// At each spine point, constructs a Frenet-Serret-like frame and places
26    /// the profile in the local normal plane.
27    pub fn compute(&self) -> Vec<Vec<[f64; 3]>> {
28        let ns = self.spine.len();
29        let np = self.profile.len();
30        let mut grid = vec![vec![[0.0f64; 3]; np]; ns];
31        for (i, &sp) in self.spine.iter().enumerate() {
32            let tangent = if i == 0 && ns > 1 {
33                vec3_normalize(vec3_sub(self.spine[1], self.spine[0]))
34            } else if i == ns - 1 && ns > 1 {
35                vec3_normalize(vec3_sub(self.spine[ns - 1], self.spine[ns - 2]))
36            } else if ns > 2 {
37                vec3_normalize(vec3_sub(self.spine[i + 1], self.spine[i - 1]))
38            } else {
39                [0.0, 0.0, 1.0]
40            };
41            let up = if tangent[0].abs() < 0.9 {
42                [1.0, 0.0, 0.0]
43            } else {
44                [0.0, 1.0, 0.0]
45            };
46            let normal = vec3_normalize(vec3_cross(tangent, up));
47            let binormal = vec3_cross(tangent, normal);
48            for (j, &[u, v]) in self.profile.iter().enumerate() {
49                grid[i][j] = vec3_add(vec3_add(sp, vec3_scale(normal, u)), vec3_scale(binormal, v));
50            }
51        }
52        grid
53    }
54}
55/// A NURBS surface in 3D space.
56#[derive(Debug, Clone)]
57pub struct NurbsSurface {
58    /// Control net in homogeneous coordinates, row-major (u direction first).
59    pub control_net: Vec<Vec<[f64; 4]>>,
60    /// Degree in u direction.
61    pub degree_u: usize,
62    /// Degree in v direction.
63    pub degree_v: usize,
64    /// Knot vector in u direction.
65    pub knots_u: Vec<f64>,
66    /// Knot vector in v direction.
67    pub knots_v: Vec<f64>,
68}
69impl NurbsSurface {
70    /// Create a NURBS surface.
71    pub fn new(
72        control_net: Vec<Vec<[f64; 4]>>,
73        degree_u: usize,
74        degree_v: usize,
75        knots_u: Vec<f64>,
76        knots_v: Vec<f64>,
77    ) -> Self {
78        Self {
79            control_net,
80            degree_u,
81            degree_v,
82            knots_u,
83            knots_v,
84        }
85    }
86    /// Evaluate the surface at `(u, v)`.
87    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
88        let nu = self.control_net.len();
89        let nv = self.control_net[0].len();
90        let span_u = find_knot_span(nu, self.degree_u, u, &self.knots_u);
91        let span_v = find_knot_span(nv, self.degree_v, v, &self.knots_v);
92        let bu = bspline_basis(span_u, self.degree_u, u, &self.knots_u);
93        let bv = bspline_basis(span_v, self.degree_v, v, &self.knots_v);
94        let mut hw = [0.0f64; 4];
95        for (i, &bui) in bu.iter().enumerate().take(self.degree_u + 1) {
96            for (j, &bvj) in bv.iter().enumerate().take(self.degree_v + 1) {
97                let cpw = self.control_net[span_u - self.degree_u + i][span_v - self.degree_v + j];
98                let b = bui * bvj;
99                for k in 0..4 {
100                    hw[k] += b * cpw[k];
101                }
102            }
103        }
104        if hw[3].abs() < 1e-300 {
105            [hw[0], hw[1], hw[2]]
106        } else {
107            [hw[0] / hw[3], hw[1] / hw[3], hw[2] / hw[3]]
108        }
109    }
110    /// Sample the surface on a `nu x nv` grid.
111    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
112        (0..nu)
113            .map(|i| {
114                let u = i as f64 / (nu - 1).max(1) as f64;
115                (0..nv)
116                    .map(|j| {
117                        let v = j as f64 / (nv - 1).max(1) as f64;
118                        self.eval(u, v)
119                    })
120                    .collect()
121            })
122            .collect()
123    }
124}
125/// A NURBS (Non-Uniform Rational B-Spline) curve in 3D space.
126#[derive(Debug, Clone)]
127pub struct NurbsCurve {
128    /// Control points in homogeneous coordinates `[wx, wy, wz, w]`.
129    pub control_points_w: Vec<[f64; 4]>,
130    /// Polynomial degree.
131    pub degree: usize,
132    /// Knot vector.
133    pub knots: Vec<f64>,
134}
135impl NurbsCurve {
136    /// Create a NURBS curve from weighted control points and a knot vector.
137    pub fn new(control_points_w: Vec<[f64; 4]>, degree: usize, knots: Vec<f64>) -> Self {
138        Self {
139            control_points_w,
140            degree,
141            knots,
142        }
143    }
144    /// Create a NURBS curve from 3D control points, weights, and a clamped knot vector.
145    pub fn from_points_and_weights(
146        points: Vec<[f64; 3]>,
147        weights: Vec<f64>,
148        degree: usize,
149    ) -> Self {
150        assert_eq!(points.len(), weights.len());
151        let knots = uniform_clamped_knots(points.len(), degree);
152        let control_points_w = points
153            .iter()
154            .zip(weights.iter())
155            .map(|(p, &w)| [p[0] * w, p[1] * w, p[2] * w, w])
156            .collect();
157        Self::new(control_points_w, degree, knots)
158    }
159    /// Evaluate the rational curve at parameter `t`.
160    pub fn eval(&self, t: f64) -> [f64; 3] {
161        let n = self.control_points_w.len();
162        let p = self.degree;
163        let span = find_knot_span(n, p, t, &self.knots);
164        let basis = bspline_basis(span, p, t, &self.knots);
165        let mut hw = [0.0f64; 4];
166        for (i, &bi) in basis.iter().enumerate().take(p + 1) {
167            let cpw = self.control_points_w[span - p + i];
168            for k in 0..4 {
169                hw[k] += bi * cpw[k];
170            }
171        }
172        if hw[3].abs() < 1e-300 {
173            [hw[0], hw[1], hw[2]]
174        } else {
175            [hw[0] / hw[3], hw[1] / hw[3], hw[2] / hw[3]]
176        }
177    }
178    /// Sample the NURBS curve at `num_samples` parameter values.
179    pub fn sample(&self, num_samples: usize) -> Vec<[f64; 3]> {
180        (0..num_samples)
181            .map(|i| {
182                let t = i as f64 / (num_samples - 1).max(1) as f64;
183                self.eval(t)
184            })
185            .collect()
186    }
187    /// Construct a NURBS circle in the XY plane with given radius.
188    ///
189    /// Uses 9 control points (degree 2) with quarter-arc weights `cos(Ï€/4)`.
190    pub fn circle(radius: f64) -> Self {
191        let w = (PI / 4.0).cos();
192        let r = radius;
193        let pts_w: Vec<[f64; 4]> = vec![
194            [r, 0.0, 0.0, 1.0],
195            [r * w, r * w, 0.0, w],
196            [0.0, r, 0.0, 1.0],
197            [-r * w, r * w, 0.0, w],
198            [-r, 0.0, 0.0, 1.0],
199            [-r * w, -r * w, 0.0, w],
200            [0.0, -r, 0.0, 1.0],
201            [r * w, -r * w, 0.0, w],
202            [r, 0.0, 0.0, 1.0],
203        ];
204        let knots = vec![
205            0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
206        ];
207        Self::new(pts_w, 2, knots)
208    }
209}
210/// Result of cubic spline interpolation: piece-wise cubic coefficients.
211///
212/// Each segment `[x[i\], x[i+1]]` has coefficients `(a, b, c, d)` such that
213/// `S(x) = a + b*(x - x[i]) + c*(x - x[i])^2 + d*(x - x[i])^3`.
214#[derive(Debug, Clone)]
215pub struct CubicSpline {
216    /// x-knots (must be strictly increasing).
217    pub x: Vec<f64>,
218    /// Coefficients a (= y values at left knot).
219    pub a: Vec<f64>,
220    /// Coefficients b.
221    pub b: Vec<f64>,
222    /// Coefficients c.
223    pub c: Vec<f64>,
224    /// Coefficients d.
225    pub d: Vec<f64>,
226}
227impl CubicSpline {
228    /// Build a **natural** cubic spline through the given `(x, y)` data.
229    ///
230    /// Natural boundary: second derivative = 0 at both endpoints.
231    pub fn natural(x: Vec<f64>, y: Vec<f64>) -> Self {
232        let n = x.len();
233        assert!(n >= 2, "need at least 2 points");
234        assert_eq!(x.len(), y.len());
235        let nm1 = n - 1;
236        let mut h = vec![0.0f64; nm1];
237        for i in 0..nm1 {
238            h[i] = x[i + 1] - x[i];
239            assert!(h[i] > 0.0, "x must be strictly increasing");
240        }
241        let mut alpha = vec![0.0f64; nm1];
242        for i in 1..nm1 {
243            alpha[i] = 3.0 / h[i] * (y[i + 1] - y[i]) - 3.0 / h[i - 1] * (y[i] - y[i - 1]);
244        }
245        let mut l = vec![1.0f64; n];
246        let mut mu = vec![0.0f64; n];
247        let mut z = vec![0.0f64; n];
248        for i in 1..nm1 {
249            l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
250            mu[i] = h[i] / l[i];
251            z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
252        }
253        let mut c = vec![0.0f64; n];
254        let mut b = vec![0.0f64; nm1];
255        let mut d = vec![0.0f64; nm1];
256        for j in (0..nm1).rev() {
257            c[j] = z[j] - mu[j] * c[j + 1];
258            b[j] = (y[j + 1] - y[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
259            d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
260        }
261        let a = y[..nm1].to_vec();
262        CubicSpline {
263            x,
264            a,
265            b,
266            c: c[..nm1].to_vec(),
267            d,
268        }
269    }
270    /// Build a **clamped** cubic spline with given endpoint slopes `d0` and `dn`.
271    pub fn clamped(x: Vec<f64>, y: Vec<f64>, d0: f64, dn: f64) -> Self {
272        let n = x.len();
273        assert!(n >= 2);
274        assert_eq!(x.len(), y.len());
275        let nm1 = n - 1;
276        let mut h = vec![0.0f64; nm1];
277        for i in 0..nm1 {
278            h[i] = x[i + 1] - x[i];
279        }
280        let mut alpha = vec![0.0f64; n];
281        alpha[0] = 3.0 * (y[1] - y[0]) / h[0] - 3.0 * d0;
282        alpha[nm1] = 3.0 * dn - 3.0 * (y[nm1] - y[nm1 - 1]) / h[nm1 - 1];
283        for i in 1..nm1 {
284            alpha[i] = 3.0 / h[i] * (y[i + 1] - y[i]) - 3.0 / h[i - 1] * (y[i] - y[i - 1]);
285        }
286        let mut l = vec![0.0f64; n];
287        let mut mu = vec![0.0f64; n];
288        let mut z = vec![0.0f64; n];
289        l[0] = 2.0 * h[0];
290        mu[0] = 0.5;
291        z[0] = alpha[0] / l[0];
292        for i in 1..nm1 {
293            l[i] = 2.0 * (x[i + 1] - x[i - 1]) - h[i - 1] * mu[i - 1];
294            mu[i] = h[i] / l[i];
295            z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
296        }
297        l[nm1] = h[nm1 - 1] * (2.0 - mu[nm1 - 1]);
298        z[nm1] = (alpha[nm1] - h[nm1 - 1] * z[nm1 - 1]) / l[nm1];
299        let mut c = vec![0.0f64; n];
300        c[nm1] = z[nm1];
301        let mut b = vec![0.0f64; nm1];
302        let mut d = vec![0.0f64; nm1];
303        for j in (0..nm1).rev() {
304            c[j] = z[j] - mu[j] * c[j + 1];
305            b[j] = (y[j + 1] - y[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
306            d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
307        }
308        let a = y[..nm1].to_vec();
309        CubicSpline {
310            x,
311            a,
312            b,
313            c: c[..nm1].to_vec(),
314            d,
315        }
316    }
317    /// Evaluate the spline at `t`.
318    pub fn eval(&self, t: f64) -> f64 {
319        let n = self.x.len() - 1;
320        let i = if t <= self.x[0] {
321            0
322        } else if t >= self.x[n] {
323            n - 1
324        } else {
325            let mut lo = 0usize;
326            let mut hi = n;
327            while hi - lo > 1 {
328                let mid = (lo + hi) / 2;
329                if self.x[mid] <= t {
330                    lo = mid;
331                } else {
332                    hi = mid;
333                }
334            }
335            lo
336        };
337        let dx = t - self.x[i];
338        self.a[i] + self.b[i] * dx + self.c[i] * dx * dx + self.d[i] * dx * dx * dx
339    }
340    /// Evaluate the first derivative at `t`.
341    pub fn eval_derivative(&self, t: f64) -> f64 {
342        let n = self.x.len() - 1;
343        let i = if t <= self.x[0] {
344            0
345        } else if t >= self.x[n] {
346            n - 1
347        } else {
348            let mut lo = 0usize;
349            let mut hi = n;
350            while hi - lo > 1 {
351                let mid = (lo + hi) / 2;
352                if self.x[mid] <= t {
353                    lo = mid;
354                } else {
355                    hi = mid;
356                }
357            }
358            lo
359        };
360        let dx = t - self.x[i];
361        self.b[i] + 2.0 * self.c[i] * dx + 3.0 * self.d[i] * dx * dx
362    }
363    /// Evaluate the second derivative at `t`.
364    pub fn eval_second_derivative(&self, t: f64) -> f64 {
365        let n = self.x.len() - 1;
366        let i = if t <= self.x[0] {
367            0
368        } else if t >= self.x[n] {
369            n - 1
370        } else {
371            let mut lo = 0usize;
372            let mut hi = n;
373            while hi - lo > 1 {
374                let mid = (lo + hi) / 2;
375                if self.x[mid] <= t {
376                    lo = mid;
377                } else {
378                    hi = mid;
379                }
380            }
381            lo
382        };
383        let dx = t - self.x[i];
384        2.0 * self.c[i] + 6.0 * self.d[i] * dx
385    }
386}
387/// A blending surface connecting two boundary curves.
388///
389/// The surface is parameterized by `(u, v)` where `u ∈ [0,1]` interpolates
390/// between the two boundary curves, and `v ∈ [0,1]` walks along them.
391#[derive(Debug, Clone)]
392pub struct BlendingSurface {
393    /// First boundary curve control points.
394    pub curve0: BezierCurve,
395    /// Second boundary curve control points.
396    pub curve1: BezierCurve,
397    /// Tangent scale factor at curve0 (for G1/G2).
398    pub tangent_scale0: f64,
399    /// Tangent scale factor at curve1 (for G1/G2).
400    pub tangent_scale1: f64,
401    /// Continuity order.
402    pub continuity: ContinuityOrder,
403}
404impl BlendingSurface {
405    /// Create a blending surface with specified continuity.
406    pub fn new(
407        curve0: BezierCurve,
408        curve1: BezierCurve,
409        tangent_scale0: f64,
410        tangent_scale1: f64,
411        continuity: ContinuityOrder,
412    ) -> Self {
413        Self {
414            curve0,
415            curve1,
416            tangent_scale0,
417            tangent_scale1,
418            continuity,
419        }
420    }
421    /// Evaluate the blending surface at `(u, v)`.
422    ///
423    /// Uses Hermite blending functions to achieve the requested continuity.
424    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
425        let p0 = self.curve0.eval(v);
426        let p1 = self.curve1.eval(v);
427        match self.continuity {
428            ContinuityOrder::G0 => vec3_lerp(p0, p1, u),
429            ContinuityOrder::G1 => {
430                let h00 = 2.0 * u * u * u - 3.0 * u * u + 1.0;
431                let h10 = u * u * u - 2.0 * u * u + u;
432                let h01 = -2.0 * u * u * u + 3.0 * u * u;
433                let h11 = u * u * u - u * u;
434                let t0 = self.curve0.tangent(v);
435                let t1 = self.curve1.tangent(v);
436                let mut result = [0.0f64; 3];
437                for k in 0..3 {
438                    result[k] = h00 * p0[k]
439                        + h10 * self.tangent_scale0 * t0[k]
440                        + h01 * p1[k]
441                        + h11 * self.tangent_scale1 * t1[k];
442                }
443                result
444            }
445            ContinuityOrder::G2 => {
446                let h00 = 1.0 - 10.0 * u * u * u + 15.0 * u * u * u * u - 6.0 * u * u * u * u * u;
447                let h10 = u - 6.0 * u * u * u + 8.0 * u * u * u * u - 3.0 * u * u * u * u * u;
448                let h20 =
449                    0.5 * u * u - 1.5 * u * u * u + 1.5 * u * u * u * u - 0.5 * u * u * u * u * u;
450                let h01 = 10.0 * u * u * u - 15.0 * u * u * u * u + 6.0 * u * u * u * u * u;
451                let h11 = -4.0 * u * u * u + 7.0 * u * u * u * u - 3.0 * u * u * u * u * u;
452                let h21 = 0.5 * u * u * u - u * u * u * u + 0.5 * u * u * u * u * u;
453                let t0 = self.curve0.tangent(v);
454                let t1 = self.curve1.tangent(v);
455                let a0 = self.curve0.second_derivative(v);
456                let a1 = self.curve1.second_derivative(v);
457                let mut result = [0.0f64; 3];
458                for k in 0..3 {
459                    result[k] = h00 * p0[k]
460                        + h10 * self.tangent_scale0 * t0[k]
461                        + h20 * self.tangent_scale0 * self.tangent_scale0 * a0[k]
462                        + h01 * p1[k]
463                        + h11 * self.tangent_scale1 * t1[k]
464                        + h21 * self.tangent_scale1 * self.tangent_scale1 * a1[k];
465                }
466                result
467            }
468        }
469    }
470    /// Sample the blending surface on a `nu x nv` grid.
471    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
472        (0..nu)
473            .map(|i| {
474                let u = i as f64 / (nu - 1).max(1) as f64;
475                (0..nv)
476                    .map(|j| {
477                        let v = j as f64 / (nv - 1).max(1) as f64;
478                        self.eval(u, v)
479                    })
480                    .collect()
481            })
482            .collect()
483    }
484}
485/// A non-rational B-spline surface.
486#[derive(Debug, Clone)]
487pub struct BSplineSurface {
488    /// Control net (nu x nv), stored as rows of v-direction points.
489    pub control_net: Vec<Vec<[f64; 3]>>,
490    /// Degree in u direction.
491    pub degree_u: usize,
492    /// Degree in v direction.
493    pub degree_v: usize,
494    /// Knot vector in u direction.
495    pub knots_u: Vec<f64>,
496    /// Knot vector in v direction.
497    pub knots_v: Vec<f64>,
498}
499impl BSplineSurface {
500    /// Create a B-spline surface with explicit knot vectors.
501    pub fn new(
502        control_net: Vec<Vec<[f64; 3]>>,
503        degree_u: usize,
504        degree_v: usize,
505        knots_u: Vec<f64>,
506        knots_v: Vec<f64>,
507    ) -> Self {
508        Self {
509            control_net,
510            degree_u,
511            degree_v,
512            knots_u,
513            knots_v,
514        }
515    }
516    /// Create a B-spline surface with uniform clamped knot vectors.
517    pub fn with_clamped_knots(
518        control_net: Vec<Vec<[f64; 3]>>,
519        degree_u: usize,
520        degree_v: usize,
521    ) -> Self {
522        let nu = control_net.len();
523        let nv = control_net[0].len();
524        let knots_u = uniform_clamped_knots(nu, degree_u);
525        let knots_v = uniform_clamped_knots(nv, degree_v);
526        Self::new(control_net, degree_u, degree_v, knots_u, knots_v)
527    }
528    /// Evaluate the surface at `(u, v)`.
529    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
530        let nu = self.control_net.len();
531        let nv = self.control_net[0].len();
532        let span_u = find_knot_span(nu, self.degree_u, u, &self.knots_u);
533        let span_v = find_knot_span(nv, self.degree_v, v, &self.knots_v);
534        let bu = bspline_basis(span_u, self.degree_u, u, &self.knots_u);
535        let bv = bspline_basis(span_v, self.degree_v, v, &self.knots_v);
536        let mut point = [0.0f64; 3];
537        for (i, &bui) in bu.iter().enumerate().take(self.degree_u + 1) {
538            for (j, &bvj) in bv.iter().enumerate().take(self.degree_v + 1) {
539                let cp = self.control_net[span_u - self.degree_u + i][span_v - self.degree_v + j];
540                let b = bui * bvj;
541                point = vec3_add(point, vec3_scale(cp, b));
542            }
543        }
544        point
545    }
546    /// Compute the surface normal at `(u, v)` using partial derivatives.
547    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
548        let eps = 1e-6;
549        let pu = vec3_sub(
550            self.eval((u + eps).min(1.0), v),
551            self.eval((u - eps).max(0.0), v),
552        );
553        let pv = vec3_sub(
554            self.eval(u, (v + eps).min(1.0)),
555            self.eval(u, (v - eps).max(0.0)),
556        );
557        vec3_normalize(vec3_cross(pu, pv))
558    }
559    /// Sample the surface on a `nu x nv` grid.
560    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
561        (0..nu)
562            .map(|i| {
563                let u = i as f64 / (nu - 1).max(1) as f64;
564                (0..nv)
565                    .map(|j| {
566                        let v = j as f64 / (nv - 1).max(1) as f64;
567                        self.eval(u, v)
568                    })
569                    .collect()
570            })
571            .collect()
572    }
573}
574/// A tensor-product Bézier surface patch.
575///
576/// Defined by a `(m+1) x (n+1)` grid of control points (degree `m` in u, degree `n` in v).
577#[derive(Debug, Clone)]
578pub struct BezierPatch {
579    /// Control point grid, row-major (u direction first), each row has `n+1` points.
580    pub control_grid: Vec<Vec<[f64; 3]>>,
581}
582impl BezierPatch {
583    /// Create a new Bézier patch.
584    pub fn new(control_grid: Vec<Vec<[f64; 3]>>) -> Self {
585        Self { control_grid }
586    }
587    /// Evaluate the patch at `(u, v)` using de Casteljau in both directions.
588    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
589        let row_pts: Vec<[f64; 3]> = self
590            .control_grid
591            .iter()
592            .map(|row| BezierCurve::new(row.clone()).eval(v))
593            .collect();
594        BezierCurve::new(row_pts).eval(u)
595    }
596    /// Sample the patch on an `nu x nv` grid.
597    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
598        (0..nu)
599            .map(|i| {
600                let u = i as f64 / (nu - 1).max(1) as f64;
601                (0..nv)
602                    .map(|j| {
603                        let v = j as f64 / (nv - 1).max(1) as f64;
604                        self.eval(u, v)
605                    })
606                    .collect()
607            })
608            .collect()
609    }
610}
611/// A Bézier curve in 3D space defined by its control polygon.
612#[derive(Debug, Clone)]
613pub struct BezierCurve {
614    /// Control points.
615    pub control_points: Vec<[f64; 3]>,
616}
617impl BezierCurve {
618    /// Create a new Bézier curve.
619    pub fn new(control_points: Vec<[f64; 3]>) -> Self {
620        Self { control_points }
621    }
622    /// Evaluate the curve at `t ∈ [0,1]` using de Casteljau's algorithm.
623    pub fn eval(&self, t: f64) -> [f64; 3] {
624        let mut pts = self.control_points.clone();
625        let n = pts.len();
626        for r in 1..n {
627            for i in 0..n - r {
628                pts[i] = vec3_lerp(pts[i], pts[i + 1], t);
629            }
630        }
631        pts[0]
632    }
633    /// Compute the tangent (first derivative) at `t`.
634    pub fn tangent(&self, t: f64) -> [f64; 3] {
635        let n = self.control_points.len();
636        if n < 2 {
637            return [0.0, 0.0, 0.0];
638        }
639        let d: Vec<[f64; 3]> = (0..n - 1)
640            .map(|i| {
641                vec3_scale(
642                    vec3_sub(self.control_points[i + 1], self.control_points[i]),
643                    (n - 1) as f64,
644                )
645            })
646            .collect();
647        BezierCurve::new(d).eval(t)
648    }
649    /// Evaluate the second derivative at `t`.
650    pub fn second_derivative(&self, t: f64) -> [f64; 3] {
651        let n = self.control_points.len();
652        if n < 3 {
653            return [0.0, 0.0, 0.0];
654        }
655        let d1: Vec<[f64; 3]> = (0..n - 1)
656            .map(|i| {
657                vec3_scale(
658                    vec3_sub(self.control_points[i + 1], self.control_points[i]),
659                    (n - 1) as f64,
660                )
661            })
662            .collect();
663        let d2: Vec<[f64; 3]> = (0..n - 2)
664            .map(|i| vec3_scale(vec3_sub(d1[i + 1], d1[i]), (n - 2) as f64))
665            .collect();
666        BezierCurve::new(d2).eval(t)
667    }
668    /// Split the Bézier curve at `t` returning two sub-curves (de Casteljau).
669    pub fn split(&self, t: f64) -> (BezierCurve, BezierCurve) {
670        let n = self.control_points.len();
671        let mut triangle = vec![self.control_points.clone()];
672        for r in 1..n {
673            let prev = &triangle[r - 1];
674            let row: Vec<[f64; 3]> = (0..n - r)
675                .map(|i| vec3_lerp(prev[i], prev[i + 1], t))
676                .collect();
677            triangle.push(row);
678        }
679        let left: Vec<[f64; 3]> = (0..n).map(|r| triangle[r][0]).collect();
680        let right: Vec<[f64; 3]> = (0..n).map(|r| triangle[n - 1 - r][r]).collect();
681        (BezierCurve::new(left), BezierCurve::new(right))
682    }
683    /// Sample the curve at `num_samples` parameter values.
684    pub fn sample(&self, num_samples: usize) -> Vec<[f64; 3]> {
685        (0..num_samples)
686            .map(|i| {
687                let t = i as f64 / (num_samples - 1).max(1) as f64;
688                self.eval(t)
689            })
690            .collect()
691    }
692    /// Compute approximate arc length using `num_segments` sub-intervals.
693    pub fn arc_length(&self, num_segments: usize) -> f64 {
694        let pts = self.sample(num_segments + 1);
695        pts.windows(2)
696            .map(|w| vec3_norm(vec3_sub(w[1], w[0])))
697            .sum()
698    }
699    /// Elevate the degree of the Bézier curve by 1.
700    pub fn degree_elevate(&self) -> BezierCurve {
701        let n = self.control_points.len();
702        let mut new_pts = Vec::with_capacity(n + 1);
703        new_pts.push(self.control_points[0]);
704        for i in 1..n {
705            let alpha = i as f64 / n as f64;
706            new_pts.push(vec3_add(
707                vec3_scale(self.control_points[i - 1], alpha),
708                vec3_scale(self.control_points[i], 1.0 - alpha),
709            ));
710        }
711        new_pts.push(self.control_points[n - 1]);
712        BezierCurve::new(new_pts)
713    }
714}
715/// A periodic (closed) B-spline curve.
716#[derive(Debug, Clone)]
717pub struct PeriodicBSpline {
718    /// Control points (the curve wraps back to the first point).
719    pub control_points: Vec<[f64; 3]>,
720    /// Polynomial degree.
721    pub degree: usize,
722}
723impl PeriodicBSpline {
724    /// Create a periodic B-spline.
725    pub fn new(control_points: Vec<[f64; 3]>, degree: usize) -> Self {
726        Self {
727            control_points,
728            degree,
729        }
730    }
731    /// Evaluate the periodic curve at `t ∈ [0,1]`.
732    ///
733    /// Wraps the control points to create a uniform periodic knot vector.
734    pub fn eval(&self, t: f64) -> [f64; 3] {
735        let n = self.control_points.len();
736        let p = self.degree;
737        let mut ext_cp = self.control_points.clone();
738        for i in 0..p {
739            ext_cp.push(self.control_points[i % n]);
740        }
741        let m = ext_cp.len() + p + 1;
742        let knots: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
743        let num_ctrl = ext_cp.len();
744        let t_min = knots[p];
745        let t_max = knots[num_ctrl];
746        let t_inner = t_min + t * (t_max - t_min);
747        let span = find_knot_span(num_ctrl, p, t_inner.min(t_max - 1e-12), &knots);
748        let basis = bspline_basis(span, p, t_inner.min(t_max - 1e-12), &knots);
749        let mut point = [0.0f64; 3];
750        for i in 0..=p {
751            let cp = ext_cp[span - p + i];
752            point = vec3_add(point, vec3_scale(cp, basis[i]));
753        }
754        point
755    }
756}
757/// A B-spline curve in 3D space defined by control points, degree, and knot vector.
758#[derive(Debug, Clone)]
759pub struct BSplineCurve {
760    /// Control points.
761    pub control_points: Vec<[f64; 3]>,
762    /// Polynomial degree.
763    pub degree: usize,
764    /// Knot vector.
765    pub knots: Vec<f64>,
766}
767impl BSplineCurve {
768    /// Create a new B-spline curve.
769    ///
770    /// # Panics
771    /// Panics if the knot vector length is not `control_points.len() + degree + 1`.
772    pub fn new(control_points: Vec<[f64; 3]>, degree: usize, knots: Vec<f64>) -> Self {
773        assert_eq!(
774            knots.len(),
775            control_points.len() + degree + 1,
776            "knot vector length must equal n + p + 2"
777        );
778        Self {
779            control_points,
780            degree,
781            knots,
782        }
783    }
784    /// Create a B-spline curve with a uniform clamped knot vector.
785    pub fn with_clamped_knots(control_points: Vec<[f64; 3]>, degree: usize) -> Self {
786        let knots = uniform_clamped_knots(control_points.len(), degree);
787        Self::new(control_points, degree, knots)
788    }
789    /// Evaluate the curve at parameter `t` in `[0, 1]`.
790    pub fn eval(&self, t: f64) -> [f64; 3] {
791        let n = self.control_points.len();
792        let p = self.degree;
793        let span = find_knot_span(n, p, t, &self.knots);
794        let basis = bspline_basis(span, p, t, &self.knots);
795        let mut point = [0.0f64; 3];
796        for (i, &bi) in basis.iter().enumerate().take(p + 1) {
797            let cp = self.control_points[span - p + i];
798            point = vec3_add(point, vec3_scale(cp, bi));
799        }
800        point
801    }
802    /// Compute the first derivative (tangent) at parameter `t`.
803    pub fn derivative(&self, t: f64) -> [f64; 3] {
804        let p = self.degree;
805        if p == 0 {
806            return [0.0, 0.0, 0.0];
807        }
808        let n = self.control_points.len();
809        let mut d_cp = Vec::with_capacity(n - 1);
810        for i in 0..n - 1 {
811            let denom = self.knots[i + p + 1] - self.knots[i + 1];
812            if denom.abs() < 1e-300 {
813                d_cp.push([0.0, 0.0, 0.0]);
814            } else {
815                let diff = vec3_sub(self.control_points[i + 1], self.control_points[i]);
816                d_cp.push(vec3_scale(diff, p as f64 / denom));
817            }
818        }
819        let d_knots = self.knots[1..self.knots.len() - 1].to_vec();
820        let d_curve = BSplineCurve::new(d_cp, p - 1, d_knots);
821        d_curve.eval(t)
822    }
823    /// Sample the curve at `num_samples` equally-spaced parameter values.
824    pub fn sample(&self, num_samples: usize) -> Vec<[f64; 3]> {
825        (0..num_samples)
826            .map(|i| {
827                let t = i as f64 / (num_samples - 1).max(1) as f64;
828                self.eval(t)
829            })
830            .collect()
831    }
832    /// Compute approximate arc length using `num_segments` samples.
833    pub fn arc_length(&self, num_segments: usize) -> f64 {
834        let pts = self.sample(num_segments + 1);
835        pts.windows(2)
836            .map(|w| vec3_norm(vec3_sub(w[1], w[0])))
837            .sum()
838    }
839}
840/// Continuity order for blending surfaces.
841#[derive(Debug, Clone, Copy, PartialEq, Eq)]
842pub enum ContinuityOrder {
843    /// G0: positional continuity.
844    G0,
845    /// G1: tangent continuity.
846    G1,
847    /// G2: curvature continuity.
848    G2,
849}