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