Skip to main content

brep_kernel/geometry/
curve.rs

1use crate::Vec3;
2use serde::{Deserialize, Serialize};
3
4const EPS: f64 = 1e-12;
5
6/// Knot IDENTITY tolerance (parameter-space): two knot parameters within this
7/// absolute band are treated as the SAME knot for snapping — insert_knot,
8/// `NurbsCurve::split`, monotonicity and clamp checks.  This is the single
9/// source for the "absolute 1e-9 knot tolerance" that `split` enforces and
10/// that imprint's trim guards mirror.  Distinct in PURPOSE (not just value)
11/// from [`KNOT_DEDUP_EPS`]; the two are deliberately NOT unified.
12pub const KNOT_IDENTITY_TOL: f64 = 1e-9;
13
14/// Numerical knot DEDUP epsilon (parameter-space): the tighter floor used when
15/// *counting distinct* knots (SSI/CSI `interior_knot_count`) or comparing whole
16/// knot vectors for exact reconstruction (analytic surface recognition).  This
17/// answers "are these two knot floats the same value?", NOT "are these the same
18/// knot for snapping?" — so it stays at 1e-12 and must NOT be unified with the
19/// looser [`KNOT_IDENTITY_TOL`].
20pub const KNOT_DEDUP_EPS: f64 = 1e-12;
21
22/// Count distinct knots strictly inside a valid knot vector's active domain.
23/// Multiplicities and values within [`KNOT_DEDUP_EPS`] of the domain ends are
24/// ignored. Used to choose sampling density for intersections and containment.
25pub(crate) fn interior_knot_count(knots: &[f64], degree: usize) -> usize {
26    distinct_interior_knots(knots, degree).count()
27}
28
29fn distinct_interior_knots(knots: &[f64], degree: usize) -> impl Iterator<Item = f64> + '_ {
30    let start = knots[degree];
31    let end = knots[knots.len() - 1 - degree];
32    let mut previous = None;
33    knots.iter().copied().filter(move |&knot| {
34        if knot <= start + KNOT_DEDUP_EPS || knot >= end - KNOT_DEDUP_EPS {
35            return false;
36        }
37        if previous.is_none_or(|value: f64| (value - knot).abs() > KNOT_DEDUP_EPS) {
38            previous = Some(knot);
39            true
40        } else {
41            false
42        }
43    })
44}
45
46#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
47pub struct Vec4 {
48    pub x: f64,
49    pub y: f64,
50    pub z: f64,
51    pub w: f64,
52}
53
54/// Construct a line in the parameter plane (z = 0).
55pub(crate) fn parameter_line(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
56    make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
57}
58
59/// Project a curve onto plane axes while preserving its knots and rational weights.
60pub(crate) fn curve_to_plane_parameters(
61    curve: &NurbsCurve,
62    origin: Vec3,
63    x_axis: Vec3,
64    y_axis: Vec3,
65) -> Result<NurbsCurve, String> {
66    NurbsCurve::new(
67        curve.degree,
68        curve.knots.clone(),
69        curve
70            .control_points
71            .iter()
72            .map(|point| {
73                let euclidean = Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w);
74                let delta = euclidean.sub(origin);
75                Vec4 {
76                    x: delta.dot(x_axis) * point.w,
77                    y: delta.dot(y_axis) * point.w,
78                    z: 0.0,
79                    w: point.w,
80                }
81            })
82            .collect(),
83    )
84}
85
86impl Vec4 {
87    pub fn from_point(point: Vec3, weight: f64) -> Self {
88        Self {
89            x: point.x * weight,
90            y: point.y * weight,
91            z: point.z * weight,
92            w: weight,
93        }
94    }
95
96    pub(crate) fn add(self, rhs: Self) -> Self {
97        Self {
98            x: self.x + rhs.x,
99            y: self.y + rhs.y,
100            z: self.z + rhs.z,
101            w: self.w + rhs.w,
102        }
103    }
104
105    pub(crate) fn scale(self, factor: f64) -> Self {
106        Self {
107            x: self.x * factor,
108            y: self.y * factor,
109            z: self.z * factor,
110            w: self.w * factor,
111        }
112    }
113
114    pub(crate) fn point(self) -> Result<Vec3, String> {
115        if self.w.abs() <= EPS {
116            return Err("cannot project a homogeneous point with zero weight".into());
117        }
118        Ok(Vec3::new(self.x / self.w, self.y / self.w, self.z / self.w))
119    }
120}
121
122/// Largest degree served by the allocation-free stack-array basis engine.
123/// Higher degrees (rare: only externally imported geometry) fall back to the
124/// heap-allocating `KnotVector` path.
125pub(crate) const MAX_STACK_DEGREE: usize = 7;
126pub(crate) const MAX_STACK_ORDER: usize = MAX_STACK_DEGREE + 1;
127
128/// `BREP_DEBUG_KNOTS=1` diagnostic for a rejected knot vector: dumps the whole
129/// vector (plus a backtrace) to stderr and appends it to the returned message,
130/// so a knot failure buried behind a `Result` that a caller only prints —
131/// STEP import's `first_error`, say — still names the offending construction
132/// site. Off by default; the message stays byte-identical without the var.
133fn knot_reject(reason: &str, knots: &[f64], degree: usize) -> String {
134    if std::env::var("BREP_DEBUG_KNOTS").is_err() {
135        return reason.to_string();
136    }
137    let mut worst_descent = f64::NEG_INFINITY;
138    let mut worst_index = 0usize;
139    for (index, pair) in knots.windows(2).enumerate() {
140        let descent = pair[0] - pair[1];
141        if descent > worst_descent {
142            worst_descent = descent;
143            worst_index = index;
144        }
145    }
146    let detail = format!(
147        "{reason} [degree={degree} count={} worst_descent={worst_descent:.6e} at index \
148         {worst_index} knots={knots:?}]",
149        knots.len()
150    );
151    eprintln!(
152        "KNOT-REJECT {detail}\n{}",
153        std::backtrace::Backtrace::force_capture()
154    );
155    detail
156}
157
158/// Validation shared by `KnotVector::new` and the lazily-validated
159/// curve/surface types. Must stay the single source of truth so cached
160/// validation and eager validation reject exactly the same inputs.
161pub(crate) fn validate_knots(knots: &[f64], degree: usize) -> Result<(), String> {
162    if degree < 1 {
163        return Err("KnotVector: degree must be >= 1".into());
164    }
165    if knots.len() < 2 * (degree + 1) {
166        return Err(format!(
167            "KnotVector: need at least {} knots for degree {}, got {}",
168            2 * (degree + 1),
169            degree,
170            knots.len()
171        ));
172    }
173    if knots.iter().any(|value| !value.is_finite()) {
174        return Err("KnotVector: knots must be finite".into());
175    }
176    if knots
177        .windows(2)
178        .any(|pair| pair[1] < pair[0] - KNOT_IDENTITY_TOL)
179    {
180        return Err(knot_reject(
181            "KnotVector: knots must be non-decreasing",
182            knots,
183            degree,
184        ));
185    }
186    let first = knots[0];
187    let last = knots[knots.len() - 1];
188    for index in 0..=degree {
189        if (knots[index] - first).abs() > KNOT_IDENTITY_TOL {
190            return Err(knot_reject(
191                "KnotVector: expected clamped start",
192                knots,
193                degree,
194            ));
195        }
196        if (knots[knots.len() - 1 - index] - last).abs() > KNOT_IDENTITY_TOL {
197            return Err(knot_reject("KnotVector: expected clamped end", knots, degree));
198        }
199    }
200    if last - first <= KNOT_IDENTITY_TOL {
201        return Err(knot_reject(
202            "KnotVector: degenerate parameter range",
203            knots,
204            degree,
205        ));
206    }
207    Ok(())
208}
209
210pub(crate) fn knot_domain(knots: &[f64], degree: usize) -> [f64; 2] {
211    [knots[degree], knots[knots.len() - 1 - degree]]
212}
213
214pub(crate) fn knot_clamp(knots: &[f64], degree: usize, parameter: f64) -> f64 {
215    let [start, end] = knot_domain(knots, degree);
216    parameter.clamp(start, end)
217}
218
219pub(crate) fn knot_find_span(knots: &[f64], degree: usize, parameter: f64) -> usize {
220    let parameter = knot_clamp(knots, degree, parameter);
221    let n = knots.len() - degree - 2;
222    if parameter >= knots[n + 1] {
223        // At/after the domain end.  A properly clamped vector has the end knot
224        // at multiplicity exactly `degree + 1`, so span `n` is the last
225        // non-empty interval.  An over-clamped end (a vendor exporter can emit
226        // multiplicity > degree + 1, e.g. two coincident knot VALUES) leaves
227        // span `n` pointing at a zero-width interval whose basis functions are
228        // all zero.  Walk back to the last interval that actually has width.
229        let mut span = n;
230        while span > degree && knots[span] >= knots[span + 1] {
231            span -= 1;
232        }
233        return span;
234    }
235    if parameter <= knots[degree] {
236        // At/before the domain start — the mirror of the case above.  For a
237        // properly clamped start (multiplicity degree + 1) this returns
238        // `degree`; for an over-clamped start it advances past the extra
239        // coincident knots to the first non-empty interval so evaluation never
240        // lands on a zero-width span (which would yield an all-zero,
241        // zero-weight homogeneous point).
242        let mut span = degree;
243        while span < n && knots[span + 1] <= parameter {
244            span += 1;
245        }
246        return span;
247    }
248    let mut low = degree;
249    let mut high = n + 1;
250    let mut middle = (low + high) / 2;
251    while parameter < knots[middle] || parameter >= knots[middle + 1] {
252        if parameter < knots[middle] {
253            high = middle;
254        } else {
255            low = middle;
256        }
257        middle = (low + high) / 2;
258    }
259    middle
260}
261
262/// Cox–de Boor basis functions into a caller-provided stack array.
263/// Requires `degree <= MAX_STACK_DEGREE`; entries `0..=degree` are written.
264pub(crate) fn basis_functions_into(
265    knots: &[f64],
266    degree: usize,
267    span: usize,
268    parameter: f64,
269    basis: &mut [f64; MAX_STACK_ORDER],
270) {
271    debug_assert!(degree <= MAX_STACK_DEGREE);
272    let mut left = [0.0f64; MAX_STACK_ORDER];
273    let mut right = [0.0f64; MAX_STACK_ORDER];
274    basis[0] = 1.0;
275    for j in 1..=degree {
276        left[j] = parameter - knots[span + 1 - j];
277        right[j] = knots[span + j] - parameter;
278        let mut saved = 0.0;
279        for r in 0..j {
280            let denominator = right[r + 1] + left[j - r];
281            let temporary = if denominator.abs() <= EPS {
282                0.0
283            } else {
284                basis[r] / denominator
285            };
286            basis[r] = saved + right[r + 1] * temporary;
287            saved = left[j - r] * temporary;
288        }
289        basis[j] = saved;
290    }
291}
292
293/// Basis derivatives (The NURBS Book A2.3) into caller-provided stack rows.
294/// Writes rows `0..=min(derivative_count, degree)` of `out`; rows above the
295/// degree keep whatever the caller initialized them to (callers zero-fill,
296/// matching the heap implementation's zero rows). Requires
297/// `degree <= MAX_STACK_DEGREE` and `out.len() > derivative_count`.
298pub(crate) fn basis_derivatives_into(
299    knots: &[f64],
300    degree: usize,
301    span: usize,
302    parameter: f64,
303    derivative_count: usize,
304    out: &mut [[f64; MAX_STACK_ORDER]],
305) {
306    debug_assert!(degree <= MAX_STACK_DEGREE);
307    debug_assert!(out.len() > derivative_count);
308    let p = degree;
309    let n = derivative_count.min(p);
310    let mut ndu = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
311    let mut left = [0.0f64; MAX_STACK_ORDER];
312    let mut right = [0.0f64; MAX_STACK_ORDER];
313    ndu[0][0] = 1.0;
314
315    for j in 1..=p {
316        left[j] = parameter - knots[span + 1 - j];
317        right[j] = knots[span + j] - parameter;
318        let mut saved = 0.0;
319        for r in 0..j {
320            ndu[j][r] = right[r + 1] + left[j - r];
321            let temporary = if ndu[j][r].abs() <= EPS {
322                0.0
323            } else {
324                ndu[r][j - 1] / ndu[j][r]
325            };
326            ndu[r][j] = saved + right[r + 1] * temporary;
327            saved = left[j - r] * temporary;
328        }
329        ndu[j][j] = saved;
330    }
331
332    for j in 0..=p {
333        out[0][j] = ndu[j][p];
334    }
335    let mut a = [[0.0f64; MAX_STACK_ORDER]; 2];
336    for r in 0..=p {
337        let mut s1 = 0;
338        let mut s2 = 1;
339        a[0][0] = 1.0;
340        for k in 1..=n {
341            a[s2] = [0.0; MAX_STACK_ORDER];
342            let mut value = 0.0;
343            let rk = r as isize - k as isize;
344            let pk = p - k;
345            if r >= k {
346                let denominator = ndu[pk + 1][rk as usize];
347                a[s2][0] = a[s1][0] / denominator;
348                value = a[s2][0] * ndu[rk as usize][pk];
349            }
350            let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
351            let j2 = if r <= pk + 1 { k - 1 } else { p - r };
352            if j1 <= j2 {
353                for j in j1..=j2 {
354                    let index = (rk + j as isize) as usize;
355                    a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
356                    value += a[s2][j] * ndu[index][pk];
357                }
358            }
359            if r <= pk {
360                a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
361                value += a[s2][k] * ndu[r][pk];
362            }
363            out[k][r] = value;
364            std::mem::swap(&mut s1, &mut s2);
365        }
366    }
367    let mut factor = p as f64;
368    for k in 1..=n {
369        for value in out[k][..=p].iter_mut() {
370            *value *= factor;
371        }
372        factor *= (p - k) as f64;
373    }
374}
375
376#[derive(Clone, Debug, Deserialize, Serialize)]
377pub struct KnotVector {
378    pub knots: Vec<f64>,
379    pub degree: usize,
380}
381
382/// Distinct interior knots, excluding endpoint neighborhoods within `1e-12`.
383pub(crate) fn interior_knots(knots: &[f64], degree: usize) -> Vec<f64> {
384    distinct_interior_knots(knots, degree).collect()
385}
386
387impl KnotVector {
388    pub fn new(knots: Vec<f64>, degree: usize) -> Result<Self, String> {
389        validate_knots(&knots, degree)?;
390        Ok(Self { knots, degree })
391    }
392
393    pub fn control_point_count(&self) -> usize {
394        self.knots.len() - self.degree - 1
395    }
396
397    pub fn domain(&self) -> [f64; 2] {
398        [
399            self.knots[self.degree],
400            self.knots[self.knots.len() - 1 - self.degree],
401        ]
402    }
403
404    pub fn clamp_param(&self, parameter: f64) -> f64 {
405        knot_clamp(&self.knots, self.degree, parameter)
406    }
407
408    pub fn find_span(&self, parameter: f64) -> usize {
409        knot_find_span(&self.knots, self.degree, parameter)
410    }
411
412    pub fn basis_functions(&self, span: usize, parameter: f64) -> Vec<f64> {
413        if self.degree <= MAX_STACK_DEGREE {
414            let mut basis = [0.0f64; MAX_STACK_ORDER];
415            basis_functions_into(&self.knots, self.degree, span, parameter, &mut basis);
416            return basis[..=self.degree].to_vec();
417        }
418        let mut basis = vec![0.0; self.degree + 1];
419        let mut left = vec![0.0; self.degree + 1];
420        let mut right = vec![0.0; self.degree + 1];
421        basis[0] = 1.0;
422        for j in 1..=self.degree {
423            left[j] = parameter - self.knots[span + 1 - j];
424            right[j] = self.knots[span + j] - parameter;
425            let mut saved = 0.0;
426            for r in 0..j {
427                let denominator = right[r + 1] + left[j - r];
428                let temporary = if denominator.abs() <= EPS {
429                    0.0
430                } else {
431                    basis[r] / denominator
432                };
433                basis[r] = saved + right[r + 1] * temporary;
434                saved = left[j - r] * temporary;
435            }
436            basis[j] = saved;
437        }
438        basis
439    }
440
441    pub fn basis_derivatives(
442        &self,
443        span: usize,
444        parameter: f64,
445        derivative_count: usize,
446    ) -> Vec<Vec<f64>> {
447        if self.degree <= MAX_STACK_DEGREE && derivative_count <= MAX_STACK_DEGREE {
448            let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
449            basis_derivatives_into(
450                &self.knots,
451                self.degree,
452                span,
453                parameter,
454                derivative_count,
455                &mut rows[..=derivative_count],
456            );
457            return rows[..=derivative_count]
458                .iter()
459                .map(|row| row[..=self.degree].to_vec())
460                .collect();
461        }
462        let p = self.degree;
463        let n = derivative_count.min(p);
464        let mut ndu = vec![vec![0.0; p + 1]; p + 1];
465        let mut left = vec![0.0; p + 1];
466        let mut right = vec![0.0; p + 1];
467        ndu[0][0] = 1.0;
468
469        for j in 1..=p {
470            left[j] = parameter - self.knots[span + 1 - j];
471            right[j] = self.knots[span + j] - parameter;
472            let mut saved = 0.0;
473            for r in 0..j {
474                ndu[j][r] = right[r + 1] + left[j - r];
475                let temporary = if ndu[j][r].abs() <= EPS {
476                    0.0
477                } else {
478                    ndu[r][j - 1] / ndu[j][r]
479                };
480                ndu[r][j] = saved + right[r + 1] * temporary;
481                saved = left[j - r] * temporary;
482            }
483            ndu[j][j] = saved;
484        }
485
486        let mut derivatives = vec![vec![0.0; p + 1]; derivative_count + 1];
487        for j in 0..=p {
488            derivatives[0][j] = ndu[j][p];
489        }
490        let mut a = vec![vec![0.0; p + 1]; 2];
491        for r in 0..=p {
492            let mut s1 = 0;
493            let mut s2 = 1;
494            a[0][0] = 1.0;
495            for k in 1..=n {
496                a[s2].fill(0.0);
497                let mut value = 0.0;
498                let rk = r as isize - k as isize;
499                let pk = p - k;
500                if r >= k {
501                    let denominator = ndu[pk + 1][rk as usize];
502                    a[s2][0] = a[s1][0] / denominator;
503                    value = a[s2][0] * ndu[rk as usize][pk];
504                }
505                let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
506                let j2 = if r <= pk + 1 { k - 1 } else { p - r };
507                if j1 <= j2 {
508                    for j in j1..=j2 {
509                        let index = (rk + j as isize) as usize;
510                        a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
511                        value += a[s2][j] * ndu[index][pk];
512                    }
513                }
514                if r <= pk {
515                    a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
516                    value += a[s2][k] * ndu[r][pk];
517                }
518                derivatives[k][r] = value;
519                std::mem::swap(&mut s1, &mut s2);
520            }
521        }
522        let mut factor = p as f64;
523        for (k, row) in derivatives.iter_mut().enumerate().take(n + 1).skip(1) {
524            for value in row {
525                *value *= factor;
526            }
527            factor *= (p - k) as f64;
528        }
529        derivatives
530    }
531}
532
533#[derive(Clone, Debug, Deserialize, Serialize)]
534pub struct NurbsCurve {
535    pub degree: usize,
536    pub knots: Vec<f64>,
537    pub control_points: Vec<Vec4>,
538    /// One-time validation cache. Deserialized curves (which bypass `new`)
539    /// run the full `new`-equivalent checks on first geometric use instead of
540    /// re-validating the knot vector on every evaluation.
541    #[serde(skip, default)]
542    validated: std::cell::Cell<bool>,
543}
544
545impl NurbsCurve {
546    pub fn new(degree: usize, knots: Vec<f64>, control_points: Vec<Vec4>) -> Result<Self, String> {
547        let curve = Self {
548            degree,
549            knots,
550            control_points,
551            validated: std::cell::Cell::new(false),
552        };
553        curve.ensure_valid()?;
554        Ok(curve)
555    }
556
557    /// The full construction-time checks, run at most once per instance.
558    fn ensure_valid(&self) -> Result<(), String> {
559        if self.validated.get() {
560            return Ok(());
561        }
562        validate_knots(&self.knots, self.degree)?;
563        let expected = self.knots.len() - self.degree - 1;
564        if self.control_points.len() != expected {
565            return Err(format!(
566                "NurbsCurve: knot vector implies {} control points, got {}",
567                expected,
568                self.control_points.len()
569            ));
570        }
571        if self.control_points.iter().any(|point| {
572            point.w <= EPS
573                || ![point.x, point.y, point.z, point.w]
574                    .iter()
575                    .all(|value| value.is_finite())
576        }) {
577            return Err("NurbsCurve: control points must be finite with positive weights".into());
578        }
579        self.validated.set(true);
580        Ok(())
581    }
582
583    fn knot_vector(&self) -> Result<KnotVector, String> {
584        KnotVector::new(self.knots.clone(), self.degree)
585    }
586
587    pub fn domain(&self) -> Result<[f64; 2], String> {
588        self.ensure_valid()?;
589        Ok(knot_domain(&self.knots, self.degree))
590    }
591
592    pub fn evaluate_homogeneous(&self, parameter: f64) -> Result<Vec4, String> {
593        self.ensure_valid()?;
594        let span = knot_find_span(&self.knots, self.degree, parameter);
595        let parameter = knot_clamp(&self.knots, self.degree, parameter);
596        let mut point = Vec4 {
597            x: 0.0,
598            y: 0.0,
599            z: 0.0,
600            w: 0.0,
601        };
602        if self.degree <= MAX_STACK_DEGREE {
603            let mut basis = [0.0f64; MAX_STACK_ORDER];
604            basis_functions_into(&self.knots, self.degree, span, parameter, &mut basis);
605            for (index, value) in basis[..=self.degree].iter().enumerate() {
606                point = point.add(self.control_points[span - self.degree + index].scale(*value));
607            }
608        } else {
609            let knot_vector = self.knot_vector()?;
610            let basis = knot_vector.basis_functions(span, parameter);
611            for (index, value) in basis.iter().enumerate() {
612                point = point.add(self.control_points[span - self.degree + index].scale(*value));
613            }
614        }
615        Ok(point)
616    }
617
618    pub fn evaluate(&self, parameter: f64) -> Result<Vec3, String> {
619        self.evaluate_homogeneous(parameter)?.point()
620    }
621
622    /// Value beyond the domain (Golovanov §2.15): every curve must answer
623    /// out-of-domain queries because intersection and projection
624    /// algorithms probe there.  Closed curves wrap the parameter
625    /// cyclically; open curves extend linearly along the end tangent.
626    pub fn evaluate_extended(&self, parameter: f64) -> Result<Vec3, String> {
627        Ok(self.derivatives_extended(parameter, 0)?[0])
628    }
629
630    /// Derivatives beyond the domain (§2.15).  The open-end extension is
631    /// linear: the first derivative is the boundary tangent and higher
632    /// derivatives vanish.
633    pub fn derivatives_extended(
634        &self,
635        parameter: f64,
636        derivative_count: usize,
637    ) -> Result<Vec<Vec3>, String> {
638        let [start, end] = self.domain()?;
639        if parameter >= start && parameter <= end {
640            return self.derivatives(parameter, derivative_count);
641        }
642        let period = end - start;
643        if period > 0.0 {
644            let closed =
645                self.evaluate(start)?.sub(self.evaluate(end)?).length() <= 1e-9 * (1.0 + period);
646            if closed {
647                let wrapped = start + (parameter - start).rem_euclid(period);
648                return self.derivatives(wrapped, derivative_count);
649            }
650        }
651        let boundary = if parameter < start { start } else { end };
652        let base = self.derivatives(boundary, derivative_count.max(1))?;
653        let mut result = Vec::with_capacity(derivative_count + 1);
654        result.push(base[0].add(base[1].scale(parameter - boundary)));
655        if derivative_count >= 1 {
656            result.push(base[1]);
657        }
658        for _ in 2..=derivative_count {
659            result.push(Vec3::default());
660        }
661        Ok(result)
662    }
663
664    pub fn derivatives(
665        &self,
666        parameter: f64,
667        derivative_count: usize,
668    ) -> Result<Vec<Vec3>, String> {
669        self.ensure_valid()?;
670        let parameter = knot_clamp(&self.knots, self.degree, parameter);
671        let span = knot_find_span(&self.knots, self.degree, parameter);
672        let calculated_count = derivative_count.min(self.degree);
673        let mut homogeneous: Vec<Vec4> = Vec::with_capacity(calculated_count + 1);
674        if self.degree <= MAX_STACK_DEGREE {
675            let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
676            basis_derivatives_into(
677                &self.knots,
678                self.degree,
679                span,
680                parameter,
681                calculated_count,
682                &mut rows[..=calculated_count],
683            );
684            for row in rows.iter().take(calculated_count + 1) {
685                let mut point = Vec4 {
686                    x: 0.0,
687                    y: 0.0,
688                    z: 0.0,
689                    w: 0.0,
690                };
691                for (index, value) in row.iter().enumerate().take(self.degree + 1) {
692                    point =
693                        point.add(self.control_points[span - self.degree + index].scale(*value));
694                }
695                homogeneous.push(point);
696            }
697        } else {
698            let knot_vector = self.knot_vector()?;
699            let basis = knot_vector.basis_derivatives(span, parameter, calculated_count);
700            for row in basis.iter().take(calculated_count + 1) {
701                let mut point = Vec4 {
702                    x: 0.0,
703                    y: 0.0,
704                    z: 0.0,
705                    w: 0.0,
706                };
707                for (index, value) in row.iter().enumerate().take(self.degree + 1) {
708                    point =
709                        point.add(self.control_points[span - self.degree + index].scale(*value));
710                }
711                homogeneous.push(point);
712            }
713        }
714
715        let mut result: Vec<Vec3> = Vec::with_capacity(derivative_count + 1);
716        for k in 0..=calculated_count {
717            let mut value = Vec3::new(homogeneous[k].x, homogeneous[k].y, homogeneous[k].z);
718            for i in 1..=k {
719                value = value.sub(result[k - i].scale(binomial(k, i) * homogeneous[i].w));
720            }
721            result.push(value.scale(1.0 / homogeneous[0].w));
722        }
723        result.resize(derivative_count + 1, Vec3::default());
724        Ok(result)
725    }
726
727    /// Allocation-free twin of [`Self::derivatives`] for the hot
728    /// `derivative_count <= 2` path.
729    ///
730    /// Byte-for-byte faithful copy of the arithmetic in [`Self::derivatives`]:
731    /// identical basis evaluation, identical summation order, and the identical
732    /// rational de-homogenization recurrence. Only the storage differs — a
733    /// fixed-size `[Vec4; 3]` / `[Vec3; 3]` on the stack instead of the heap
734    /// `Vec`s. Entries beyond `derivative_count.min(degree)` stay
735    /// `Vec3::default()`, exactly as the heap version's trailing `resize` leaves
736    /// them.
737    pub(crate) fn derivatives_small(
738        &self,
739        parameter: f64,
740        derivative_count: usize,
741    ) -> Result<[Vec3; 3], String> {
742        debug_assert!(derivative_count <= 2);
743        self.ensure_valid()?;
744        let parameter = knot_clamp(&self.knots, self.degree, parameter);
745        let span = knot_find_span(&self.knots, self.degree, parameter);
746        let calculated_count = derivative_count.min(self.degree);
747        let zero = Vec4 {
748            x: 0.0,
749            y: 0.0,
750            z: 0.0,
751            w: 0.0,
752        };
753        let mut homogeneous = [zero; 3];
754        if self.degree <= MAX_STACK_DEGREE {
755            let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
756            basis_derivatives_into(
757                &self.knots,
758                self.degree,
759                span,
760                parameter,
761                calculated_count,
762                &mut rows[..=calculated_count],
763            );
764            for (k, row) in rows.iter().take(calculated_count + 1).enumerate() {
765                let mut point = zero;
766                for (index, value) in row.iter().enumerate().take(self.degree + 1) {
767                    point =
768                        point.add(self.control_points[span - self.degree + index].scale(*value));
769                }
770                homogeneous[k] = point;
771            }
772        } else {
773            let knot_vector = self.knot_vector()?;
774            let basis = knot_vector.basis_derivatives(span, parameter, calculated_count);
775            for (k, row) in basis.iter().take(calculated_count + 1).enumerate() {
776                let mut point = zero;
777                for (index, value) in row.iter().enumerate().take(self.degree + 1) {
778                    point =
779                        point.add(self.control_points[span - self.degree + index].scale(*value));
780                }
781                homogeneous[k] = point;
782            }
783        }
784
785        let mut result = [Vec3::default(); 3];
786        for k in 0..=calculated_count {
787            let mut value = Vec3::new(homogeneous[k].x, homogeneous[k].y, homogeneous[k].z);
788            for i in 1..=k {
789                value = value.sub(result[k - i].scale(binomial(k, i) * homogeneous[i].w));
790            }
791            result[k] = value.scale(1.0 / homogeneous[0].w);
792        }
793        Ok(result)
794    }
795
796    /// Point and first derivative `(C, C')` with zero heap allocation.
797    /// Bit-identical to `derivatives(t, 1)` at indices `[0]` and `[1]`.
798    #[inline]
799    pub(crate) fn deriv1(&self, parameter: f64) -> Result<(Vec3, Vec3), String> {
800        let d = self.derivatives_small(parameter, 1)?;
801        Ok((d[0], d[1]))
802    }
803
804    pub fn reversed(&self) -> Result<Self, String> {
805        let start = self.knots[0];
806        let end = self.knots[self.knots.len() - 1];
807        let knots = self
808            .knots
809            .iter()
810            .rev()
811            .map(|knot| start + end - knot)
812            .collect();
813        let control_points = self.control_points.iter().rev().copied().collect();
814        Self::new(self.degree, knots, control_points)
815    }
816
817    pub fn insert_knot(&self, parameter: f64, requested: usize) -> Result<Self, String> {
818        let knot_vector = self.knot_vector()?;
819        let degree = self.degree;
820        let parameter = knot_vector.clamp_param(parameter);
821        // A parameter within knot tolerance of an existing knot must BE
822        // that knot: counting it as a multiplicity while find_span places
823        // it in the span below (parameter infinitesimally smaller) makes
824        // the Boehm index bookkeeping inconsistent and corrupts the net.
825        let parameter = self
826            .knots
827            .iter()
828            .copied()
829            .find(|knot| (knot - parameter).abs() <= KNOT_IDENTITY_TOL)
830            .unwrap_or(parameter);
831        let multiplicity = self
832            .knots
833            .iter()
834            .filter(|knot| (**knot - parameter).abs() <= KNOT_IDENTITY_TOL)
835            .count();
836        let insertion_count = requested.min(degree.saturating_sub(multiplicity));
837        if insertion_count == 0 {
838            return Ok(self.clone());
839        }
840        let span = knot_vector.find_span(parameter);
841        let last_control = self.control_points.len() - 1;
842        let mut knots = Vec::with_capacity(self.knots.len() + insertion_count);
843        knots.extend_from_slice(&self.knots[..=span]);
844        knots.extend(std::iter::repeat_n(parameter, insertion_count));
845        knots.extend_from_slice(&self.knots[span + 1..]);
846
847        let mut output = vec![
848            Vec4 {
849                x: 0.0,
850                y: 0.0,
851                z: 0.0,
852                w: 1.0,
853            };
854            last_control + 1 + insertion_count
855        ];
856        output[..=span - degree].copy_from_slice(&self.control_points[..=span - degree]);
857        for index in span - multiplicity..=last_control {
858            output[index + insertion_count] = self.control_points[index];
859        }
860        let mut affected = vec![
861            Vec4 {
862                x: 0.0,
863                y: 0.0,
864                z: 0.0,
865                w: 1.0,
866            };
867            degree + 1
868        ];
869        affected[..=degree - multiplicity]
870            .copy_from_slice(&self.control_points[span - degree..=span - multiplicity]);
871        let mut left = 0;
872        for insertion in 1..=insertion_count {
873            left = span - degree + insertion;
874            for index in 0..=degree - insertion - multiplicity {
875                let denominator = self.knots[index + span + 1] - self.knots[left + index];
876                let alpha = (parameter - self.knots[left + index]) / denominator;
877                affected[index] = affected[index + 1]
878                    .scale(alpha)
879                    .add(affected[index].scale(1.0 - alpha));
880            }
881            output[left] = affected[0];
882            output[span + insertion_count - insertion - multiplicity] =
883                affected[degree - insertion - multiplicity];
884        }
885        for index in left + 1..span - multiplicity {
886            output[index] = affected[index - left];
887        }
888        Self::new(degree, knots, output)
889    }
890
891    pub fn split(&self, parameter: f64) -> Result<(Self, Self), String> {
892        let [start, end] = self.domain()?;
893        if parameter <= start + KNOT_IDENTITY_TOL || parameter >= end - KNOT_IDENTITY_TOL {
894            return Err(format!(
895                "NurbsCurve.split: parameter {parameter} must be strictly inside domain [{start}, {end}]"
896            ));
897        }
898        // Snap onto a coincident knot so multiplicity and span agree (see
899        // insert_knot).
900        let parameter = self
901            .knots
902            .iter()
903            .copied()
904            .find(|knot| (knot - parameter).abs() <= KNOT_IDENTITY_TOL)
905            .unwrap_or(parameter);
906        let multiplicity = self
907            .knots
908            .iter()
909            .filter(|knot| (**knot - parameter).abs() <= KNOT_IDENTITY_TOL)
910            .count();
911        let refined = self.insert_knot(parameter, self.degree.saturating_sub(multiplicity))?;
912        let first = refined
913            .knots
914            .iter()
915            .position(|knot| (*knot - parameter).abs() <= KNOT_IDENTITY_TOL)
916            .ok_or_else(|| "NurbsCurve.split: inserted knot not found".to_string())?;
917        let mut left_knots = refined.knots[..first + self.degree].to_vec();
918        left_knots.push(parameter);
919        let left_points = refined.control_points[..first].to_vec();
920        let mut right_knots = vec![parameter; self.degree + 1];
921        right_knots.extend_from_slice(&refined.knots[first + self.degree..]);
922        let right_points = refined.control_points[first - 1..].to_vec();
923        Ok((
924            Self::new(self.degree, left_knots, left_points)?,
925            Self::new(self.degree, right_knots, right_points)?,
926        ))
927    }
928}
929
930pub fn make_line(start: Vec3, end: Vec3) -> Result<NurbsCurve, String> {
931    NurbsCurve::new(
932        1,
933        vec![0.0, 0.0, 1.0, 1.0],
934        vec![Vec4::from_point(start, 1.0), Vec4::from_point(end, 1.0)],
935    )
936}
937
938pub fn make_arc(
939    center: Vec3,
940    x_axis: Vec3,
941    y_axis: Vec3,
942    radius: f64,
943    start_angle: f64,
944    end_angle: f64,
945) -> Result<NurbsCurve, String> {
946    if radius <= EPS {
947        return Err("makeArc: radius must be positive".into());
948    }
949    let x_axis = x_axis.normalized()?;
950    let y_axis = y_axis.normalized()?;
951    if x_axis.dot(y_axis).abs() > 1e-9 {
952        return Err("makeArc: xAxis and yAxis must be orthogonal".into());
953    }
954    let mut theta = end_angle - start_angle;
955    if theta <= EPS {
956        return Err("makeArc: endAngle must exceed startAngle".into());
957    }
958    if theta > std::f64::consts::TAU + EPS {
959        return Err("makeArc: sweep exceeds full circle".into());
960    }
961    theta = theta.min(std::f64::consts::TAU);
962    let segment_count = ((theta / std::f64::consts::FRAC_PI_2 - EPS).ceil() as usize).clamp(1, 4);
963    let segment_angle = theta / segment_count as f64;
964    let middle_weight = (segment_angle / 2.0).cos();
965    let point_at = |angle: f64| {
966        center
967            .add(x_axis.scale(radius * angle.cos()))
968            .add(y_axis.scale(radius * angle.sin()))
969    };
970    let tangent_at = |angle: f64| x_axis.scale(-angle.sin()).add(y_axis.scale(angle.cos()));
971
972    let mut points = Vec::with_capacity(2 * segment_count + 1);
973    let mut angle = start_angle;
974    let mut first_point = point_at(angle);
975    let mut first_tangent = tangent_at(angle);
976    points.push(Vec4::from_point(first_point, 1.0));
977    for _ in 0..segment_count {
978        angle += segment_angle;
979        let end_point = point_at(angle);
980        let end_tangent = tangent_at(angle);
981        let cross = first_tangent.cross(end_tangent);
982        let denominator = cross.length_squared();
983        if denominator <= EPS {
984            return Err("makeArc: arc tangents are parallel".into());
985        }
986        let distance = end_point.sub(first_point).cross(end_tangent).dot(cross) / denominator;
987        let middle = first_point.add(first_tangent.scale(distance));
988        points.push(Vec4::from_point(middle, middle_weight));
989        points.push(Vec4::from_point(end_point, 1.0));
990        first_point = end_point;
991        first_tangent = end_tangent;
992    }
993    let mut knots = vec![0.0, 0.0, 0.0];
994    for index in 1..segment_count {
995        let knot = index as f64 / segment_count as f64;
996        knots.extend([knot, knot]);
997    }
998    knots.extend([1.0, 1.0, 1.0]);
999    NurbsCurve::new(2, knots, points)
1000}
1001
1002pub fn make_circle(center: Vec3, normal: Vec3, radius: f64) -> Result<NurbsCurve, String> {
1003    let normal = normal.normalized()?;
1004    let x_axis = normal.perpendicular()?;
1005    let y_axis = normal.cross(x_axis).normalized()?;
1006    make_arc(center, x_axis, y_axis, radius, 0.0, std::f64::consts::TAU)
1007}
1008
1009/// Build the exactly orthonormal local frame shared by the conic
1010/// constructors.  The conic algebra below (implicit equation, tangent
1011/// intersection, shoulder weight) is only exact in an orthonormal frame, so
1012/// the in-plane hint is Gram-Schmidt-projected against the primary direction
1013/// instead of trusted verbatim: callers only owe us non-parallel vectors.
1014fn conic_frame(fn_name: &str, primary: Vec3, hint: Vec3) -> Result<(Vec3, Vec3), String> {
1015    let x_axis = primary
1016        .normalized()
1017        .map_err(|_| format!("{fn_name}: primary axis must be non-zero"))?;
1018    if hint.length() <= EPS {
1019        return Err(format!("{fn_name}: in-plane direction must be non-zero"));
1020    }
1021    hint.sub(x_axis.scale(hint.dot(x_axis)))
1022        .normalized()
1023        .map(|y_axis| (x_axis, y_axis))
1024        .map_err(|_| format!("{fn_name}: frame directions must not be parallel"))
1025}
1026
1027/// Trim-range validation shared by the conic constructors.  The knot span IS
1028/// the conic parameter range, so it must clear the knot identity band or the
1029/// resulting curve would fail knot validation with an unhelpful message.
1030fn conic_range(fn_name: &str, t0: f64, t1: f64) -> Result<(), String> {
1031    if !t0.is_finite() || !t1.is_finite() {
1032        return Err(format!("{fn_name}: parameter range must be finite"));
1033    }
1034    if t1 - t0 <= KNOT_IDENTITY_TOL {
1035        return Err(format!("{fn_name}: t1 ({t1}) must exceed t0 ({t0})"));
1036    }
1037    Ok(())
1038}
1039
1040/// Extreme trim parameters overflow the conic point formulas (cosh exceeds
1041/// f64 near |t| ~ 710, focal·t² near |t| ~ 1e150); surface that as the
1042/// constructor's own honest error instead of the generic NurbsCurve
1043/// finiteness rejection.
1044fn conic_points_finite(fn_name: &str, points: &[Vec4]) -> Result<(), String> {
1045    if points.iter().any(|point| {
1046        ![point.x, point.y, point.z, point.w]
1047            .iter()
1048            .all(|value| value.is_finite())
1049    }) {
1050        return Err(format!(
1051            "{fn_name}: control points overflow f64 — parameter range too extreme"
1052        ));
1053    }
1054    Ok(())
1055}
1056
1057/// Exact parabola segment y² = 4·focal·x in the local frame (vertex at the
1058/// origin, `axis` = +x, `latus_direction` = +y), parametrized the standard
1059/// way P(t) = (focal·t², 2·focal·t) and trimmed to t ∈ [t0, t1].  A parabola
1060/// segment is a plain quadratic polynomial in t (all weights 1), so a single
1061/// degree-2 Bézier over the knot span [t0, t1] reproduces both the point set
1062/// AND the parametrization exactly — no fitting, and evaluate(t) == P(t) for
1063/// every t, not just at the ends.
1064pub fn make_parabola(
1065    vertex: Vec3,
1066    axis: Vec3,
1067    latus_direction: Vec3,
1068    focal: f64,
1069    t0: f64,
1070    t1: f64,
1071) -> Result<NurbsCurve, String> {
1072    if !focal.is_finite() || focal <= EPS {
1073        return Err("make_parabola: focal distance must be positive".into());
1074    }
1075    conic_range("make_parabola", t0, t1)?;
1076    let (x_axis, y_axis) = conic_frame("make_parabola", axis, latus_direction)?;
1077    let point_at = |t: f64| {
1078        vertex
1079            .add(x_axis.scale(focal * t * t))
1080            .add(y_axis.scale(2.0 * focal * t))
1081    };
1082    // Bernstein middle point of the quadratic polynomial,
1083    // P(t0) + (t1−t0)/2·P'(t0), collapses to local (focal·t0·t1,
1084    // focal·(t0+t1)) — which is also the intersection of the two end
1085    // tangents, as it must be for any parabola segment.
1086    let middle = vertex
1087        .add(x_axis.scale(focal * t0 * t1))
1088        .add(y_axis.scale(focal * (t0 + t1)));
1089    let control_points = vec![
1090        Vec4::from_point(point_at(t0), 1.0),
1091        Vec4::from_point(middle, 1.0),
1092        Vec4::from_point(point_at(t1), 1.0),
1093    ];
1094    conic_points_finite("make_parabola", &control_points)?;
1095    NurbsCurve::new(2, vec![t0, t0, t0, t1, t1, t1], control_points)
1096}
1097
1098/// Exact arc of the hyperbola branch x²/a² − y²/b² = 1, x > 0 in the local
1099/// frame (center at the origin, `major_axis` = +x, `minor_axis` = +y),
1100/// parametrized P(t) = (a·cosh t, b·sinh t) and trimmed to t ∈ [t0, t1].
1101/// A single rational quadratic Bézier is exact for any sweep on one branch:
1102/// endpoints on the curve, middle control point at the intersection of the
1103/// end tangents, and middle weight cosh((t1−t0)/2) chosen so the shoulder
1104/// point lands back on the branch.  Only the point set is hyperbola-exact
1105/// away from the ends; the NURBS parameter coincides with the hyperbolic
1106/// parameter t exactly at t0, (t0+t1)/2 and t1.
1107pub fn make_hyperbola(
1108    center: Vec3,
1109    major_axis: Vec3,
1110    minor_axis: Vec3,
1111    a: f64,
1112    b: f64,
1113    t0: f64,
1114    t1: f64,
1115) -> Result<NurbsCurve, String> {
1116    if !a.is_finite() || a <= EPS {
1117        return Err("make_hyperbola: semi-axis a must be positive".into());
1118    }
1119    if !b.is_finite() || b <= EPS {
1120        return Err("make_hyperbola: semi-axis b must be positive".into());
1121    }
1122    conic_range("make_hyperbola", t0, t1)?;
1123    let (x_axis, y_axis) = conic_frame("make_hyperbola", major_axis, minor_axis)?;
1124    let point_at = |t: f64| {
1125        center
1126            .add(x_axis.scale(a * t.cosh()))
1127            .add(y_axis.scale(b * t.sinh()))
1128    };
1129    let mid = 0.5 * (t0 + t1);
1130    let middle_weight = (0.5 * (t1 - t0)).cosh();
1131    // In the scaled frame (x/a, y/b) the branch is the unit hyperbola and the
1132    // tangent at t is the line X·cosh t − Y·sinh t = 1; Cramer's rule on the
1133    // two end-tangent lines collapses their intersection to
1134    // P(mid)/cosh(half-sweep), so no explicit line-line solve is needed.
1135    // With this middle point, weight cosh(half-sweep) puts the shoulder point
1136    // (P0 + 2w·P1 + P2)/(2 + 2w) back on the branch at P(mid).
1137    let apex = center
1138        .add(x_axis.scale(a * mid.cosh() / middle_weight))
1139        .add(y_axis.scale(b * mid.sinh() / middle_weight));
1140    let control_points = vec![
1141        Vec4::from_point(point_at(t0), 1.0),
1142        Vec4::from_point(apex, middle_weight),
1143        Vec4::from_point(point_at(t1), 1.0),
1144    ];
1145    conic_points_finite("make_hyperbola", &control_points)?;
1146    NurbsCurve::new(2, vec![t0, t0, t0, t1, t1, t1], control_points)
1147}
1148
1149pub fn uniform_clamped_knots(
1150    control_point_count: usize,
1151    degree: usize,
1152) -> Result<Vec<f64>, String> {
1153    if control_point_count == 0 || control_point_count - 1 < degree {
1154        return Err("uniformClampedKnots: need at least degree+1 control points".into());
1155    }
1156    let n = control_point_count - 1;
1157    let mut knots = vec![0.0; degree + 1];
1158    let interior = n - degree;
1159    for index in 1..=interior {
1160        knots.push(index as f64 / (interior + 1) as f64);
1161    }
1162    knots.extend(std::iter::repeat_n(1.0, degree + 1));
1163    Ok(knots)
1164}
1165
1166fn binomial(n: usize, k: usize) -> f64 {
1167    if k > n {
1168        return 0.0;
1169    }
1170    let k = k.min(n - k);
1171    (1..=k).fold(1.0, |value, index| {
1172        value * (n - k + index) as f64 / index as f64
1173    })
1174}
1175
1176// BREP private tests: 6ef392d382ba5b75
1177
1178// BREP private tests: 94ba6c381a283411