1#![allow(
5 clippy::many_single_char_names,
6 clippy::similar_names,
7 clippy::suboptimal_flops,
8 clippy::needless_range_loop,
9 clippy::cast_precision_loss,
10 clippy::doc_markdown
11)]
12
13use crate::MathError;
14use crate::nurbs::basis::basis_funs;
15use crate::nurbs::fitting;
16use crate::nurbs::fitting::interpolate;
17use crate::nurbs::surface::NurbsSurface;
18use crate::vec::Point3;
19
20pub fn interpolate_surface(
46 points: &[Vec<Point3>],
47 degree_u: usize,
48 degree_v: usize,
49) -> Result<NurbsSurface, MathError> {
50 let num_rows = points.len();
51 if num_rows < 2 {
52 return Err(MathError::EmptyInput);
53 }
54
55 let num_cols = points[0].len();
56 if num_cols < 2 {
57 return Err(MathError::EmptyInput);
58 }
59
60 for row in points {
61 if row.len() != num_cols {
62 return Err(MathError::InvalidControlPointGrid {
63 expected_rows: num_rows,
64 expected_cols: num_cols,
65 });
66 }
67 }
68
69 let mut row_curves = Vec::with_capacity(num_rows);
71 for row in points {
72 let curve = interpolate(row, degree_v)?;
73 row_curves.push(curve);
74 }
75
76 let n_cp_v = row_curves[0].control_points().len();
79
80 let mut col_points: Vec<Vec<Point3>> = Vec::with_capacity(n_cp_v);
83 for j in 0..n_cp_v {
84 let col: Vec<Point3> = row_curves.iter().map(|c| c.control_points()[j]).collect();
85 col_points.push(col);
86 }
87
88 let mut col_curves = Vec::with_capacity(n_cp_v);
90 for col in &col_points {
91 let curve = interpolate(col, degree_u)?;
92 col_curves.push(curve);
93 }
94
95 let n_cp_u = col_curves[0].control_points().len();
97 let knots_u = col_curves[0].knots().to_vec();
98 let knots_v = row_curves[0].knots().to_vec();
99
100 let mut control_points: Vec<Vec<Point3>> = Vec::with_capacity(n_cp_u);
101 for i in 0..n_cp_u {
102 let row: Vec<Point3> = col_curves.iter().map(|c| c.control_points()[i]).collect();
103 control_points.push(row);
104 }
105
106 let weights: Vec<Vec<f64>> = control_points
107 .iter()
108 .map(|row| vec![1.0; row.len()])
109 .collect();
110
111 NurbsSurface::new(
112 degree_u,
113 degree_v,
114 knots_u,
115 knots_v,
116 control_points,
117 weights,
118 )
119}
120
121#[allow(
140 clippy::too_many_arguments,
141 clippy::too_many_lines,
142 clippy::cast_precision_loss,
143 clippy::needless_range_loop
144)]
145pub fn approximate_surface_lspia(
146 points: &[Vec<Point3>],
147 degree_u: usize,
148 degree_v: usize,
149 num_cps_u: usize,
150 num_cps_v: usize,
151 tolerance: f64,
152 max_iterations: usize,
153) -> Result<NurbsSurface, MathError> {
154 if points.is_empty() || points[0].is_empty() {
155 return Err(MathError::EmptyInput);
156 }
157 let rows = points.len();
158 let cols = points[0].len();
159 for row in points {
160 if row.len() != cols {
161 return Err(MathError::InvalidControlPointGrid {
162 expected_rows: rows,
163 expected_cols: cols,
164 });
165 }
166 }
167
168 let pu = degree_u.min(rows - 1);
169 let pv = degree_v.min(cols - 1);
170 let mu = num_cps_u.min(rows).max(pu + 1);
171 let mv = num_cps_v.min(cols).max(pv + 1);
172
173 let params_u = compute_grid_params_u(points, rows, cols);
174 let params_v = compute_grid_params_v(points, rows, cols);
175
176 let knots_u = fitting::build_approximation_knots(¶ms_u, pu, mu, rows);
177 let knots_v = fitting::build_approximation_knots(¶ms_v, pv, mv, cols);
178
179 let mut cps: Vec<Vec<Point3>> = Vec::with_capacity(mu);
181 for i in 0..mu {
182 let mut row = Vec::with_capacity(mv);
183 for j in 0..mv {
184 let ui = if mu > 1 { i * (rows - 1) / (mu - 1) } else { 0 };
185 let vj = if mv > 1 { j * (cols - 1) / (mv - 1) } else { 0 };
186 row.push(points[ui.min(rows - 1)][vj.min(cols - 1)]);
187 }
188 cps.push(row);
189 }
190
191 let weights = vec![vec![1.0; mv]; mu];
192
193 let basis_u_data: Vec<(usize, Vec<f64>)> = params_u
194 .iter()
195 .map(|&u| {
196 let span = fitting::find_span(u, pu, &knots_u, mu);
197 let n = basis_funs(span, u, pu, &knots_u);
198 (span, n)
199 })
200 .collect();
201 let basis_v_data: Vec<(usize, Vec<f64>)> = params_v
202 .iter()
203 .map(|&v| {
204 let span = fitting::find_span(v, pv, &knots_v, mv);
205 let n = basis_funs(span, v, pv, &knots_v);
206 (span, n)
207 })
208 .collect();
209
210 let mu_u = col_sum_lambda(&basis_u_data, pu, mu);
213 let mu_v = col_sum_lambda(&basis_v_data, pv, mv);
214 let lambda_max = mu_u * mu_v;
215 let step: f64 = if lambda_max < 1e-30 {
216 1.0
217 } else {
218 1.0 / lambda_max
219 };
220
221 for iter in 0..max_iterations {
222 let surface = NurbsSurface::new(
223 pu,
224 pv,
225 knots_u.clone(),
226 knots_v.clone(),
227 cps.clone(),
228 weights.clone(),
229 )?;
230
231 let mut max_err = 0.0f64;
232 let mut deltas = vec![vec![(0.0f64, 0.0f64, 0.0f64); mv]; mu];
233
234 for (i, (su, nu)) in basis_u_data.iter().enumerate() {
235 for (j, (sv, nv)) in basis_v_data.iter().enumerate() {
236 let q = surface.evaluate(params_u[i], params_v[j]);
237 let err_x = points[i][j].x() - q.x();
238 let err_y = points[i][j].y() - q.y();
239 let err_z = points[i][j].z() - q.z();
240 let err_mag = (err_x * err_x + err_y * err_y + err_z * err_z).sqrt();
241 max_err = max_err.max(err_mag);
242
243 for (ku, &bu) in nu.iter().enumerate() {
244 for (kv, &bv) in nv.iter().enumerate() {
245 let ci = su - pu + ku;
246 let cj = sv - pv + kv;
247 if ci < mu && cj < mv {
248 let w = bu * bv;
249 deltas[ci][cj].0 += w * err_x;
250 deltas[ci][cj].1 += w * err_y;
251 deltas[ci][cj].2 += w * err_z;
252 }
253 }
254 }
255 }
256 }
257
258 if max_err < tolerance {
259 return NurbsSurface::new(pu, pv, knots_u, knots_v, cps, weights);
260 }
261
262 for i in 0..mu {
264 for j in 0..mv {
265 cps[i][j] = Point3::new(
266 step.mul_add(deltas[i][j].0, cps[i][j].x()),
267 step.mul_add(deltas[i][j].1, cps[i][j].y()),
268 step.mul_add(deltas[i][j].2, cps[i][j].z()),
269 );
270 }
271 }
272
273 if iter == max_iterations - 1 {
274 return NurbsSurface::new(pu, pv, knots_u, knots_v, cps, weights);
275 }
276 }
277
278 NurbsSurface::new(pu, pv, knots_u, knots_v, cps, weights)
279}
280
281#[allow(clippy::cast_precision_loss)]
283fn compute_grid_params_u(points: &[Vec<Point3>], rows: usize, cols: usize) -> Vec<f64> {
284 let mut params = vec![0.0f64; rows];
285 for j in 0..cols {
286 let col: Vec<Point3> = (0..rows).map(|i| points[i][j]).collect();
287 let col_params = fitting::chord_length_params(&col);
288 for (i, &p) in col_params.iter().enumerate() {
289 params[i] += p;
290 }
291 }
292 let cols_f = cols as f64;
293 for p in &mut params {
294 *p /= cols_f;
295 }
296 params
297}
298
299#[allow(clippy::cast_precision_loss)]
301fn compute_grid_params_v(points: &[Vec<Point3>], rows: usize, cols: usize) -> Vec<f64> {
302 let mut params = vec![0.0f64; cols];
303 for i in 0..rows {
304 let row_params = fitting::chord_length_params(&points[i]);
305 for (j, &p) in row_params.iter().enumerate() {
306 params[j] += p;
307 }
308 }
309 let rows_f = rows as f64;
310 for p in &mut params {
311 *p /= rows_f;
312 }
313 params
314}
315
316fn col_sum_lambda(basis_data: &[(usize, Vec<f64>)], p: usize, m: usize) -> f64 {
318 let mut col_sums = vec![0.0f64; m];
319 for (span, n_vals) in basis_data {
320 for (k, &nv) in n_vals.iter().enumerate() {
321 let j = span - p + k;
322 if j < m {
323 col_sums[j] += nv * nv;
324 }
325 }
326 }
327 col_sums.iter().copied().fold(0.0f64, f64::max).max(1e-30)
328}
329
330#[cfg(test)]
331mod tests {
332 #![allow(clippy::unwrap_used, clippy::cast_lossless, clippy::suboptimal_flops)]
333
334 use crate::tolerance::Tolerance;
335 use crate::vec::Point3;
336
337 use super::*;
338
339 #[test]
340 fn interpolate_flat_grid() {
341 let points = vec![
343 vec![
344 Point3::new(0.0, 0.0, 0.0),
345 Point3::new(1.0, 0.0, 0.0),
346 Point3::new(2.0, 0.0, 0.0),
347 ],
348 vec![
349 Point3::new(0.0, 1.0, 0.0),
350 Point3::new(1.0, 1.0, 0.0),
351 Point3::new(2.0, 1.0, 0.0),
352 ],
353 vec![
354 Point3::new(0.0, 2.0, 0.0),
355 Point3::new(1.0, 2.0, 0.0),
356 Point3::new(2.0, 2.0, 0.0),
357 ],
358 ];
359
360 let surface = interpolate_surface(&points, 2, 2).unwrap();
361
362 let tol = Tolerance::new();
363
364 let p00 = surface.evaluate(0.0, 0.0);
366 assert!(tol.approx_eq(p00.x(), 0.0), "corner (0,0) x: {}", p00.x());
367 assert!(tol.approx_eq(p00.y(), 0.0), "corner (0,0) y: {}", p00.y());
368 assert!(tol.approx_eq(p00.z(), 0.0), "corner (0,0) z: {}", p00.z());
369
370 let p11 = surface.evaluate(1.0, 1.0);
371 assert!(tol.approx_eq(p11.x(), 2.0), "corner (1,1) x: {}", p11.x());
372 assert!(tol.approx_eq(p11.y(), 2.0), "corner (1,1) y: {}", p11.y());
373
374 let p_mid = surface.evaluate(0.5, 0.5);
376 assert!(
377 p_mid.z().abs() < 0.01,
378 "flat grid mid z should be ~0, got {}",
379 p_mid.z()
380 );
381 }
382
383 #[test]
384 fn interpolate_curved_grid() {
385 let points = vec![
387 vec![
388 Point3::new(0.0, 0.0, 0.0),
389 Point3::new(1.0, 0.0, 1.0),
390 Point3::new(2.0, 0.0, 4.0),
391 ],
392 vec![
393 Point3::new(0.0, 1.0, 1.0),
394 Point3::new(1.0, 1.0, 2.0),
395 Point3::new(2.0, 1.0, 5.0),
396 ],
397 vec![
398 Point3::new(0.0, 2.0, 4.0),
399 Point3::new(1.0, 2.0, 5.0),
400 Point3::new(2.0, 2.0, 8.0),
401 ],
402 ];
403
404 let surface = interpolate_surface(&points, 2, 2).unwrap();
405
406 let tol = Tolerance::new();
407 let p00 = surface.evaluate(0.0, 0.0);
409 assert!(tol.approx_eq(p00.z(), 0.0), "corner z: {}", p00.z());
410
411 let p11 = surface.evaluate(1.0, 1.0);
412 assert!(tol.approx_eq(p11.z(), 8.0), "far corner z: {}", p11.z());
413 }
414
415 #[test]
416 fn interpolate_surface_too_small() {
417 let points = vec![vec![Point3::new(0.0, 0.0, 0.0)]];
418 assert!(interpolate_surface(&points, 1, 1).is_err());
419 }
420
421 #[test]
422 fn interpolate_surface_inconsistent_rows() {
423 let points = vec![
424 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
425 vec![Point3::new(0.0, 1.0, 0.0)], ];
427 assert!(interpolate_surface(&points, 1, 1).is_err());
428 }
429
430 #[test]
433 #[allow(clippy::cast_precision_loss)]
434 fn lspia_surface_fits_plane() {
435 let grid: Vec<Vec<Point3>> = (0..10)
436 .map(|i| {
437 (0..10)
438 .map(|j| {
439 let u = i as f64 / 9.0;
440 let v = j as f64 / 9.0;
441 Point3::new(u, v, 0.5)
442 })
443 .collect()
444 })
445 .collect();
446 let surface = approximate_surface_lspia(&grid, 3, 3, 6, 6, 1e-6, 50).unwrap();
447 let p = surface.evaluate(0.5, 0.5);
448 assert!(
449 (p.z() - 0.5).abs() < 0.01,
450 "plane z at center: expected ~0.5, got {}",
451 p.z()
452 );
453 }
454
455 #[test]
456 fn lspia_surface_empty_returns_error() {
457 let grid: Vec<Vec<Point3>> = Vec::new();
458 assert!(approximate_surface_lspia(&grid, 3, 3, 4, 4, 1e-6, 50).is_err());
459 }
460
461 #[test]
462 fn lspia_surface_inconsistent_rows_returns_error() {
463 let grid = vec![
464 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)],
465 vec![Point3::new(0.0, 1.0, 0.0)],
466 ];
467 assert!(approximate_surface_lspia(&grid, 1, 1, 2, 2, 1e-6, 50).is_err());
468 }
469
470 #[test]
471 #[allow(clippy::cast_precision_loss)]
472 fn lspia_surface_fits_paraboloid() {
473 let grid: Vec<Vec<Point3>> = (0..8)
474 .map(|i| {
475 (0..8)
476 .map(|j| {
477 let u = i as f64 / 7.0;
478 let v = j as f64 / 7.0;
479 Point3::new(u, v, u * u + v * v)
480 })
481 .collect()
482 })
483 .collect();
484 let surface = approximate_surface_lspia(&grid, 3, 3, 6, 6, 1e-4, 100).unwrap();
485 let p = surface.evaluate(0.5, 0.5);
486 assert!(
488 (p.z() - 0.5).abs() < 0.15,
489 "paraboloid z at center: expected ~0.5, got {}",
490 p.z()
491 );
492 }
493}