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