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
57fn 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
106fn 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
124fn 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 for row in (column + 1)..n {
165 let factor = matrix[row][column] / matrix[column][column];
166 if factor == 0.0 {
167 continue;
168 }
169 for k in column..n {
170 matrix[row][k] -= factor * matrix[column][k];
171 }
172 for axis in 0..3 {
173 rhs[row][axis] -= factor * rhs[column][axis];
174 }
175 }
176 }
177
178 let mut solution = vec![[0.0; 3]; n];
179 for row in (0..n).rev() {
180 let mut accumulated = rhs[row];
181 for (k, solved) in solution.iter().enumerate().take(n).skip(row + 1) {
182 for (axis, value) in accumulated.iter_mut().enumerate() {
183 *value -= matrix[row][k] * solved[axis];
184 }
185 }
186 let pivot = matrix[row][row];
187 for (axis, value) in solution[row].iter_mut().enumerate() {
188 *value = accumulated[axis] / pivot;
189 }
190 }
191 Ok(solution)
192}
193
194fn interpolate_with(points: &[Point3], parameters: &[Scalar]) -> GeomResult<BSplineCurve3> {
196 let count = points.len();
197 let degree = usize::from(degree_for(count));
198 let expanded = averaged_knots(parameters, degree);
199 let n = count - 1;
200
201 let mut matrix = vec![vec![0.0; count]; count];
204 let mut rhs = vec![[0.0; 3]; count];
205 for (row, (&t, point)) in parameters.iter().zip(points).enumerate() {
206 let span = span_of(&expanded, n, degree, t);
207 let basis = basis_at(span, t, degree, &expanded);
208 for (offset, value) in basis.iter().enumerate() {
209 matrix[row][span - degree + offset] = *value;
210 }
211 rhs[row] = [point.x, point.y, point.z];
212 }
213
214 let solved = solve(matrix, rhs)?;
215 let control_points: Vec<Point3> = solved
216 .into_iter()
217 .map(|c| Point3::new(c[0], c[1], c[2]))
218 .collect();
219
220 let (knots, multiplicities) = collapse(&expanded);
223
224 Ok(BSplineCurve3 {
225 degree: u16::try_from(degree)
226 .map_err(|_| GeomError::InvalidInput("degree overflows".to_owned()))?,
227 control_points,
228 knots,
229 multiplicities,
230 weights: None,
231 knot_spec: KnotSpec::Unspecified,
232 closed: false,
233 self_intersect: None,
234 })
235}
236
237fn collapse(expanded: &[Scalar]) -> (Vec<Scalar>, Vec<u32>) {
239 let mut knots: Vec<Scalar> = Vec::new();
240 let mut multiplicities: Vec<u32> = Vec::new();
241 for &knot in expanded {
242 if knots.last().is_some_and(|&last| last == knot) {
243 *multiplicities
244 .last_mut()
245 .expect("knots and counts stay in step") += 1;
246 } else {
247 knots.push(knot);
248 multiplicities.push(1);
249 }
250 }
251 (knots, multiplicities)
252}
253
254pub fn loft_surface(sections: &[BSplineCurve3]) -> GeomResult<BSplineSurface> {
267 if sections.len() < 2 {
268 return Err(GeomError::InvalidInput(
269 "lofting needs at least two sections".to_owned(),
270 ));
271 }
272
273 let first = §ions[0];
274 let width = first.control_points.len();
275 for (index, section) in sections.iter().enumerate() {
276 if section.degree != first.degree {
277 return Err(GeomError::InvalidInput(format!(
278 "section {index} has degree {} but section 0 has degree {}; \
279 elevate explicitly rather than having the loft change your curves",
280 section.degree, first.degree
281 )));
282 }
283 if section.control_points.len() != width {
284 return Err(GeomError::InvalidInput(format!(
285 "section {index} has {} control points but section 0 has {width}; \
286 sections must share a control net width",
287 section.control_points.len()
288 )));
289 }
290 if section.weights.is_some() {
291 return Err(GeomError::Unsupported {
292 backend: axiolid_contracts::BackendId::new("nurbs"),
293 operation: axiolid_contracts::Operation::SurfaceEvaluation,
294 });
295 }
296 }
297
298 let mut spans = vec![0.0];
302 let mut total = 0.0;
303 for pair in sections.windows(2) {
304 let mean: Scalar = pair[0]
305 .control_points
306 .iter()
307 .zip(&pair[1].control_points)
308 .map(|(a, b)| (*b - *a).length())
309 .sum::<Scalar>()
310 / width as Scalar;
311 if mean <= 0.0 {
312 return Err(GeomError::Degenerate(
313 "consecutive sections coincide, so loft spacing is undefined".to_owned(),
314 ));
315 }
316 total += mean;
317 spans.push(total);
318 }
319 let u_parameters: Vec<Scalar> = spans.into_iter().map(|d| d / total).collect();
320
321 let u_degree = usize::from(degree_for(sections.len()));
325 let u_expanded = averaged_knots(&u_parameters, u_degree);
326 let rows = sections.len();
327 let n = rows - 1;
328
329 let mut matrix = vec![vec![0.0; rows]; rows];
330 for (row, &t) in u_parameters.iter().enumerate() {
331 let span = span_of(&u_expanded, n, u_degree, t);
332 let basis = basis_at(span, t, u_degree, &u_expanded);
333 for (offset, value) in basis.iter().enumerate() {
334 matrix[row][span - u_degree + offset] = *value;
335 }
336 }
337
338 let mut net: Vec<Vec<Point3>> = vec![Vec::with_capacity(width); rows];
339 for column in 0..width {
340 let rhs: Vec<[Scalar; 3]> = sections
341 .iter()
342 .map(|s| {
343 let p = s.control_points[column];
344 [p.x, p.y, p.z]
345 })
346 .collect();
347 let solved = solve(matrix.clone(), rhs)?;
348 for (row, coordinate) in solved.into_iter().enumerate() {
349 net[row].push(Point3::new(coordinate[0], coordinate[1], coordinate[2]));
350 }
351 }
352
353 let (u_knots, u_multiplicities) = collapse(&u_expanded);
354
355 Ok(BSplineSurface {
356 u_degree: u16::try_from(u_degree)
357 .map_err(|_| GeomError::InvalidInput("u degree overflows".to_owned()))?,
358 v_degree: first.degree,
359 control_points: net,
360 u_knots,
361 u_multiplicities,
362 v_knots: first.knots.clone(),
365 v_multiplicities: first.multiplicities.clone(),
366 weights: None,
367 u_closed: false,
368 v_closed: first.closed,
369 knot_spec: KnotSpec::Unspecified,
370 self_intersect: None,
371 })
372}