Skip to main content

brep_kernel/geometry/
fit.rs

1use crate::{KnotVector, NurbsCurve, Vec3, Vec4};
2
3#[derive(Clone, Debug)]
4pub struct PolylineFit {
5    pub curve: NurbsCurve,
6    pub parameters: Vec<f64>,
7    pub kept: Vec<Vec3>,
8}
9
10pub fn solve_dense(mut matrix: Vec<Vec<f64>>, mut rhs: Vec<f64>) -> Result<Vec<f64>, String> {
11    let count = rhs.len();
12    if count == 0 || matrix.len() != count || matrix.iter().any(|row| row.len() != count) {
13        return Err("solve_dense: matrix must be square and match RHS".into());
14    }
15    let scale = matrix
16        .iter()
17        .flatten()
18        .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
19    if scale == 0.0 {
20        return Err("solve_dense: singular matrix".into());
21    }
22    for column in 0..count {
23        let pivot = (column..count)
24            .max_by(|&a, &b| matrix[a][column].abs().total_cmp(&matrix[b][column].abs()))
25            .unwrap();
26        if matrix[pivot][column].abs() <= 1e-13 * scale {
27            return Err("solve_dense: singular matrix".into());
28        }
29        matrix.swap(column, pivot);
30        rhs.swap(column, pivot);
31        let diagonal = matrix[column][column];
32        for row in column + 1..count {
33            let factor = matrix[row][column] / diagonal;
34            matrix[row][column] = 0.0;
35            for entry in column + 1..count {
36                matrix[row][entry] -= factor * matrix[column][entry];
37            }
38            rhs[row] -= factor * rhs[column];
39        }
40    }
41    let mut result = vec![0.0; count];
42    for row in (0..count).rev() {
43        let remainder: f64 = (row + 1..count)
44            .map(|column| matrix[row][column] * result[column])
45            .sum();
46        result[row] = (rhs[row] - remainder) / matrix[row][row];
47    }
48    Ok(result)
49}
50
51/// `solve_dense` for fixed-size systems on the stack — identical pivoting and
52/// thresholds, no allocation. `count <= N` solves the leading `count`-sized
53/// block (for callers that shrink the system by fixing parameters).
54pub fn solve_small<const N: usize>(
55    mut matrix: [[f64; N]; N],
56    mut rhs: [f64; N],
57    count: usize,
58) -> Result<[f64; N], String> {
59    if count == 0 || count > N {
60        return Err("solve_small: invalid system size".into());
61    }
62    let scale = matrix
63        .iter()
64        .take(count)
65        .flat_map(|row| row.iter().take(count))
66        .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
67    if scale == 0.0 {
68        return Err("solve_small: singular matrix".into());
69    }
70    for column in 0..count {
71        let pivot = (column..count)
72            .max_by(|&a, &b| matrix[a][column].abs().total_cmp(&matrix[b][column].abs()))
73            .unwrap();
74        if matrix[pivot][column].abs() <= 1e-13 * scale {
75            return Err("solve_small: singular matrix".into());
76        }
77        matrix.swap(column, pivot);
78        rhs.swap(column, pivot);
79        let diagonal = matrix[column][column];
80        for row in column + 1..count {
81            let factor = matrix[row][column] / diagonal;
82            matrix[row][column] = 0.0;
83            for entry in column + 1..count {
84                matrix[row][entry] -= factor * matrix[column][entry];
85            }
86            rhs[row] -= factor * rhs[column];
87        }
88    }
89    let mut result = [0.0; N];
90    for row in (0..count).rev() {
91        let remainder: f64 = (row + 1..count)
92            .map(|column| matrix[row][column] * result[column])
93            .sum();
94        result[row] = (rhs[row] - remainder) / matrix[row][row];
95    }
96    Ok(result)
97}
98
99/// Solve a B-spline interpolation (collocation) system. The collocation matrix
100/// `N_j(τ_i)` produced by the knot-averaging schemes below is banded with
101/// half-bandwidth ≤ `degree` (Schoenberg–Whitney) and totally positive, so a
102/// no-pivot BANDED elimination is O(n·degree²) — vs the O(n³) of the general
103/// dense solver — and is numerically identical to it on these systems.
104///
105/// The banded result is accepted only when its in-band residual is tiny;
106/// otherwise (a pathological / near-singular system) it falls back to the
107/// partial-pivoting dense solver. This keeps large STEP-import pcurve fits
108/// (which push `n` toward ~500) from turning each solve into a ~500³ blowup.
109pub fn solve_collocation(
110    matrix: &[Vec<f64>],
111    rhs: &[f64],
112    degree: usize,
113) -> Result<Vec<f64>, String> {
114    let count = rhs.len();
115    let bandwidth = degree.max(1).min(count.saturating_sub(1).max(1));
116    if let Ok(solution) = solve_banded(matrix, rhs, bandwidth) {
117        let scale = rhs
118            .iter()
119            .fold(0.0_f64, |maximum, value| maximum.max(value.abs()));
120        let tolerance = 1e-9 * scale.max(1.0);
121        let mut residual = 0.0_f64;
122        for row in 0..count {
123            let lo = row.saturating_sub(bandwidth);
124            let hi = (row + bandwidth).min(count - 1);
125            let mut accumulated = 0.0;
126            for col in lo..=hi {
127                accumulated += matrix[row][col] * solution[col];
128            }
129            residual = residual.max((accumulated - rhs[row]).abs());
130        }
131        if residual <= tolerance {
132            return Ok(solution);
133        }
134    }
135    solve_dense(matrix.to_vec(), rhs.to_vec())
136}
137
138pub fn solve_banded(
139    matrix: &[Vec<f64>],
140    rhs: &[f64],
141    bandwidth: usize,
142) -> Result<Vec<f64>, String> {
143    let count = rhs.len();
144    if count == 0 || matrix.len() != count || matrix.iter().any(|row| row.len() != count) {
145        return Err("solve_banded: matrix must be square and match RHS".into());
146    }
147    let mut matrix = matrix.to_vec();
148    let mut rhs = rhs.to_vec();
149    for column in 0..count {
150        let pivot = matrix[column][column];
151        if pivot.abs() <= 1e-300 {
152            return Err("solve_banded: singular matrix".into());
153        }
154        let maximum_row = (column + bandwidth).min(count - 1);
155        for row in column + 1..=maximum_row {
156            let factor = matrix[row][column] / pivot;
157            if factor == 0.0 {
158                continue;
159            }
160            let maximum_column = (column + bandwidth).min(count - 1);
161            for entry in column..=maximum_column {
162                matrix[row][entry] -= factor * matrix[column][entry];
163            }
164            rhs[row] -= factor * rhs[column];
165        }
166    }
167    let mut result = vec![0.0; count];
168    for row in (0..count).rev() {
169        let mut value = rhs[row];
170        let maximum_column = (row + bandwidth).min(count - 1);
171        for column in row + 1..=maximum_column {
172            value -= matrix[row][column] * result[column];
173        }
174        if matrix[row][row].abs() <= 1e-300 {
175            return Err("solve_banded: singular matrix".into());
176        }
177        result[row] = value / matrix[row][row];
178    }
179    Ok(result)
180}
181
182/// Global B-spline interpolation (The NURBS Book A9.1), matching the
183/// reference implementation's supplied-parameter path.
184/// Interpolate HOMOGENEOUS points (Vec4, rational data) at the given
185/// parameters with the same averaged-knot scheme as `interpolate_curve`.
186/// Parameters must start at 0 and end at 1. Rows fitted with identical
187/// parameters share knots — the contract the blend-surface rows rely on.
188pub fn interpolate_homogeneous(
189    points: &[Vec4],
190    degree: usize,
191    parameters: &[f64],
192) -> Result<NurbsCurve, String> {
193    if points.len() < 2 || parameters.len() != points.len() {
194        return Err(
195            "interpolate_homogeneous: points and parameters must have matching length >= 2".into(),
196        );
197    }
198    if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
199        return Err("interpolate_homogeneous: parameters must be strictly increasing".into());
200    }
201    let n = points.len() - 1;
202    let degree = degree.min(n);
203    let m = n + degree + 1;
204    let mut knots = vec![0.0; m + 1];
205    for knot in &mut knots[m - degree..=m] {
206        *knot = 1.0;
207    }
208    for j in 1..=n.saturating_sub(degree) {
209        knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
210    }
211    let knot_vector = KnotVector::new(knots.clone(), degree)?;
212    let mut matrix = vec![vec![0.0; n + 1]; n + 1];
213    for (row, &parameter) in parameters.iter().enumerate() {
214        let span = knot_vector.find_span(parameter);
215        let basis = knot_vector.basis_functions(span, parameter);
216        for (offset, value) in basis.into_iter().enumerate() {
217            matrix[row][span - degree + offset] = value;
218        }
219    }
220    let solve_axis = |axis: fn(&Vec4) -> f64| {
221        solve_collocation(
222            &matrix,
223            &points.iter().map(axis).collect::<Vec<_>>(),
224            degree,
225        )
226    };
227    let xs = solve_axis(|point| point.x)?;
228    let ys = solve_axis(|point| point.y)?;
229    let zs = solve_axis(|point| point.z)?;
230    let ws = solve_axis(|point| point.w)?;
231    NurbsCurve::new(
232        degree,
233        knots,
234        (0..=n)
235            .map(|index| Vec4 {
236                x: xs[index],
237                y: ys[index],
238                z: zs[index],
239                w: ws[index],
240            })
241            .collect(),
242    )
243}
244
245/// Global B-spline interpolation (The NURBS Book A9.1) through `points` at the
246/// supplied strictly increasing `parameters`.
247///
248/// The result is CLAMPED over exactly `[parameters[0], parameters[n]]` — the
249/// caller's own parameter interval, not a normalized `[0, 1]`. A9.1's end knots
250/// are `ū_0` and `ū_n`; hardcoding `0.0`/`1.0` there is only correct when the
251/// caller already normalized, and silently emits a knot vector that DESCENDS at
252/// the clamp when it did not (STEP import's `reconcile_edges_onto_surfaces`
253/// re-fits an edge over its own domain — on ABC 00000290 that is `[1.0,
254/// 1.2499]`, producing interior knots up to 1.234 followed by the four 1.0 end
255/// knots, i.e. a 0.234 descent that `validate_knots` rightly rejected). Since
256/// the averaged interior knots always lie strictly between `parameters[0]` and
257/// `parameters[n]`, taking the ends from the parameters makes the vector valid
258/// for ANY strictly increasing input, and is bit-identical for the already-
259/// normalized callers.
260pub fn interpolate_curve(
261    points: &[Vec3],
262    degree: usize,
263    parameters: &[f64],
264) -> Result<NurbsCurve, String> {
265    if points.len() < 2 || parameters.len() != points.len() {
266        return Err(
267            "interpolate_curve: points and parameters must have matching length >= 2".into(),
268        );
269    }
270    if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
271        return Err("interpolate_curve: parameters must be strictly increasing".into());
272    }
273    let n = points.len() - 1;
274    let degree = degree.min(n);
275    let m = n + degree + 1;
276    let mut knots = vec![parameters[0]; m + 1];
277    for knot in &mut knots[m - degree..=m] {
278        *knot = parameters[n];
279    }
280    for j in 1..=n.saturating_sub(degree) {
281        knots[j + degree] = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
282    }
283    let knot_vector = KnotVector::new(knots.clone(), degree)?;
284    let mut matrix = vec![vec![0.0; n + 1]; n + 1];
285    for (row, &parameter) in parameters.iter().enumerate() {
286        let span = knot_vector.find_span(parameter);
287        let basis = knot_vector.basis_functions(span, parameter);
288        for (offset, value) in basis.into_iter().enumerate() {
289            matrix[row][span - degree + offset] = value;
290        }
291    }
292    let solve_axis = |axis: fn(Vec3) -> f64| {
293        solve_collocation(
294            &matrix,
295            &points.iter().copied().map(axis).collect::<Vec<_>>(),
296            degree,
297        )
298    };
299    let xs = solve_axis(|point| point.x)?;
300    let ys = solve_axis(|point| point.y)?;
301    let zs = solve_axis(|point| point.z)?;
302    NurbsCurve::new(
303        degree,
304        knots,
305        (0..=n)
306            .map(|index| Vec4::from_point(Vec3::new(xs[index], ys[index], zs[index]), 1.0))
307            .collect(),
308    )
309}
310
311/// Cubic interpolation with PRESCRIBED end derivatives (Piegl–Tiller §9.2.2):
312/// n+1 points plus two tangent rows give n+3 clamped control points. The
313/// derivative conditions use the exact clamped end forms
314/// C'(t0) = p/(u_{p+1}−t0)·(Q1−Q0) and C'(t1) = p/(t1−u_{m−p−1})·(Qn−Qn−1),
315/// so the requested tangents are reproduced exactly — the §5.8 loft tangency
316/// building block.
317pub fn interpolate_curve_with_end_tangents(
318    points: &[Vec3],
319    parameters: &[f64],
320    start_tangent: Vec3,
321    end_tangent: Vec3,
322) -> Result<NurbsCurve, String> {
323    if points.len() < 2 || parameters.len() != points.len() {
324        return Err(
325            "interpolate_curve_with_end_tangents: points and parameters must match, >= 2".into(),
326        );
327    }
328    if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
329        return Err("interpolate_curve_with_end_tangents: parameters must increase".into());
330    }
331    let degree = 3usize;
332    let n = points.len() - 1;
333    let control_count = n + 3;
334    let t0 = parameters[0];
335    let t1 = parameters[n];
336    // Clamped knots with n−1 interior values averaged over parameter runs
337    // (the tangent rows consume the two extra controls).
338    let mut knots = vec![t0; degree + 1];
339    for j in 0..n.saturating_sub(1) {
340        let window = &parameters[j + 1..(j + degree).min(n) + 1];
341        knots.push(window.iter().sum::<f64>() / window.len() as f64);
342    }
343    knots.extend(std::iter::repeat(t1).take(degree + 1));
344    if knots.len() != control_count + degree + 1 {
345        return Err(format!(
346            "interpolate_curve_with_end_tangents: internal knot count {} for {} controls",
347            knots.len(),
348            control_count
349        ));
350    }
351    let knot_vector = KnotVector::new(knots.clone(), degree)?;
352    let mut matrix = vec![vec![0.0; control_count]; control_count];
353    let mut rhs_points = vec![Vec3::default(); control_count];
354    // Row 0: C(t0) = P0; row 1: start tangent; rows 2..=n: interior + end
355    // interpolation; row n+1... reorganized: standard layout is
356    // [P0, T0, P1..Pn-1, T1, Pn].
357    matrix[0][0] = 1.0;
358    rhs_points[0] = points[0];
359    let start_span = knots[degree + 1] - t0;
360    matrix[1][0] = -(degree as f64) / start_span;
361    matrix[1][1] = (degree as f64) / start_span;
362    rhs_points[1] = start_tangent;
363    for (index, &parameter) in parameters.iter().enumerate().take(n).skip(1) {
364        let row = index + 1;
365        let span = knot_vector.find_span(parameter);
366        let basis = knot_vector.basis_functions(span, parameter);
367        for (offset, value) in basis.into_iter().enumerate() {
368            matrix[row][span - degree + offset] = value;
369        }
370        rhs_points[row] = points[index];
371    }
372    let end_span = t1 - knots[control_count - 1];
373    matrix[control_count - 2][control_count - 2] = -(degree as f64) / end_span;
374    matrix[control_count - 2][control_count - 1] = (degree as f64) / end_span;
375    rhs_points[control_count - 2] = end_tangent;
376    matrix[control_count - 1][control_count - 1] = 1.0;
377    rhs_points[control_count - 1] = points[n];
378    let solve_axis = |axis: fn(Vec3) -> f64| {
379        solve_dense(
380            matrix.clone(),
381            rhs_points.iter().copied().map(axis).collect::<Vec<_>>(),
382        )
383    };
384    let xs = solve_axis(|point| point.x)?;
385    let ys = solve_axis(|point| point.y)?;
386    let zs = solve_axis(|point| point.z)?;
387    NurbsCurve::new(
388        degree,
389        knots,
390        (0..control_count)
391            .map(|index| Vec4::from_point(Vec3::new(xs[index], ys[index], zs[index]), 1.0))
392            .collect(),
393    )
394}
395
396pub fn interpolate_curve_thinned(
397    points: &[Vec3],
398    degree: usize,
399    maximum_points: usize,
400) -> Result<NurbsCurve, String> {
401    if maximum_points < 2 {
402        return Err("interpolate_curve_thinned: maximum point count must be at least 2".into());
403    }
404    if points.len() <= maximum_points {
405        let parameters = chord_parameters(points);
406        return interpolate_curve(points, degree, &parameters);
407    }
408    let step = (points.len() - 1) as f64 / (maximum_points - 1) as f64;
409    let thinned = (0..maximum_points)
410        .map(|index| points[(index as f64 * step).round() as usize])
411        .collect::<Vec<_>>();
412    let parameters = chord_parameters(&thinned);
413    interpolate_curve(&thinned, degree, &parameters)
414}
415
416fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
417    if points.len() < 2 {
418        return vec![0.0; points.len()];
419    }
420    let mut parameters = vec![0.0; points.len()];
421    for index in 1..points.len() {
422        parameters[index] = parameters[index - 1] + points[index].sub(points[index - 1]).length();
423    }
424    let length = parameters[points.len() - 1];
425    if length <= 1e-15 {
426        for (index, parameter) in parameters.iter_mut().enumerate() {
427            *parameter = index as f64 / (points.len() - 1) as f64;
428        }
429    } else {
430        for parameter in &mut parameters {
431            *parameter /= length;
432        }
433    }
434    parameters
435}
436
437pub fn simplify_polyline(points: &[Vec3], tolerance: f64) -> Vec<Vec3> {
438    if points.len() <= 2 {
439        return points.to_vec();
440    }
441    let mut keep = vec![false; points.len()];
442    keep[0] = true;
443    keep[points.len() - 1] = true;
444    let mut stack = vec![(0usize, points.len() - 1)];
445    while let Some((start, end)) = stack.pop() {
446        if end - start < 2 {
447            continue;
448        }
449        let a = points[start];
450        let direction = points[end].sub(a);
451        let length_squared = direction.length_squared().max(1e-300);
452        let mut worst = None;
453        let mut worst_distance = tolerance;
454        for (index, point) in points.iter().enumerate().take(end).skip(start + 1) {
455            let fraction = point.sub(a).dot(direction) / length_squared;
456            let fraction = fraction.clamp(0.0, 1.0);
457            let distance = point.sub(a.add(direction.scale(fraction))).length();
458            if distance > worst_distance {
459                worst_distance = distance;
460                worst = Some(index);
461            }
462        }
463        if let Some(index) = worst {
464            keep[index] = true;
465            stack.push((start, index));
466            stack.push((index, end));
467        }
468    }
469    points
470        .iter()
471        .copied()
472        .zip(keep)
473        .filter_map(|(point, keep)| keep.then_some(point))
474        .collect()
475}
476
477pub fn interpolate_curve_local(
478    points: &[Vec3],
479    parameters: &[f64],
480    tension: f64,
481) -> Result<NurbsCurve, String> {
482    if points.len() != parameters.len() || points.len() < 2 {
483        return Err(
484            "interpolate_curve_local: points and parameters must have matching length >= 2".into(),
485        );
486    }
487    if points.len() == 2 {
488        return interpolate_curve(points, 1, parameters);
489    }
490    let count = points.len();
491    let mut tangents = vec![Vec3::default(); count];
492    tangents[0] = points[1]
493        .sub(points[0])
494        .scale(1.0 / (parameters[1] - parameters[0]));
495    tangents[count - 1] = points[count - 1]
496        .sub(points[count - 2])
497        .scale(1.0 / (parameters[count - 1] - parameters[count - 2]));
498    for index in 1..count - 1 {
499        let previous_interval = parameters[index] - parameters[index - 1];
500        let next_interval = parameters[index + 1] - parameters[index];
501        let total = previous_interval + next_interval;
502        let previous_secant = points[index]
503            .sub(points[index - 1])
504            .scale(1.0 / previous_interval);
505        let next_secant = points[index + 1]
506            .sub(points[index])
507            .scale(1.0 / next_interval);
508        let mut tangent =
509            points[index - 1]
510                .scale(-next_interval / (previous_interval * total))
511                .add(points[index].scale(
512                    (next_interval - previous_interval) / (previous_interval * next_interval),
513                ))
514                .add(points[index + 1].scale(previous_interval / (next_interval * total)));
515        if previous_secant.dot(next_secant) <= 0.0
516            || tangent.dot(previous_secant) <= 0.0
517            || tangent.dot(next_secant) <= 0.0
518        {
519            tangent = Vec3::default();
520        } else {
521            let maximum = 3.0 * previous_secant.length().min(next_secant.length());
522            if tangent.length() > maximum {
523                tangent = tangent.normalized()?.scale(maximum);
524            }
525        }
526        tangents[index] = tangent;
527    }
528    if tension != 1.0 {
529        for tangent in &mut tangents {
530            *tangent = tangent.scale(tension);
531        }
532    }
533    let mut control_points = vec![Vec4::from_point(points[0], 1.0)];
534    for index in 0..count - 1 {
535        let interval = parameters[index + 1] - parameters[index];
536        control_points.extend([
537            Vec4::from_point(
538                points[index].add(tangents[index].scale(interval / 3.0)),
539                1.0,
540            ),
541            Vec4::from_point(
542                points[index + 1].sub(tangents[index + 1].scale(interval / 3.0)),
543                1.0,
544            ),
545            Vec4::from_point(points[index + 1], 1.0),
546        ]);
547    }
548    let mut knots = vec![parameters[0]; 4];
549    for parameter in &parameters[1..count - 1] {
550        knots.extend([*parameter; 3]);
551    }
552    knots.extend([parameters[count - 1]; 4]);
553    NurbsCurve::new(3, knots, control_points)
554}
555
556pub fn fit_polyline(
557    points: &[Vec3],
558    tolerance: f64,
559    maximum_points: usize,
560    local_interpolation: bool,
561) -> Result<PolylineFit, String> {
562    let mut kept = simplify_polyline(points, tolerance);
563    if kept.len() > 2 {
564        let total: f64 = kept
565            .windows(2)
566            .map(|pair| pair[1].sub(pair[0]).length())
567            .sum();
568        let floor = (tolerance * 0.01).max(total * 1e-4);
569        let first = kept[0];
570        let last = kept[kept.len() - 1];
571        let mut conditioned = vec![first];
572        for point in &kept[1..kept.len() - 1] {
573            if point.sub(first).length() > floor
574                && point.sub(last).length() > floor
575                && point.sub(*conditioned.last().unwrap()).length() > floor
576            {
577                conditioned.push(*point);
578            }
579        }
580        conditioned.push(last);
581        kept = conditioned;
582    } else {
583        let mut distinct = Vec::new();
584        for point in kept {
585            if distinct
586                .last()
587                .is_none_or(|previous: &Vec3| point.sub(*previous).length() > tolerance * 0.01)
588            {
589                distinct.push(point);
590            }
591        }
592        kept = distinct;
593    }
594    if kept.len() < 2 {
595        return Err("fit_polyline: degenerate polyline".into());
596    }
597    let maximum_points = maximum_points.max(2);
598    if kept.len() > maximum_points {
599        let step = (kept.len() - 1) as f64 / (maximum_points - 1) as f64;
600        kept = (0..maximum_points)
601            .map(|index| kept[(index as f64 * step).round() as usize])
602            .collect();
603    }
604    let total: f64 = kept
605        .windows(2)
606        .map(|pair| pair[1].sub(pair[0]).length())
607        .sum();
608    if total <= 0.0 {
609        return Err("fit_polyline: degenerate polyline".into());
610    }
611    let mut parameters = vec![0.0; kept.len()];
612    let mut accumulated = 0.0;
613    for index in 1..kept.len() {
614        accumulated += kept[index].sub(kept[index - 1]).length();
615        parameters[index] = accumulated / total;
616    }
617    *parameters.last_mut().unwrap() = 1.0;
618    let curve = if local_interpolation {
619        interpolate_curve_local(&kept, &parameters, 1.0)?
620    } else {
621        interpolate_curve(&kept, 3usize.min(kept.len() - 1), &parameters)?
622    };
623    Ok(PolylineFit {
624        curve,
625        parameters,
626        kept,
627    })
628}
629
630// BREP private tests: 7d3d630bf075d2f1
631
632// BREP private tests: 05cf36d4895e74b7
633
634/// Cox–de Boor basis over a RAW (possibly unclamped) knot array — the local
635/// helper the periodic interpolation needs; `KnotVector` validation rightly
636/// rejects unclamped arrays, so this stays private to the fit module.
637fn raw_basis(knots: &[f64], degree: usize, span: usize, parameter: f64) -> Vec<f64> {
638    let mut basis = vec![0.0; degree + 1];
639    let mut left = vec![0.0; degree + 1];
640    let mut right = vec![0.0; degree + 1];
641    basis[0] = 1.0;
642    for j in 1..=degree {
643        left[j] = parameter - knots[span + 1 - j];
644        right[j] = knots[span + j] - parameter;
645        let mut saved = 0.0;
646        for r in 0..j {
647            let denominator = right[r + 1] + left[j - r];
648            let temp = if denominator.abs() > 0.0 {
649                basis[r] / denominator
650            } else {
651                0.0
652            };
653            basis[r] = saved + right[r + 1] * temp;
654            saved = left[j - r] * temp;
655        }
656        basis[j] = saved;
657    }
658    basis
659}
660
661/// Boehm single-knot insertion on raw arrays (degree fixed by caller).
662fn raw_insert_knot(knots: &mut Vec<f64>, controls: &mut Vec<Vec3>, degree: usize, parameter: f64) {
663    // span: last index with knots[span] <= parameter, clamped to the valid
664    // control range (the textbook find_span clamp — inserting at the domain
665    // end otherwise indexes one past the control array).
666    let span = knots
667        .iter()
668        .rposition(|&knot| knot <= parameter + 1e-14)
669        .unwrap()
670        .min(controls.len() - 1);
671    let mut fresh = Vec::with_capacity(controls.len() + 1);
672    fresh.extend_from_slice(&controls[..=span - degree]);
673    for i in span - degree + 1..=span {
674        let denominator = knots[i + degree] - knots[i];
675        let alpha = if denominator.abs() > 0.0 {
676            (parameter - knots[i]) / denominator
677        } else {
678            0.0
679        };
680        fresh.push(
681            controls[i - 1]
682                .scale(1.0 - alpha)
683                .add(controls[i].scale(alpha)),
684        );
685    }
686    fresh.extend_from_slice(&controls[span..]);
687    *controls = fresh;
688    knots.insert(span + 1, parameter);
689}
690
691/// EXACT closed (periodic) cubic interpolation. `points` are the S >= 4
692/// distinct stations (first NOT repeated); `parameters` has S+1 strictly
693/// increasing values whose last entry closes the period. The cyclic
694/// collocation system is solved densely (S is small for lofts), and the
695/// periodic B-spline is re-expressed in CLAMPED form by Boehm-inserting the
696/// domain ends to full multiplicity — the representation every kernel
697/// consumer expects — so the seam is C² by construction, not by welding.
698pub fn interpolate_curve_closed(points: &[Vec3], parameters: &[f64]) -> Result<NurbsCurve, String> {
699    let degree = 3usize;
700    let station_count = points.len();
701    if station_count < 4 {
702        return Err("interpolate_curve_closed: need at least 4 stations".into());
703    }
704    if parameters.len() != station_count + 1 {
705        return Err(
706            "interpolate_curve_closed: parameters must have one more entry than points".into(),
707        );
708    }
709    if parameters.windows(2).any(|pair| pair[1] <= pair[0]) {
710        return Err("interpolate_curve_closed: parameters must increase".into());
711    }
712    let period = parameters[station_count] - parameters[0];
713    // Cyclic knot line u_j = t_{j mod S} + floor(j/S)·T for j in −3..S+4,
714    // stored with offset 3: raw[k] = u_{k−3}.
715    let cyclic = |j: i64| -> f64 {
716        let s = station_count as i64;
717        let wrap = j.div_euclid(s);
718        parameters[j.rem_euclid(s) as usize] + wrap as f64 * period
719    };
720    let raw_knots: Vec<f64> = (-3..=(station_count as i64 + 3)).map(cyclic).collect();
721    // Collocation: row i evaluates the cubic basis at t_i; the span in the
722    // raw array is the one containing t_i (raw index i+3 == u_i).
723    let mut matrix = vec![vec![0.0; station_count]; station_count];
724    for i in 0..station_count {
725        let span = i + 3;
726        let basis = raw_basis(&raw_knots, degree, span, parameters[i]);
727        for (offset, value) in basis.iter().enumerate() {
728            // Control j = span − degree + offset in unclamped indexing, i.e.
729            // cyclic control (i + offset − 3) mod S.
730            let index = (i as i64 + offset as i64 - 3).rem_euclid(station_count as i64) as usize;
731            matrix[i][index] += value;
732        }
733    }
734    let solve_axis = |axis: fn(Vec3) -> f64| {
735        solve_dense(
736            matrix.clone(),
737            points.iter().copied().map(axis).collect::<Vec<_>>(),
738        )
739    };
740    let xs = solve_axis(|point| point.x)?;
741    let ys = solve_axis(|point| point.y)?;
742    let zs = solve_axis(|point| point.z)?;
743    let cyclic_controls: Vec<Vec3> = (0..station_count)
744        .map(|index| Vec3::new(xs[index], ys[index], zs[index]))
745        .collect();
746    // Window covering [t_0, t_S]: controls D_{−3..S−1} cyclically.
747    let mut window_controls: Vec<Vec3> = (-3..(station_count as i64))
748        .map(|j| cyclic_controls[j.rem_euclid(station_count as i64) as usize])
749        .collect();
750    let mut window_knots = raw_knots.clone();
751    // Clamp both domain ends to full multiplicity (degree insertions each —
752    // the ends currently sit at multiplicity 1).
753    for _ in 0..degree {
754        raw_insert_knot(
755            &mut window_knots,
756            &mut window_controls,
757            degree,
758            parameters[0],
759        );
760    }
761    for _ in 0..degree {
762        raw_insert_knot(
763            &mut window_knots,
764            &mut window_controls,
765            degree,
766            parameters[station_count],
767        );
768    }
769    // Slice out the clamped sub-curve over [t_0, t_S]: knots from the first
770    // occurrence of t_0 through the last of t_S, controls aligned so that
771    // control k pairs with knot span k..k+degree+1.
772    let first = window_knots
773        .iter()
774        .position(|&knot| (knot - parameters[0]).abs() < 1e-12)
775        .ok_or("interpolate_curve_closed: clamp lost the start knot")?;
776    let last = window_knots
777        .iter()
778        .rposition(|&knot| (knot - parameters[station_count]).abs() < 1e-12)
779        .ok_or("interpolate_curve_closed: clamp lost the end knot")?;
780    let clamped_knots: Vec<f64> = window_knots[first..=last].to_vec();
781    let control_count = clamped_knots.len() - degree - 1;
782    let clamped_controls: Vec<Vec4> = window_controls[first..first + control_count]
783        .iter()
784        .map(|point| Vec4::from_point(*point, 1.0))
785        .collect();
786    NurbsCurve::new(degree, clamped_knots, clamped_controls)
787}
788
789// BREP private tests: aa6a4fea97fb3e37