Skip to main content

oxiphysics_geometry/parametric/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::{
6    add3, arbitrary_perp, cross3, dist3, dot3, lerp3, normalize3, scale3, sub3,
7};
8
9/// A tube mesh built by extruding a circular cross-section along a spine curve.
10///
11/// Uses the Frenet frame at each spine sample to orient the cross-section
12/// circles, then connects them into a triangle strip.
13pub struct TubeGeometry {
14    /// Vertex positions.
15    pub vertices: Vec<[f64; 3]>,
16    /// Triangle indices (each triple is one triangle, CCW from outside).
17    pub triangles: Vec<[usize; 3]>,
18}
19impl TubeGeometry {
20    /// Build a tube along a `BezierCurve`.
21    ///
22    /// # Parameters
23    /// - `curve`:          The spine curve.
24    /// - `radius`:         Radius of the circular cross-section.
25    /// - `n_spine`:        Number of spine samples (at least 2).
26    /// - `n_radial`:       Number of vertices around each ring (at least 3).
27    pub fn from_bezier(curve: &BezierCurve, radius: f64, n_spine: usize, n_radial: usize) -> Self {
28        assert!(n_spine >= 2, "n_spine must be >= 2");
29        assert!(n_radial >= 3, "n_radial must be >= 3");
30        use std::f64::consts::TAU;
31        let mut vertices: Vec<[f64; 3]> = Vec::with_capacity(n_spine * n_radial);
32        let mut triangles: Vec<[usize; 3]> = Vec::new();
33        for spine_i in 0..n_spine {
34            let t = spine_i as f64 / (n_spine - 1) as f64;
35            let center = curve.evaluate(t);
36            let frame = FrenetFrame::from_bezier(curve, t);
37            for rad_j in 0..n_radial {
38                let angle = TAU * rad_j as f64 / n_radial as f64;
39                let (sin_a, cos_a) = angle.sin_cos();
40                let offset = add3(
41                    scale3(frame.normal, radius * cos_a),
42                    scale3(frame.binormal, radius * sin_a),
43                );
44                vertices.push(add3(center, offset));
45            }
46        }
47        for spine_i in 0..n_spine - 1 {
48            let ring0 = spine_i * n_radial;
49            let ring1 = (spine_i + 1) * n_radial;
50            for rad_j in 0..n_radial {
51                let next_j = (rad_j + 1) % n_radial;
52                let a = ring0 + rad_j;
53                let b = ring0 + next_j;
54                let c = ring1 + rad_j;
55                let d = ring1 + next_j;
56                triangles.push([a, b, d]);
57                triangles.push([a, d, c]);
58            }
59        }
60        TubeGeometry {
61            vertices,
62            triangles,
63        }
64    }
65}
66/// An orthonormal tangent frame at a surface point.
67#[derive(Debug, Clone)]
68pub struct TangentFrame {
69    /// Surface normal (unit vector).
70    pub normal: [f64; 3],
71    /// Tangent along u direction (unit vector).
72    pub tangent_u: [f64; 3],
73    /// Tangent along v direction (unit vector, ≈ normal × tangent_u).
74    pub tangent_v: [f64; 3],
75}
76impl TangentFrame {
77    /// Compute the tangent frame of a `BezierSurface` at `(u, v)`.
78    pub fn from_bezier_surface(surf: &BezierSurface, u: f64, v: f64) -> Self {
79        let eps = 1e-5_f64;
80        let u0 = (u - eps).max(0.0);
81        let u1 = (u + eps).min(1.0);
82        let v0 = (v - eps).max(0.0);
83        let v1 = (v + eps).min(1.0);
84        let du_raw = scale3(
85            sub3(surf.evaluate(u1, v), surf.evaluate(u0, v)),
86            1.0 / (u1 - u0),
87        );
88        let dv_raw = scale3(
89            sub3(surf.evaluate(u, v1), surf.evaluate(u, v0)),
90            1.0 / (v1 - v0),
91        );
92        let normal = normalize3(cross3(du_raw, dv_raw));
93        let tangent_u = normalize3(du_raw);
94        let tangent_v = normalize3(cross3(normal, tangent_u));
95        TangentFrame {
96            normal,
97            tangent_u,
98            tangent_v,
99        }
100    }
101    /// Compute the tangent frame of a `LoftSurface` at `(u, v)`.
102    pub fn from_loft_surface(surf: &LoftSurface, u: f64, v: f64) -> Self {
103        let eps = 1e-5_f64;
104        let u0 = (u - eps).max(0.0);
105        let u1 = (u + eps).min(1.0);
106        let v0 = (v - eps).max(0.0);
107        let v1 = (v + eps).min(1.0);
108        let du_raw = scale3(
109            sub3(surf.evaluate(u1, v), surf.evaluate(u0, v)),
110            1.0 / (u1 - u0),
111        );
112        let dv_raw = scale3(
113            sub3(surf.evaluate(u, v1), surf.evaluate(u, v0)),
114            1.0 / (v1 - v0),
115        );
116        let normal = normalize3(cross3(du_raw, dv_raw));
117        let tangent_u = normalize3(du_raw);
118        let tangent_v = normalize3(cross3(normal, tangent_u));
119        TangentFrame {
120            normal,
121            tangent_u,
122            tangent_v,
123        }
124    }
125}
126/// A NURBS surface defined by a control net of weighted 3-D points.
127///
128/// Each element `control_net[i][j]` is `[x, y, z, w]` (homogeneous coordinates).
129pub struct NurbsSurface {
130    /// Control net: `control_net[i][j] = [x, y, z, w]`.
131    pub control_net: Vec<Vec<[f64; 4]>>,
132    /// Knot vector in the u direction.
133    pub u_knots: Vec<f64>,
134    /// Knot vector in the v direction.
135    pub v_knots: Vec<f64>,
136    /// Degree in the u direction.
137    pub degree_u: usize,
138    /// Degree in the v direction.
139    pub degree_v: usize,
140}
141impl NurbsSurface {
142    /// Construct a new NURBS surface.
143    pub fn new(
144        control_net: Vec<Vec<[f64; 4]>>,
145        u_knots: Vec<f64>,
146        v_knots: Vec<f64>,
147        degree_u: usize,
148        degree_v: usize,
149    ) -> Self {
150        NurbsSurface {
151            control_net,
152            u_knots,
153            v_knots,
154            degree_u,
155            degree_v,
156        }
157    }
158    /// Evaluate the NURBS surface at `(u, v)`.
159    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
160        let n_u = self.control_net.len();
161        let n_v = if n_u == 0 {
162            0
163        } else {
164            self.control_net[0].len()
165        };
166        let mut num = [0.0f64; 3];
167        let mut den = 0.0f64;
168        for i in 0..n_u {
169            let bu = NurbsCurve::b_spline_basis(i, self.degree_u, u, &self.u_knots);
170            for j in 0..n_v {
171                let bv = NurbsCurve::b_spline_basis(j, self.degree_v, v, &self.v_knots);
172                let pt = self.control_net[i][j];
173                let w = pt[3];
174                let wb = w * bu * bv;
175                num[0] += wb * pt[0];
176                num[1] += wb * pt[1];
177                num[2] += wb * pt[2];
178                den += wb;
179            }
180        }
181        if den.abs() < 1e-12 {
182            [0.0, 0.0, 0.0]
183        } else {
184            [num[0] / den, num[1] / den, num[2] / den]
185        }
186    }
187}
188/// A tensor-product B-spline surface defined by a rectangular control net.
189///
190/// `control_net[i][j]` is the `(i, j)` control point.  Knot vectors in
191/// each parameter direction are stored separately.
192pub struct BsplineSurface {
193    /// Control net — outer index u, inner index v.
194    pub control_net: Vec<Vec<[f64; 3]>>,
195    /// Knot vector in the u direction.
196    pub u_knots: Vec<f64>,
197    /// Knot vector in the v direction.
198    pub v_knots: Vec<f64>,
199    /// Polynomial degree in u.
200    pub degree_u: usize,
201    /// Polynomial degree in v.
202    pub degree_v: usize,
203}
204impl BsplineSurface {
205    /// Construct a `BsplineSurface`.
206    pub fn new(
207        control_net: Vec<Vec<[f64; 3]>>,
208        u_knots: Vec<f64>,
209        v_knots: Vec<f64>,
210        degree_u: usize,
211        degree_v: usize,
212    ) -> Self {
213        BsplineSurface {
214            control_net,
215            u_knots,
216            v_knots,
217            degree_u,
218            degree_v,
219        }
220    }
221    /// Evaluate the surface at parameter `(u, v)` using tensor-product de Boor.
222    pub fn eval(&self, u: f64, v: f64) -> [f64; 3] {
223        let n_u = self.control_net.len();
224        let n_v = if n_u == 0 {
225            0
226        } else {
227            self.control_net[0].len()
228        };
229        let mut result = [0.0_f64; 3];
230        let mut denom = 0.0_f64;
231        for i in 0..n_u {
232            let bu = NurbsCurve::b_spline_basis(i, self.degree_u, u, &self.u_knots);
233            for j in 0..n_v {
234                let bv = NurbsCurve::b_spline_basis(j, self.degree_v, v, &self.v_knots);
235                let b = bu * bv;
236                let pt = self.control_net[i][j];
237                result[0] += b * pt[0];
238                result[1] += b * pt[1];
239                result[2] += b * pt[2];
240                denom += b;
241            }
242        }
243        if denom.abs() < 1e-14 {
244            [0.0; 3]
245        } else {
246            scale3(result, 1.0 / denom)
247        }
248    }
249    /// Partial derivative with respect to u (finite differences).
250    fn deriv_u(&self, u: f64, v: f64) -> [f64; 3] {
251        let eps = 1e-5_f64;
252        let u0 = (u - eps).max(self.u_knots[0]);
253        let u1 = (u + eps).min(*self.u_knots.last().unwrap_or(&1.0));
254        scale3(sub3(self.eval(u1, v), self.eval(u0, v)), 1.0 / (u1 - u0))
255    }
256    /// Partial derivative with respect to v (finite differences).
257    fn deriv_v(&self, u: f64, v: f64) -> [f64; 3] {
258        let eps = 1e-5_f64;
259        let v0 = (v - eps).max(self.v_knots[0]);
260        let v1 = (v + eps).min(*self.v_knots.last().unwrap_or(&1.0));
261        scale3(sub3(self.eval(u, v1), self.eval(u, v0)), 1.0 / (v1 - v0))
262    }
263    /// Second partial derivative with respect to u (finite differences).
264    fn deriv_uu(&self, u: f64, v: f64) -> [f64; 3] {
265        let eps = 1e-5_f64;
266        let u0 = (u - eps).max(self.u_knots[0]);
267        let u1 = (u + eps).min(*self.u_knots.last().unwrap_or(&1.0));
268        let du0 = self.deriv_u(u0, v);
269        let du1 = self.deriv_u(u1, v);
270        scale3(sub3(du1, du0), 1.0 / (u1 - u0))
271    }
272    /// Second partial derivative with respect to v (finite differences).
273    fn deriv_vv(&self, u: f64, v: f64) -> [f64; 3] {
274        let eps = 1e-5_f64;
275        let v0 = (v - eps).max(self.v_knots[0]);
276        let v1 = (v + eps).min(*self.v_knots.last().unwrap_or(&1.0));
277        let dv0 = self.deriv_v(u, v0);
278        let dv1 = self.deriv_v(u, v1);
279        scale3(sub3(dv1, dv0), 1.0 / (v1 - v0))
280    }
281    /// Mixed partial derivative ∂²S/∂u∂v (finite differences).
282    fn deriv_uv(&self, u: f64, v: f64) -> [f64; 3] {
283        let eps = 1e-5_f64;
284        let v0 = (v - eps).max(self.v_knots[0]);
285        let v1 = (v + eps).min(*self.v_knots.last().unwrap_or(&1.0));
286        let du_v0 = self.deriv_u(u, v0);
287        let du_v1 = self.deriv_u(u, v1);
288        scale3(sub3(du_v1, du_v0), 1.0 / (v1 - v0))
289    }
290    /// Compute Gaussian curvature K and mean curvature H at `(u, v)`.
291    ///
292    /// Uses the first and second fundamental forms computed via finite
293    /// differences of the surface position.
294    ///
295    /// Returns `(K, H)`.  Both values may be `NaN` at degenerate points.
296    pub fn compute_curvature(&self, u: f64, v: f64) -> (f64, f64) {
297        let su = self.deriv_u(u, v);
298        let sv = self.deriv_v(u, v);
299        let suu = self.deriv_uu(u, v);
300        let svv = self.deriv_vv(u, v);
301        let suv = self.deriv_uv(u, v);
302        let normal_raw = cross3(su, sv);
303        let normal_len =
304            (normal_raw[0].powi(2) + normal_raw[1].powi(2) + normal_raw[2].powi(2)).sqrt();
305        if normal_len < 1e-14 {
306            return (0.0, 0.0);
307        }
308        let n = scale3(normal_raw, 1.0 / normal_len);
309        let big_e = dot3(su, su);
310        let big_f = dot3(su, sv);
311        let big_g = dot3(sv, sv);
312        let w = big_e * big_g - big_f * big_f;
313        if w.abs() < 1e-20 {
314            return (0.0, 0.0);
315        }
316        let big_l = dot3(suu, n);
317        let big_m = dot3(suv, n);
318        let big_n = dot3(svv, n);
319        let gaussian = (big_l * big_n - big_m * big_m) / w;
320        let mean = (big_e * big_n - 2.0 * big_f * big_m + big_g * big_l) / (2.0 * w);
321        (gaussian, mean)
322    }
323    /// H-refinement via knot insertion in both parameter directions.
324    ///
325    /// `new_u_knots` and `new_v_knots` are sorted lists of knot values to
326    /// insert (repeated insertions are handled by the Boehm algorithm).
327    ///
328    /// Returns a new `BsplineSurface` with the refined control net.
329    pub fn refine_knots(&self, new_u_knots: &[f64], new_v_knots: &[f64]) -> Self {
330        let refined_u = self.refine_u(new_u_knots);
331        refined_u.refine_v(new_v_knots)
332    }
333    /// Insert a single knot `t` in the u direction using the Boehm algorithm
334    /// applied to each row of the control net independently.
335    fn insert_u_knot(&self, t: f64) -> Self {
336        let n_u = self.control_net.len();
337        let n_v = if n_u == 0 {
338            0
339        } else {
340            self.control_net[0].len()
341        };
342        let p = self.degree_u;
343        let knots = &self.u_knots;
344        let mut k = p;
345        for idx in p..(knots.len() - p - 1) {
346            if knots[idx] <= t && t < knots[idx + 1] {
347                k = idx;
348                break;
349            }
350        }
351        let mut new_knots = knots.clone();
352        new_knots.insert(k + 1, t);
353        let mut new_net: Vec<Vec<[f64; 3]>> = Vec::with_capacity(n_u + 1);
354        for j in 0..n_v {
355            let row: Vec<[f64; 3]> = (0..n_u).map(|i| self.control_net[i][j]).collect();
356            let mut new_row: Vec<[f64; 3]> = Vec::with_capacity(n_u + 1);
357            for i in 0..=(n_u) {
358                if i <= k - p {
359                    new_row.push(row[i]);
360                } else if i <= k {
361                    let alpha = if (knots[i + p] - knots[i]).abs() < 1e-14 {
362                        0.0
363                    } else {
364                        (t - knots[i]) / (knots[i + p] - knots[i])
365                    };
366                    let prev = if i == 0 { [0.0; 3] } else { row[i - 1] };
367                    let curr = row[i.min(row.len() - 1)];
368                    new_row.push(add3(scale3(prev, 1.0 - alpha), scale3(curr, alpha)));
369                } else {
370                    new_row.push(row[(i - 1).min(row.len() - 1)]);
371                }
372            }
373            for i in 0..=(n_u) {
374                while new_net.len() <= i {
375                    new_net.push(Vec::with_capacity(n_v));
376                }
377                new_net[i].push(if i < new_row.len() {
378                    new_row[i]
379                } else {
380                    [0.0; 3]
381                });
382            }
383            let _ = row;
384        }
385        BsplineSurface {
386            control_net: new_net,
387            u_knots: new_knots,
388            v_knots: self.v_knots.clone(),
389            degree_u: self.degree_u,
390            degree_v: self.degree_v,
391        }
392    }
393    /// Refine all u-direction knots in `new_knots` sequentially.
394    fn refine_u(&self, new_knots: &[f64]) -> Self {
395        let mut surf = BsplineSurface {
396            control_net: self.control_net.clone(),
397            u_knots: self.u_knots.clone(),
398            v_knots: self.v_knots.clone(),
399            degree_u: self.degree_u,
400            degree_v: self.degree_v,
401        };
402        for &t in new_knots {
403            surf = surf.insert_u_knot(t);
404        }
405        surf
406    }
407    /// Insert a single knot `t` in the v direction using the Boehm algorithm
408    /// applied to each column of the control net independently.
409    fn insert_v_knot(&self, t: f64) -> Self {
410        let n_u = self.control_net.len();
411        let p = self.degree_v;
412        let knots = &self.v_knots;
413        let mut k = p;
414        for idx in p..(knots.len().saturating_sub(p + 1)) {
415            if knots[idx] <= t && t < knots[idx + 1] {
416                k = idx;
417                break;
418            }
419        }
420        let mut new_knots = knots.clone();
421        new_knots.insert(k + 1, t);
422        let mut new_net: Vec<Vec<[f64; 3]>> = Vec::with_capacity(n_u);
423        for i in 0..n_u {
424            let row = &self.control_net[i];
425            let n_v = row.len();
426            let mut new_row: Vec<[f64; 3]> = Vec::with_capacity(n_v + 1);
427            for j in 0..=(n_v) {
428                if j <= k.saturating_sub(p) {
429                    new_row.push(row[j.min(n_v.saturating_sub(1))]);
430                } else if j <= k {
431                    let alpha = if (knots[j + p] - knots[j]).abs() < 1e-14 {
432                        0.0
433                    } else {
434                        (t - knots[j]) / (knots[j + p] - knots[j])
435                    };
436                    let prev = if j == 0 {
437                        [0.0; 3]
438                    } else {
439                        row[(j - 1).min(n_v - 1)]
440                    };
441                    let curr = row[j.min(n_v - 1)];
442                    new_row.push(add3(scale3(prev, 1.0 - alpha), scale3(curr, alpha)));
443                } else {
444                    new_row.push(row[(j - 1).min(n_v - 1)]);
445                }
446            }
447            new_net.push(new_row);
448        }
449        BsplineSurface {
450            control_net: new_net,
451            u_knots: self.u_knots.clone(),
452            v_knots: new_knots,
453            degree_u: self.degree_u,
454            degree_v: self.degree_v,
455        }
456    }
457    /// Refine all v-direction knots in `new_knots` sequentially.
458    fn refine_v(&self, new_knots: &[f64]) -> Self {
459        let mut surf = BsplineSurface {
460            control_net: self.control_net.clone(),
461            u_knots: self.u_knots.clone(),
462            v_knots: self.v_knots.clone(),
463            degree_u: self.degree_u,
464            degree_v: self.degree_v,
465        };
466        for &t in new_knots {
467            surf = surf.insert_v_knot(t);
468        }
469        surf
470    }
471}
472/// A piecewise cubic Hermite spline interpolating through control points
473/// with user-supplied tangent vectors at each knot.
474///
475/// Guarantees C0 continuity at joints; C1 continuity if tangents are chosen
476/// consistently (e.g., Catmull–Rom style).
477pub struct HermiteCurve {
478    /// Interpolation knots (positions).
479    pub points: Vec<[f64; 3]>,
480    /// Tangent vectors at each knot, matching `points` in length.
481    pub tangents: Vec<[f64; 3]>,
482}
483impl HermiteCurve {
484    /// Construct a Hermite spline from points and tangents.
485    ///
486    /// # Panics
487    /// Panics if `points.len() != tangents.len()` or fewer than 2 points.
488    pub fn new(points: Vec<[f64; 3]>, tangents: Vec<[f64; 3]>) -> Self {
489        assert!(points.len() >= 2, "HermiteCurve needs at least 2 points");
490        assert_eq!(
491            points.len(),
492            tangents.len(),
493            "points and tangents must have the same length"
494        );
495        HermiteCurve { points, tangents }
496    }
497    /// Evaluate the spline at a global parameter `t ∈ [0, n_segments]`.
498    ///
499    /// The integer part selects the segment; the fractional part is the
500    /// local Hermite parameter.
501    pub fn eval(&self, t: f64) -> [f64; 3] {
502        let n = self.points.len() - 1;
503        let t = t.clamp(0.0, n as f64);
504        let seg = (t.floor() as usize).min(n - 1);
505        let u = t - seg as f64;
506        let p0 = self.points[seg];
507        let p1 = self.points[seg + 1];
508        let m0 = self.tangents[seg];
509        let m1 = self.tangents[seg + 1];
510        let h00 = 2.0 * u * u * u - 3.0 * u * u + 1.0;
511        let h10 = u * u * u - 2.0 * u * u + u;
512        let h01 = -2.0 * u * u * u + 3.0 * u * u;
513        let h11 = u * u * u - u * u;
514        let mut result = [0.0f64; 3];
515        for i in 0..3 {
516            result[i] = h00 * p0[i] + h10 * m0[i] + h01 * p1[i] + h11 * m1[i];
517        }
518        result
519    }
520    /// Derivative of the spline at global parameter `t`.
521    pub fn derivative(&self, t: f64) -> [f64; 3] {
522        let n = self.points.len() - 1;
523        let t = t.clamp(0.0, n as f64);
524        let seg = (t.floor() as usize).min(n - 1);
525        let u = t - seg as f64;
526        let p0 = self.points[seg];
527        let p1 = self.points[seg + 1];
528        let m0 = self.tangents[seg];
529        let m1 = self.tangents[seg + 1];
530        let dh00 = 6.0 * u * u - 6.0 * u;
531        let dh10 = 3.0 * u * u - 4.0 * u + 1.0;
532        let dh01 = -6.0 * u * u + 6.0 * u;
533        let dh11 = 3.0 * u * u - 2.0 * u;
534        let mut result = [0.0f64; 3];
535        for i in 0..3 {
536            result[i] = dh00 * p0[i] + dh10 * m0[i] + dh01 * p1[i] + dh11 * m1[i];
537        }
538        result
539    }
540}
541/// A swept surface: a 2-D profile curve (in a local frame) swept along a
542/// spine `BezierCurve`. The profile is evaluated at parameter `v` and
543/// translated along the spine at parameter `u`.
544///
545/// The swept point is:
546/// `spine(u)  +  profile(v)`
547/// (no rotation — this is a translational sweep).
548pub struct SweptSurface {
549    /// Spine curve parameterised by `u`.
550    pub spine: BezierCurve,
551    /// Cross-section profile parameterised by `v`.
552    pub profile: BezierCurve,
553}
554impl SweptSurface {
555    /// Create a new swept surface.
556    pub fn new(spine: BezierCurve, profile: BezierCurve) -> Self {
557        SweptSurface { spine, profile }
558    }
559    /// Evaluate the surface at `(u, v) ∈ [0, 1]²`.
560    ///
561    /// The result is `spine(u) + (profile(v) - profile(0))`.
562    /// This centres the profile at the spine point.
563    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
564        let sp = self.spine.evaluate(u);
565        let prof = self.profile.evaluate(v);
566        let prof0 = self.profile.evaluate(0.0);
567        add3(sp, sub3(prof, prof0))
568    }
569    /// Surface normal at `(u, v)` via finite differences.
570    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
571        let eps = 1e-5_f64;
572        let u0 = (u - eps).max(0.0);
573        let u1 = (u + eps).min(1.0);
574        let v0 = (v - eps).max(0.0);
575        let v1 = (v + eps).min(1.0);
576        let du = scale3(
577            sub3(self.evaluate(u1, v), self.evaluate(u0, v)),
578            1.0 / (u1 - u0),
579        );
580        let dv = scale3(
581            sub3(self.evaluate(u, v1), self.evaluate(u, v0)),
582            1.0 / (v1 - v0),
583        );
584        normalize3(cross3(du, dv))
585    }
586}
587/// A parametric circular arc in 3-D space.
588///
589/// The arc lies in a plane defined by two orthogonal unit vectors `u_axis` and
590/// `v_axis`. The arc sweeps from `start_angle` to `end_angle` (in radians)
591/// around `center` at the given `radius`.
592pub struct Arc {
593    /// Centre point of the arc.
594    pub center: [f64; 3],
595    /// Radius of the arc.
596    pub radius: f64,
597    /// Start angle in radians.
598    pub start_angle: f64,
599    /// End angle in radians.
600    pub end_angle: f64,
601    /// First axis of the arc plane (should be a unit vector).
602    pub u_axis: [f64; 3],
603    /// Second axis of the arc plane (should be a unit vector perpendicular to `u_axis`).
604    pub v_axis: [f64; 3],
605}
606impl Arc {
607    /// Construct an arc in the XY plane.
608    pub fn in_xy_plane(center: [f64; 3], radius: f64, start_angle: f64, end_angle: f64) -> Self {
609        Arc {
610            center,
611            radius,
612            start_angle,
613            end_angle,
614            u_axis: [1.0, 0.0, 0.0],
615            v_axis: [0.0, 1.0, 0.0],
616        }
617    }
618    /// Evaluate the arc at parameter `t ∈ [0, 1]`.
619    ///
620    /// `t = 0` gives `start_angle`, `t = 1` gives `end_angle`.
621    pub fn evaluate(&self, t: f64) -> [f64; 3] {
622        let angle = self.start_angle + t * (self.end_angle - self.start_angle);
623        let (sin_a, cos_a) = angle.sin_cos();
624        add3(
625            self.center,
626            add3(
627                scale3(self.u_axis, self.radius * cos_a),
628                scale3(self.v_axis, self.radius * sin_a),
629            ),
630        )
631    }
632    /// Tangent vector at parameter `t` (not normalised).
633    pub fn tangent(&self, t: f64) -> [f64; 3] {
634        let angle = self.start_angle + t * (self.end_angle - self.start_angle);
635        let da = self.end_angle - self.start_angle;
636        let (sin_a, cos_a) = angle.sin_cos();
637        add3(
638            scale3(self.u_axis, -self.radius * sin_a * da),
639            scale3(self.v_axis, self.radius * cos_a * da),
640        )
641    }
642    /// Arc length (exact: radius × |end_angle − start_angle|).
643    pub fn arc_length(&self) -> f64 {
644        self.radius * (self.end_angle - self.start_angle).abs()
645    }
646    /// Sample the arc at `n` uniformly-spaced parameter values `t ∈ [0, 1]`.
647    pub fn sample(&self, n: usize) -> Vec<[f64; 3]> {
648        if n == 0 {
649            return Vec::new();
650        }
651        (0..n)
652            .map(|i| self.evaluate(i as f64 / (n - 1).max(1) as f64))
653            .collect()
654    }
655}
656/// A lofted surface interpolated between two Bezier curves.
657///
658/// For parameter `(u, v)`, the surface point is:
659/// `lerp( curve_a.evaluate(u),  curve_b.evaluate(u),  v )`.
660pub struct LoftSurface {
661    /// The "bottom" profile curve (at v = 0).
662    pub curve_a: BezierCurve,
663    /// The "top" profile curve (at v = 1).
664    pub curve_b: BezierCurve,
665}
666impl LoftSurface {
667    /// Create a new loft surface from two Bezier curves.
668    pub fn new(curve_a: BezierCurve, curve_b: BezierCurve) -> Self {
669        LoftSurface { curve_a, curve_b }
670    }
671    /// Evaluate the lofted surface at `(u, v) ∈ [0, 1]²`.
672    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
673        let pa = self.curve_a.evaluate(u);
674        let pb = self.curve_b.evaluate(u);
675        lerp3(pa, pb, v)
676    }
677    /// Surface normal at `(u, v)` via finite differences: ∂P/∂u × ∂P/∂v.
678    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
679        let eps = 1e-5_f64;
680        let u0 = (u - eps).max(0.0);
681        let u1 = (u + eps).min(1.0);
682        let v0 = (v - eps).max(0.0);
683        let v1 = (v + eps).min(1.0);
684        let du = scale3(
685            sub3(self.evaluate(u1, v), self.evaluate(u0, v)),
686            1.0 / (u1 - u0),
687        );
688        let dv = scale3(
689            sub3(self.evaluate(u, v1), self.evaluate(u, v0)),
690            1.0 / (v1 - v0),
691        );
692        normalize3(cross3(du, dv))
693    }
694    /// Sample the surface as a `nu × nv` grid, returned row-major.
695    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
696        (0..nu)
697            .map(|i| {
698                let u = i as f64 / (nu - 1).max(1) as f64;
699                (0..nv)
700                    .map(|j| {
701                        let v = j as f64 / (nv - 1).max(1) as f64;
702                        self.evaluate(u, v)
703                    })
704                    .collect()
705            })
706            .collect()
707    }
708}
709/// A tensor-product Bezier surface (typically bicubic: rows = cols = 4).
710pub struct BezierSurface {
711    /// Control grid, indexed as `control_grid[row][col]`.
712    pub control_grid: Vec<Vec<[f64; 3]>>,
713    /// Number of rows in the control grid.
714    pub rows: usize,
715    /// Number of columns in the control grid.
716    pub cols: usize,
717}
718impl BezierSurface {
719    /// Create a bicubic (4×4) Bezier surface from a control grid.
720    pub fn new_bicubic(control_grid: Vec<Vec<[f64; 3]>>) -> Self {
721        let rows = control_grid.len();
722        assert!(rows > 0, "control grid must have at least one row");
723        let cols = control_grid[0].len();
724        assert!(cols > 0, "control grid must have at least one column");
725        BezierSurface {
726            control_grid,
727            rows,
728            cols,
729        }
730    }
731    /// Evaluate the surface at `(u, v) ∈ [0, 1]²`.
732    ///
733    /// Apply de Casteljau row-wise (over u) to get `rows` intermediate points,
734    /// then apply de Casteljau over those points along v.
735    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
736        let row_pts: Vec<[f64; 3]> = self
737            .control_grid
738            .iter()
739            .map(|row| {
740                let curve = BezierCurve::new(row.clone());
741                curve.evaluate(u)
742            })
743            .collect();
744        let col_curve = BezierCurve::new(row_pts);
745        col_curve.evaluate(v)
746    }
747    /// Surface normal at `(u, v)`: ∂P/∂u × ∂P/∂v, normalized.
748    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
749        let eps = 1e-5_f64;
750        let du = {
751            let u0 = (u - eps).max(0.0);
752            let u1 = (u + eps).min(1.0);
753            let p0 = self.evaluate(u0, v);
754            let p1 = self.evaluate(u1, v);
755            scale3(sub3(p1, p0), 1.0 / (u1 - u0))
756        };
757        let dv = {
758            let v0 = (v - eps).max(0.0);
759            let v1 = (v + eps).min(1.0);
760            let p0 = self.evaluate(u, v0);
761            let p1 = self.evaluate(u, v1);
762            scale3(sub3(p1, p0), 1.0 / (v1 - v0))
763        };
764        normalize3(cross3(du, dv))
765    }
766}
767/// A non-uniform B-spline curve defined by a knot vector and control points.
768///
769/// Uses the Cox–de Boor recursion for evaluation and supports knot insertion.
770pub struct BSpline {
771    /// Control points.
772    pub control_points: Vec<[f64; 3]>,
773    /// Knot vector (must have length = `control_points.len() + degree + 1`).
774    pub knot_vector: Vec<f64>,
775    /// Polynomial degree.
776    pub degree: usize,
777}
778impl BSpline {
779    /// Construct a new B-spline curve.
780    ///
781    /// # Panics
782    /// Panics if `knot_vector.len() != control_points.len() + degree + 1`.
783    pub fn new(degree: usize, control_points: Vec<[f64; 3]>, knot_vector: Vec<f64>) -> Self {
784        assert_eq!(
785            knot_vector.len(),
786            control_points.len() + degree + 1,
787            "knot_vector length must equal n_ctrl + degree + 1"
788        );
789        BSpline {
790            control_points,
791            knot_vector,
792            degree,
793        }
794    }
795    /// Create a clamped uniform B-spline (interpolates first and last control points).
796    pub fn clamped_uniform(degree: usize, control_points: Vec<[f64; 3]>) -> Self {
797        let n = control_points.len();
798        let m = n + degree + 1;
799        let interior = m - 2 * (degree + 1);
800        let mut knots = Vec::with_capacity(m);
801        knots.extend(std::iter::repeat_n(0.0_f64, degree + 1));
802        for i in 1..=(interior) {
803            knots.push(i as f64 / (interior + 1) as f64);
804        }
805        knots.extend(std::iter::repeat_n(1.0_f64, degree + 1));
806        Self::new(degree, control_points, knots)
807    }
808    /// Evaluate the B-spline at parameter `t` using the de Boor algorithm.
809    pub fn eval(&self, t: f64) -> [f64; 3] {
810        let p = self.degree;
811        let knots = &self.knot_vector;
812        let t = t.clamp(
813            *knots.first().unwrap_or(&0.0),
814            *knots.last().unwrap_or(&1.0),
815        );
816        let k = self.find_span(t);
817        let mut d: Vec<[f64; 3]> = (0..=(p)).map(|j| self.control_points[k - p + j]).collect();
818        for r in 1..=(p) {
819            for j in (r..=(p)).rev() {
820                let i = k - p + j;
821                let denom = knots[i + p - r + 1] - knots[i];
822                let alpha = if denom.abs() < 1e-12 {
823                    0.0
824                } else {
825                    (t - knots[i]) / denom
826                };
827                d[j] = lerp3(d[j - 1], d[j], alpha);
828            }
829        }
830        d[p]
831    }
832    /// Find the knot span index for `t` (index k such that knots\[k\] <= t < knots\[k+1\]).
833    fn find_span(&self, t: f64) -> usize {
834        let n = self.control_points.len();
835        let p = self.degree;
836        let knots = &self.knot_vector;
837        if (t - knots[n]).abs() < 1e-12 {
838            let mut k = n - 1;
839            while k > p && (knots[k] - knots[n]).abs() < 1e-12 {
840                k -= 1;
841            }
842            return k;
843        }
844        let mut lo = p;
845        let mut hi = n;
846        let mut mid = (lo + hi) / 2;
847        while t < knots[mid] || t >= knots[mid + 1] {
848            if t < knots[mid] {
849                hi = mid;
850            } else {
851                lo = mid;
852            }
853            mid = (lo + hi) / 2;
854            if lo + 1 >= hi {
855                break;
856            }
857        }
858        mid
859    }
860    /// Insert knot `t_new` into the knot vector (Boehm's algorithm).
861    ///
862    /// Returns a new `BSpline` with one additional control point and knot.
863    pub fn insert_knot(&self, t_new: f64) -> Self {
864        let p = self.degree;
865        let knots = &self.knot_vector;
866        let pts = &self.control_points;
867        let n = pts.len();
868        let k = self.find_span(t_new);
869        let mut new_knots = Vec::with_capacity(knots.len() + 1);
870        new_knots.extend_from_slice(&knots[..=k]);
871        new_knots.push(t_new);
872        new_knots.extend_from_slice(&knots[k + 1..]);
873        let mut new_pts: Vec<[f64; 3]> = Vec::with_capacity(n + 1);
874        for i in 0..=n {
875            if i <= k - p {
876                new_pts.push(pts[i]);
877            } else if i <= k {
878                let alpha_denom = knots[i + p] - knots[i];
879                let alpha = if alpha_denom.abs() < 1e-12 {
880                    1.0
881                } else {
882                    (t_new - knots[i]) / alpha_denom
883                };
884                let prev = if i == 0 { [0.0; 3] } else { pts[i - 1] };
885                let curr = pts[i];
886                new_pts.push(lerp3(prev, curr, alpha));
887            } else {
888                new_pts.push(pts[i - 1]);
889            }
890        }
891        BSpline {
892            control_points: new_pts,
893            knot_vector: new_knots,
894            degree: p,
895        }
896    }
897}
898impl BSpline {
899    /// Insert knot `t` in place, modifying this B-spline (Boehm's algorithm).
900    pub fn insert_knot_inplace(&mut self, t: f64) {
901        let new_spline = self.insert_knot(t);
902        self.control_points = new_spline.control_points;
903        self.knot_vector = new_spline.knot_vector;
904    }
905}
906impl BSpline {
907    /// Derivative of the B-spline at parameter `t` using finite differences.
908    pub fn derivative(&self, t: f64) -> [f64; 3] {
909        let eps = 1e-5_f64;
910        let knots = &self.knot_vector;
911        let t_min = *knots.first().unwrap_or(&0.0);
912        let t_max = *knots.last().unwrap_or(&1.0);
913        let t0 = (t - eps).max(t_min);
914        let t1 = (t + eps).min(t_max);
915        let p0 = self.eval(t0);
916        let p1 = self.eval(t1);
917        scale3(sub3(p1, p0), 1.0 / (t1 - t0))
918    }
919    /// Arc length approximation by sampling `n_samples` parameter values.
920    pub fn arc_length(&self, n_samples: usize) -> f64 {
921        assert!(n_samples >= 2, "arc_length needs at least 2 samples");
922        let knots = &self.knot_vector;
923        let t_min = *knots.first().unwrap_or(&0.0);
924        let t_max = *knots.last().unwrap_or(&1.0);
925        let mut len = 0.0_f64;
926        let mut prev = self.eval(t_min);
927        for k in 1..=n_samples {
928            let t = t_min + (t_max - t_min) * k as f64 / n_samples as f64;
929            let curr = self.eval(t);
930            len += dist3(prev, curr);
931            prev = curr;
932        }
933        len
934    }
935}
936/// A Bezier curve of degree `n = control_points.len() - 1`.
937pub struct BezierCurve {
938    /// Control points of the curve.
939    pub control_points: Vec<[f64; 3]>,
940}
941impl BezierCurve {
942    /// Create a new Bezier curve from a list of control points.
943    pub fn new(points: Vec<[f64; 3]>) -> Self {
944        assert!(
945            !points.is_empty(),
946            "BezierCurve needs at least one control point"
947        );
948        BezierCurve {
949            control_points: points,
950        }
951    }
952    /// Degree of the curve (= number of control points − 1).
953    pub fn degree(&self) -> usize {
954        self.control_points.len() - 1
955    }
956    /// Evaluate the curve at parameter `t ∈ [0, 1]` using de Casteljau's algorithm.
957    pub fn evaluate(&self, t: f64) -> [f64; 3] {
958        let mut pts = self.control_points.clone();
959        let n = pts.len();
960        for r in 1..n {
961            for i in 0..(n - r) {
962                pts[i] = lerp3(pts[i], pts[i + 1], t);
963            }
964        }
965        pts[0]
966    }
967    /// Tangent vector at `t` — the derivative Bezier of degree `n-1`.
968    pub fn derivative(&self, t: f64) -> [f64; 3] {
969        let n = self.degree();
970        if n == 0 {
971            return [0.0, 0.0, 0.0];
972        }
973        let deriv_pts: Vec<[f64; 3]> = (0..n)
974            .map(|i| {
975                scale3(
976                    sub3(self.control_points[i + 1], self.control_points[i]),
977                    n as f64,
978                )
979            })
980            .collect();
981        let deriv_curve = BezierCurve::new(deriv_pts);
982        deriv_curve.evaluate(t)
983    }
984    /// Approximate arc length by sampling `n_samples` equidistant parameter values.
985    pub fn arc_length(&self, n_samples: usize) -> f64 {
986        assert!(n_samples >= 2, "arc_length needs at least 2 samples");
987        let mut length = 0.0;
988        let mut prev = self.evaluate(0.0);
989        for k in 1..=n_samples {
990            let t = k as f64 / n_samples as f64;
991            let curr = self.evaluate(t);
992            length += dist3(prev, curr);
993            prev = curr;
994        }
995        length
996    }
997}
998impl BezierCurve {
999    /// Alias for `evaluate(t)` — de Casteljau evaluation at `t ∈ [0, 1]`.
1000    pub fn eval(&self, t: f64) -> [f64; 3] {
1001        self.evaluate(t)
1002    }
1003}
1004/// The Frenet–Serret frame at a point on a curve.
1005#[derive(Debug, Clone)]
1006pub struct FrenetFrame {
1007    /// Unit tangent vector T = C'(t) / |C'(t)|.
1008    pub tangent: [f64; 3],
1009    /// Unit normal vector N = T'(t) / |T'(t)|.
1010    pub normal: [f64; 3],
1011    /// Binormal vector B = T × N.
1012    pub binormal: [f64; 3],
1013}
1014impl FrenetFrame {
1015    /// Compute the Frenet frame at parameter `t` for a curve whose tangent and
1016    /// second derivative are provided as closures.
1017    ///
1018    /// `tangent_fn(t)` should return `C'(t)` and `dtangent_fn(t)` should
1019    /// return `C''(t)`.  Both are then normalized internally.
1020    pub fn compute(
1021        tangent_fn: impl Fn(f64) -> [f64; 3],
1022        dtangent_fn: impl Fn(f64) -> [f64; 3],
1023        t: f64,
1024    ) -> Self {
1025        let tan_raw = tangent_fn(t);
1026        let tangent = normalize3(tan_raw);
1027        let dtan = dtangent_fn(t);
1028        let proj = dot3(dtan, tangent);
1029        let normal_raw = sub3(dtan, scale3(tangent, proj));
1030        let normal = if dot3(normal_raw, normal_raw).sqrt() < 1e-12 {
1031            arbitrary_perp(tangent)
1032        } else {
1033            normalize3(normal_raw)
1034        };
1035        let binormal = normalize3(cross3(tangent, normal));
1036        FrenetFrame {
1037            tangent,
1038            normal,
1039            binormal,
1040        }
1041    }
1042    /// Compute the Frenet frame for a `BezierCurve` at `t` using finite
1043    /// differences for the second derivative.
1044    pub fn from_bezier(curve: &BezierCurve, t: f64) -> Self {
1045        let eps = 1e-5_f64;
1046        let tangent_fn = |u: f64| curve.derivative(u);
1047        let dtangent_fn = |u: f64| {
1048            let t0 = (u - eps).max(0.0);
1049            let t1 = (u + eps).min(1.0);
1050            let d = sub3(curve.derivative(t1), curve.derivative(t0));
1051            scale3(d, 1.0 / (t1 - t0))
1052        };
1053        Self::compute(tangent_fn, dtangent_fn, t)
1054    }
1055}
1056/// A surface of revolution: a profile curve in the XZ plane rotated around
1057/// the Z axis.
1058///
1059/// `u ∈ [0, 1]` sweeps the full 2π revolution (or a fraction thereof),
1060/// `v ∈ [0, 1]` traverses the profile curve.
1061pub struct RevolutionSurface {
1062    /// Profile curve (in XZ plane: profile\[.\]\[0\] = X offset, profile\[.\]\[2\] = Z height).
1063    pub profile: BezierCurve,
1064    /// Total sweep angle in radians (default 2Ï€ for a full revolution).
1065    pub sweep_angle: f64,
1066}
1067impl RevolutionSurface {
1068    /// Create a surface of revolution with a full 2Ï€ sweep.
1069    pub fn new(profile: BezierCurve) -> Self {
1070        RevolutionSurface {
1071            profile,
1072            sweep_angle: std::f64::consts::TAU,
1073        }
1074    }
1075    /// Create a surface of revolution with an explicit sweep angle.
1076    pub fn with_angle(profile: BezierCurve, sweep_angle: f64) -> Self {
1077        RevolutionSurface {
1078            profile,
1079            sweep_angle,
1080        }
1081    }
1082    /// Evaluate at `(u, v)`:
1083    /// - `v` selects a point on the profile `(r, 0, z)`,
1084    /// - `u` rotates it by `u * sweep_angle` around the Z axis.
1085    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
1086        let p = self.profile.evaluate(v);
1087        let r = p[0];
1088        let z = p[2];
1089        let angle = u * self.sweep_angle;
1090        let (sin_a, cos_a) = angle.sin_cos();
1091        [r * cos_a, r * sin_a, z]
1092    }
1093    /// Surface normal at `(u, v)` via finite differences.
1094    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
1095        let eps = 1e-5_f64;
1096        let u0 = (u - eps).max(0.0);
1097        let u1 = (u + eps).min(1.0);
1098        let v0 = (v - eps).max(0.0);
1099        let v1 = (v + eps).min(1.0);
1100        let du = scale3(
1101            sub3(self.evaluate(u1, v), self.evaluate(u0, v)),
1102            1.0 / (u1 - u0),
1103        );
1104        let dv = scale3(
1105            sub3(self.evaluate(u, v1), self.evaluate(u, v0)),
1106            1.0 / (v1 - v0),
1107        );
1108        normalize3(cross3(du, dv))
1109    }
1110    /// Sample the surface as a `nu × nv` grid.
1111    pub fn sample_grid(&self, nu: usize, nv: usize) -> Vec<Vec<[f64; 3]>> {
1112        (0..nu)
1113            .map(|i| {
1114                let u = i as f64 / (nu - 1).max(1) as f64;
1115                (0..nv)
1116                    .map(|j| {
1117                        let v = j as f64 / (nv - 1).max(1) as f64;
1118                        self.evaluate(u, v)
1119                    })
1120                    .collect()
1121            })
1122            .collect()
1123    }
1124}
1125/// A Non-Uniform Rational B-Spline (NURBS) curve.
1126pub struct NurbsCurve {
1127    /// Control points.
1128    pub control_points: Vec<[f64; 3]>,
1129    /// Rational weights, one per control point.
1130    pub weights: Vec<f64>,
1131    /// Knot vector.
1132    pub knot_vector: Vec<f64>,
1133    /// Degree of the B-spline basis.
1134    pub degree: usize,
1135}
1136impl NurbsCurve {
1137    /// Construct a NURBS curve.
1138    pub fn new(
1139        degree: usize,
1140        control_points: Vec<[f64; 3]>,
1141        weights: Vec<f64>,
1142        knot_vector: Vec<f64>,
1143    ) -> Self {
1144        assert_eq!(
1145            control_points.len(),
1146            weights.len(),
1147            "control_points and weights must have the same length"
1148        );
1149        NurbsCurve {
1150            control_points,
1151            weights,
1152            knot_vector,
1153            degree,
1154        }
1155    }
1156    /// Cox–de Boor B-spline basis function N_{i,p}(t).
1157    pub fn b_spline_basis(i: usize, p: usize, t: f64, knots: &[f64]) -> f64 {
1158        if p == 0 {
1159            let last = *knots.last().unwrap_or(&0.0);
1160            if knots[i] <= t && t < knots[i + 1] {
1161                return 1.0;
1162            }
1163            if (t - last).abs() < 1e-12 && (knots[i + 1] - last).abs() < 1e-12 {
1164                return 1.0;
1165            }
1166            return 0.0;
1167        }
1168        let denom1 = knots[i + p] - knots[i];
1169        let denom2 = knots[i + p + 1] - knots[i + 1];
1170        let term1 = if denom1.abs() < 1e-12 {
1171            0.0
1172        } else {
1173            (t - knots[i]) / denom1 * Self::b_spline_basis(i, p - 1, t, knots)
1174        };
1175        let term2 = if denom2.abs() < 1e-12 {
1176            0.0
1177        } else {
1178            (knots[i + p + 1] - t) / denom2 * Self::b_spline_basis(i + 1, p - 1, t, knots)
1179        };
1180        term1 + term2
1181    }
1182    /// Find the knot span index for parameter `t` using binary search.
1183    ///
1184    /// Returns index `k` such that `knots[k] <= t < knots[k+1]`.
1185    pub fn find_span(n: usize, p: usize, t: f64, knots: &[f64]) -> usize {
1186        if (t - knots[n + 1]).abs() < 1e-12 {
1187            return n;
1188        }
1189        let mut lo = p;
1190        let mut hi = n + 1;
1191        let mut mid = (lo + hi) / 2;
1192        while t < knots[mid] || t >= knots[mid + 1] {
1193            if t < knots[mid] {
1194                hi = mid;
1195            } else {
1196                lo = mid;
1197            }
1198            mid = (lo + hi) / 2;
1199        }
1200        mid
1201    }
1202    /// Evaluate the NURBS curve at parameter `t`.
1203    ///
1204    /// Uses the rational formula: `Σ(w_i · N_{i,p}(t) · P_i) / Σ(w_i · N_{i,p}(t))`.
1205    pub fn evaluate(&self, t: f64) -> [f64; 3] {
1206        let n = self.control_points.len();
1207        let p = self.degree;
1208        let knots = &self.knot_vector;
1209        let mut numerator = [0.0_f64; 3];
1210        let mut denominator = 0.0_f64;
1211        for i in 0..n {
1212            let basis = Self::b_spline_basis(i, p, t, knots);
1213            let w = self.weights[i];
1214            let wn = w * basis;
1215            numerator[0] += wn * self.control_points[i][0];
1216            numerator[1] += wn * self.control_points[i][1];
1217            numerator[2] += wn * self.control_points[i][2];
1218            denominator += wn;
1219        }
1220        if denominator.abs() < 1e-12 {
1221            [0.0, 0.0, 0.0]
1222        } else {
1223            scale3(numerator, 1.0 / denominator)
1224        }
1225    }
1226}
1227impl NurbsCurve {
1228    /// Compute the arc length of the NURBS curve over `[t_start, t_end]`
1229    /// using `n_samples` trapezoid-rule segments.
1230    pub fn arc_length(&self, t_start: f64, t_end: f64, n_samples: usize) -> f64 {
1231        let n = n_samples.max(2);
1232        let mut total = 0.0_f64;
1233        let mut prev = self.evaluate(t_start);
1234        for k in 1..=n {
1235            let t = t_start + (t_end - t_start) * k as f64 / n as f64;
1236            let curr = self.evaluate(t);
1237            total += dist3(prev, curr);
1238            prev = curr;
1239        }
1240        total
1241    }
1242    /// Build an arc-length reparametrization table with `n_samples` samples.
1243    ///
1244    /// Returns a pair of `Vec`f64` — `(arc_lengths, params)` — where
1245    /// `arc_lengths\[i\]` is the arc length at parameter `params\[i\]`.  The
1246    /// parameter range is taken from the knot vector.
1247    pub fn arc_length_table(&self, n_samples: usize) -> (Vec<f64>, Vec<f64>) {
1248        let n = n_samples.max(2);
1249        let t_min = *self.knot_vector.first().unwrap_or(&0.0);
1250        let t_max = *self.knot_vector.last().unwrap_or(&1.0);
1251        let mut params = Vec::with_capacity(n + 1);
1252        let mut arc_lengths = Vec::with_capacity(n + 1);
1253        let mut total = 0.0_f64;
1254        let mut prev = self.evaluate(t_min);
1255        params.push(t_min);
1256        arc_lengths.push(0.0);
1257        for k in 1..=n {
1258            let t = t_min + (t_max - t_min) * k as f64 / n as f64;
1259            let curr = self.evaluate(t);
1260            total += dist3(prev, curr);
1261            prev = curr;
1262            params.push(t);
1263            arc_lengths.push(total);
1264        }
1265        (arc_lengths, params)
1266    }
1267    /// Invert the arc-length table: find the parameter `t` corresponding to
1268    /// a given arc length `s` by linear interpolation.
1269    fn invert_arc_length_table(s: f64, arc_lengths: &[f64], params: &[f64]) -> f64 {
1270        if s <= arc_lengths[0] {
1271            return params[0];
1272        }
1273        let last = *arc_lengths.last().expect("collection should not be empty");
1274        if s >= last {
1275            return *params.last().expect("collection should not be empty");
1276        }
1277        let mut lo = 0_usize;
1278        let mut hi = arc_lengths.len() - 1;
1279        while hi - lo > 1 {
1280            let mid = (lo + hi) / 2;
1281            if arc_lengths[mid] <= s {
1282                lo = mid;
1283            } else {
1284                hi = mid;
1285            }
1286        }
1287        let t_lo = params[lo];
1288        let t_hi = params[hi];
1289        let s_lo = arc_lengths[lo];
1290        let s_hi = arc_lengths[hi];
1291        let frac = if (s_hi - s_lo).abs() < 1e-14 {
1292            0.0
1293        } else {
1294            (s - s_lo) / (s_hi - s_lo)
1295        };
1296        t_lo + frac * (t_hi - t_lo)
1297    }
1298    /// Reparametrize by arc length: sample the curve at `n_out` equally-spaced
1299    /// arc-length values and return the corresponding 3-D positions.
1300    ///
1301    /// Internally builds an arc-length table with `n_table` entries.
1302    pub fn compute_arc_length_reparametrize(&self, n_table: usize, n_out: usize) -> Vec<[f64; 3]> {
1303        let (arc_lengths, params) = self.arc_length_table(n_table);
1304        let total_len = *arc_lengths.last().unwrap_or(&0.0);
1305        if total_len < 1e-14 || n_out == 0 {
1306            return Vec::new();
1307        }
1308        (0..n_out)
1309            .map(|k| {
1310                let s = total_len * k as f64 / (n_out - 1).max(1) as f64;
1311                let t = Self::invert_arc_length_table(s, &arc_lengths, &params);
1312                self.evaluate(t)
1313            })
1314            .collect()
1315    }
1316}
1317/// A Catmull–Rom spline with centripetal parameterization (α = 0.5).
1318///
1319/// Interpolates through all control points.  Tangents are computed
1320/// automatically using the Catmull–Rom formula so the spline is C1.
1321pub struct CatmullRomSpline {
1322    /// Interpolation points.
1323    pub points: Vec<[f64; 3]>,
1324    /// Knot sequence for centripetal parameterization (α = 0.5).
1325    pub knots: Vec<f64>,
1326}
1327impl CatmullRomSpline {
1328    /// Construct a centripetal Catmull–Rom spline from the given points.
1329    ///
1330    /// # Panics
1331    /// Panics if fewer than 2 points are supplied.
1332    pub fn new(points: Vec<[f64; 3]>) -> Self {
1333        assert!(
1334            points.len() >= 2,
1335            "CatmullRomSpline needs at least 2 points"
1336        );
1337        let knots = Self::centripetal_knots(&points);
1338        CatmullRomSpline { points, knots }
1339    }
1340    /// Centripetal knot sequence: t_0 = 0, t_{i+1} = t_i + |P_{i+1} - P_i|^0.5.
1341    fn centripetal_knots(pts: &[[f64; 3]]) -> Vec<f64> {
1342        let mut t = vec![0.0f64];
1343        for i in 1..pts.len() {
1344            let d = dist3(pts[i], pts[i - 1]);
1345            t.push(t[i - 1] + d.sqrt().max(1e-12));
1346        }
1347        let last = *t.last().expect("collection should not be empty");
1348        if last > 1e-12 {
1349            t.iter_mut().for_each(|v| *v /= last);
1350        }
1351        t
1352    }
1353    /// Evaluate the spline at global parameter `t ∈ \[0, 1\]`.
1354    pub fn eval(&self, t: f64) -> [f64; 3] {
1355        let pts = &self.points;
1356        let knots = &self.knots;
1357        let n = pts.len();
1358        let t = t.clamp(0.0, 1.0);
1359        let mut seg = 0usize;
1360        for i in 0..n - 1 {
1361            if t <= knots[i + 1] + 1e-12 {
1362                seg = i;
1363                break;
1364            }
1365            seg = n - 2;
1366        }
1367        let i0 = if seg == 0 { 0 } else { seg - 1 };
1368        let i1 = seg;
1369        let i2 = (seg + 1).min(n - 1);
1370        let i3 = (seg + 2).min(n - 1);
1371        let t0 = knots[i0];
1372        let t1 = knots[i1];
1373        let t2 = knots[i2];
1374        let t3 = knots[i3];
1375        Self::barry_goldman(pts[i0], pts[i1], pts[i2], pts[i3], t0, t1, t2, t3, t)
1376    }
1377    /// Barry–Goldman non-uniform Catmull–Rom evaluation.
1378    fn barry_goldman(
1379        p0: [f64; 3],
1380        p1: [f64; 3],
1381        p2: [f64; 3],
1382        p3: [f64; 3],
1383        t0: f64,
1384        t1: f64,
1385        t2: f64,
1386        t3: f64,
1387        t: f64,
1388    ) -> [f64; 3] {
1389        fn interp(a: [f64; 3], b: [f64; 3], ta: f64, tb: f64, t: f64) -> [f64; 3] {
1390            let span = tb - ta;
1391            if span.abs() < 1e-12 {
1392                return a;
1393            }
1394            lerp3(a, b, (t - ta) / span)
1395        }
1396        let a1 = interp(p0, p1, t0, t1, t);
1397        let a2 = interp(p1, p2, t1, t2, t);
1398        let a3 = interp(p2, p3, t2, t3, t);
1399        let b1 = interp(a1, a2, t0, t2, t);
1400        let b2 = interp(a2, a3, t1, t3, t);
1401        interp(b1, b2, t1, t2, t)
1402    }
1403}