1use axiolid_contracts::{GeomError, GeomResult};
30use axiolid_core::{Point3, Scalar};
31use axiolid_curve::{BSplineCurve3, KnotSpec};
32use axiolid_surface::BSplineSurface;
33
34pub fn interpolate_curve3(points: &[Point3]) -> GeomResult<BSplineCurve3> {
43 if points.len() < 2 {
44 return Err(GeomError::InvalidInput(
45 "interpolation needs at least two points".to_owned(),
46 ));
47 }
48 if !points.iter().all(|p| p.is_finite()) {
49 return Err(GeomError::InvalidInput(
50 "interpolation points must be finite".to_owned(),
51 ));
52 }
53 let parameters = chord_parameters(points)?;
54 interpolate_with(points, ¶meters)
55}
56
57pub(crate) fn chord_parameters(points: &[Point3]) -> GeomResult<Vec<Scalar>> {
59 let mut distances = Vec::with_capacity(points.len());
60 distances.push(0.0);
61 let mut total = 0.0;
62 for pair in points.windows(2) {
63 let step = (pair[1] - pair[0]).length();
64 if step <= 0.0 {
65 return Err(GeomError::Degenerate(
66 "consecutive interpolation points coincide, so chord length is undefined"
67 .to_owned(),
68 ));
69 }
70 total += step;
71 distances.push(total);
72 }
73 Ok(distances.into_iter().map(|d| d / total).collect())
74}
75
76fn degree_for(count: usize) -> u16 {
82 match count {
83 0 | 1 => 0,
84 2 => 1,
85 3 => 2,
86 _ => 3,
87 }
88}
89
90fn averaged_knots(parameters: &[Scalar], degree: usize) -> Vec<Scalar> {
96 let n = parameters.len() - 1;
97 let mut knots = vec![0.0; degree + 1];
98 for j in 1..=n.saturating_sub(degree) {
99 let sum: Scalar = parameters[j..j + degree].iter().sum();
100 knots.push(sum / degree as Scalar);
101 }
102 knots.extend(core::iter::repeat_n(1.0, degree + 1));
103 knots
104}
105
106pub(crate) fn span_of(knots: &[Scalar], n: usize, degree: usize, t: Scalar) -> usize {
108 if t >= knots[n + 1] {
109 return n;
110 }
111 let (mut lo, mut hi) = (degree, n + 1);
112 let mut mid = lo.midpoint(hi);
113 while t < knots[mid] || t >= knots[mid + 1] {
114 if t < knots[mid] {
115 hi = mid;
116 } else {
117 lo = mid;
118 }
119 mid = lo.midpoint(hi);
120 }
121 mid
122}
123
124pub(crate) fn basis_at(span: usize, t: Scalar, degree: usize, knots: &[Scalar]) -> Vec<Scalar> {
126 let mut basis = vec![0.0; degree + 1];
127 let mut left = vec![0.0; degree + 1];
128 let mut right = vec![0.0; degree + 1];
129 basis[0] = 1.0;
130 for j in 1..=degree {
131 left[j] = t - knots[span + 1 - j];
132 right[j] = knots[span + j] - t;
133 let mut saved = 0.0;
134 for r in 0..j {
135 let temp = basis[r] / (right[r + 1] + left[j - r]);
136 basis[r] = saved + right[r + 1] * temp;
137 saved = left[j - r] * temp;
138 }
139 basis[j] = saved;
140 }
141 basis
142}
143
144fn solve(mut matrix: Vec<Vec<Scalar>>, mut rhs: Vec<[Scalar; 3]>) -> GeomResult<Vec<[Scalar; 3]>> {
151 let n = matrix.len();
152 for column in 0..n {
153 let pivot = (column..n)
154 .max_by(|&a, &b| matrix[a][column].abs().total_cmp(&matrix[b][column].abs()))
155 .expect("range is non-empty");
156 if matrix[pivot][column].abs() < 1e-12 {
157 return Err(GeomError::Degenerate(
158 "interpolation system is singular for these points".to_owned(),
159 ));
160 }
161 matrix.swap(column, pivot);
162 rhs.swap(column, pivot);
163
164 let pivot_row = matrix[column].clone();
165 let pivot_rhs = rhs[column];
166 for row in (column + 1)..n {
167 let factor = matrix[row][column] / matrix[column][column];
168 if factor == 0.0 {
169 continue;
170 }
171 for (slot, pivot) in matrix[row].iter_mut().zip(&pivot_row).skip(column) {
172 *slot -= factor * pivot;
173 }
174 for (slot, pivot) in rhs[row].iter_mut().zip(pivot_rhs) {
175 *slot -= factor * pivot;
176 }
177 }
178 }
179
180 let mut solution = vec![[0.0; 3]; n];
181 for row in (0..n).rev() {
182 let mut accumulated = rhs[row];
183 for (k, solved) in solution.iter().enumerate().take(n).skip(row + 1) {
184 for (axis, value) in accumulated.iter_mut().enumerate() {
185 *value -= matrix[row][k] * solved[axis];
186 }
187 }
188 let pivot = matrix[row][row];
189 for (axis, value) in solution[row].iter_mut().enumerate() {
190 *value = accumulated[axis] / pivot;
191 }
192 }
193 Ok(solution)
194}
195
196fn interpolate_with(points: &[Point3], parameters: &[Scalar]) -> GeomResult<BSplineCurve3> {
198 let count = points.len();
199 let degree = usize::from(degree_for(count));
200 let expanded = averaged_knots(parameters, degree);
201 let n = count - 1;
202
203 let mut matrix = vec![vec![0.0; count]; count];
206 let mut rhs = vec![[0.0; 3]; count];
207 for (row, (&t, point)) in parameters.iter().zip(points).enumerate() {
208 let span = span_of(&expanded, n, degree, t);
209 let basis = basis_at(span, t, degree, &expanded);
210 for (offset, value) in basis.iter().enumerate() {
211 matrix[row][span - degree + offset] = *value;
212 }
213 rhs[row] = [point.x, point.y, point.z];
214 }
215
216 let solved = solve(matrix, rhs)?;
217 let control_points: Vec<Point3> = solved
218 .into_iter()
219 .map(|c| Point3::new(c[0], c[1], c[2]))
220 .collect();
221
222 let (knots, multiplicities) = collapse(&expanded);
225
226 Ok(BSplineCurve3 {
227 degree: u16::try_from(degree)
228 .map_err(|_| GeomError::InvalidInput("degree overflows".to_owned()))?,
229 control_points,
230 knots,
231 multiplicities,
232 weights: None,
233 knot_spec: KnotSpec::Unspecified,
234 closed: false,
235 self_intersect: None,
236 })
237}
238
239pub(crate) fn collapse(expanded: &[Scalar]) -> (Vec<Scalar>, Vec<u32>) {
241 let mut knots: Vec<Scalar> = Vec::new();
242 let mut multiplicities: Vec<u32> = Vec::new();
243 for &knot in expanded {
244 if knots.last().is_some_and(|&last| last == knot) {
245 *multiplicities
246 .last_mut()
247 .expect("knots and counts stay in step") += 1;
248 } else {
249 knots.push(knot);
250 multiplicities.push(1);
251 }
252 }
253 (knots, multiplicities)
254}
255
256pub fn loft_surface(sections: &[BSplineCurve3]) -> GeomResult<BSplineSurface> {
269 if sections.len() < 2 {
270 return Err(GeomError::InvalidInput(
271 "lofting needs at least two sections".to_owned(),
272 ));
273 }
274
275 let first = §ions[0];
276 let width = first.control_points.len();
277 for (index, section) in sections.iter().enumerate() {
278 if section.degree != first.degree {
279 return Err(GeomError::InvalidInput(format!(
280 "section {index} has degree {} but section 0 has degree {}; \
281 elevate explicitly rather than having the loft change your curves",
282 section.degree, first.degree
283 )));
284 }
285 if section.control_points.len() != width {
286 return Err(GeomError::InvalidInput(format!(
287 "section {index} has {} control points but section 0 has {width}; \
288 sections must share a control net width",
289 section.control_points.len()
290 )));
291 }
292 if section.weights.is_some() {
293 return Err(GeomError::Unsupported {
294 backend: axiolid_contracts::BackendId::new("nurbs"),
295 operation: axiolid_contracts::Operation::SurfaceEvaluation,
296 });
297 }
298 }
299
300 let mut spans = vec![0.0];
304 let mut total = 0.0;
305 for pair in sections.windows(2) {
306 let mean: Scalar = pair[0]
307 .control_points
308 .iter()
309 .zip(&pair[1].control_points)
310 .map(|(a, b)| (*b - *a).length())
311 .sum::<Scalar>()
312 / width as Scalar;
313 if mean <= 0.0 {
314 return Err(GeomError::Degenerate(
315 "consecutive sections coincide, so loft spacing is undefined".to_owned(),
316 ));
317 }
318 total += mean;
319 spans.push(total);
320 }
321 let u_parameters: Vec<Scalar> = spans.into_iter().map(|d| d / total).collect();
322
323 let u_degree = usize::from(degree_for(sections.len()));
327 let u_expanded = averaged_knots(&u_parameters, u_degree);
328 let rows = sections.len();
329 let n = rows - 1;
330
331 let mut matrix = vec![vec![0.0; rows]; rows];
332 for (row, &t) in u_parameters.iter().enumerate() {
333 let span = span_of(&u_expanded, n, u_degree, t);
334 let basis = basis_at(span, t, u_degree, &u_expanded);
335 for (offset, value) in basis.iter().enumerate() {
336 matrix[row][span - u_degree + offset] = *value;
337 }
338 }
339
340 let mut net: Vec<Vec<Point3>> = vec![Vec::with_capacity(width); rows];
341 for column in 0..width {
342 let rhs: Vec<[Scalar; 3]> = sections
343 .iter()
344 .map(|s| {
345 let p = s.control_points[column];
346 [p.x, p.y, p.z]
347 })
348 .collect();
349 let solved = solve(matrix.clone(), rhs)?;
350 for (row, coordinate) in solved.into_iter().enumerate() {
351 net[row].push(Point3::new(coordinate[0], coordinate[1], coordinate[2]));
352 }
353 }
354
355 let (u_knots, u_multiplicities) = collapse(&u_expanded);
356
357 Ok(BSplineSurface {
358 u_degree: u16::try_from(u_degree)
359 .map_err(|_| GeomError::InvalidInput("u degree overflows".to_owned()))?,
360 v_degree: first.degree,
361 control_points: net,
362 u_knots,
363 u_multiplicities,
364 v_knots: first.knots.clone(),
367 v_multiplicities: first.multiplicities.clone(),
368 weights: None,
369 u_closed: false,
370 v_closed: first.closed,
371 knot_spec: KnotSpec::Unspecified,
372 self_intersect: None,
373 })
374}