1use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
18use axiolid_core::{Point3, Scalar};
19use axiolid_curve::{BSplineCurve, BSplineCurve3};
20use axiolid_evaluate::surface::bspline_jet;
21use axiolid_surface::BSplineSurface;
22
23use crate::surface_transform::u_curve;
24
25type Homogeneous = [Scalar; 4];
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct BoundedSurface {
31 pub surface: BSplineSurface,
33 pub deviation_upper_bound: Scalar,
36}
37
38#[derive(Debug, Clone, PartialEq)]
41pub struct SurfaceKnotRemoval {
42 pub surface: BSplineSurface,
45 pub removed: u32,
47 pub deviation_upper_bound: Scalar,
51}
52
53pub fn remove_surface_knot_u(
67 surface: &BSplineSurface,
68 parameter: Scalar,
69 times: u32,
70 tolerance: Scalar,
71) -> GeomResult<SurfaceKnotRemoval> {
72 bspline_jet(surface, 0.0, 0.0)?;
73 if !tolerance.is_finite() || tolerance < 0.0 {
74 return Err(GeomError::InvalidInput(
75 "tolerance must be finite and non-negative".to_owned(),
76 ));
77 }
78 let index = surface
79 .u_knots
80 .iter()
81 .position(|&k| k == parameter)
82 .ok_or_else(|| GeomError::InvalidInput(format!("{parameter} is not a u knot")))?;
83 if index == 0 || index + 1 == surface.u_knots.len() {
84 return Err(GeomError::InvalidInput(
85 "end knots bound the domain and cannot be removed".to_owned(),
86 ));
87 }
88 let rational = surface.weights.is_some();
89 let mut columns = homogeneous_columns(surface);
90 let mut removed = 0;
91 let mut bound = 0.0;
92 while removed < times && columns[0].knots.contains(¶meter) {
93 let Some((candidate, step)) = remove_once(&columns, parameter, rational) else {
94 break;
95 };
96 if bound + step > tolerance {
97 break;
98 }
99 columns = candidate;
100 bound += step;
101 removed += 1;
102 }
103 let surface = if removed == 0 {
104 surface.clone()
105 } else {
106 from_homogeneous_columns(surface, &columns)?
107 };
108 Ok(SurfaceKnotRemoval {
109 surface,
110 removed,
111 deviation_upper_bound: bound,
112 })
113}
114
115pub fn remove_surface_knot_v(
122 surface: &BSplineSurface,
123 parameter: Scalar,
124 times: u32,
125 tolerance: Scalar,
126) -> GeomResult<SurfaceKnotRemoval> {
127 let mut result = remove_surface_knot_u(&transpose(surface), parameter, times, tolerance)?;
128 result.surface = transpose(&result.surface);
129 Ok(result)
130}
131
132pub fn elevate_surface_degree_u(surface: &BSplineSurface) -> GeomResult<BSplineSurface> {
142 bspline_jet(surface, 0.0, 0.0)?;
143 let columns = (0..surface.control_points[0].len())
144 .map(|v| crate::degree::elevate_degree3(&u_curve(surface, v)))
145 .collect::<GeomResult<Vec<_>>>()?;
146 assemble_columns(surface, &columns)
147}
148
149pub fn elevate_surface_degree_v(surface: &BSplineSurface) -> GeomResult<BSplineSurface> {
155 Ok(transpose(&elevate_surface_degree_u(&transpose(surface))?))
156}
157
158pub fn reduce_surface_degree_u(
170 surface: &BSplineSurface,
171 tolerance: Scalar,
172) -> GeomResult<BoundedSurface> {
173 bspline_jet(surface, 0.0, 0.0)?;
174 if !tolerance.is_finite() || tolerance < 0.0 {
175 return Err(GeomError::InvalidInput(
176 "tolerance must be finite and non-negative".to_owned(),
177 ));
178 }
179 if surface.weights.is_some() {
180 return Err(GeomError::Unsupported {
181 backend: BackendId::new("nurbs"),
182 operation: Operation::SurfaceEvaluation,
183 });
184 }
185 if surface.u_degree < 2 {
186 return Err(GeomError::InvalidInput(
187 "a u degree of 1 cannot be reduced".to_owned(),
188 ));
189 }
190 let mut reduced = Vec::with_capacity(surface.control_points[0].len());
191 let mut bound: Scalar = 0.0;
192 for v in 0..surface.control_points[0].len() {
193 let column = u_curve(surface, v);
194 let candidate = crate::degree::reduce_degree3(&column, Scalar::MAX)?.curve;
197 let back = crate::degree::elevate_degree3(&candidate)?;
202 let original = bezier_form(&column)?;
203 if back.knots != original.knots
204 || back.multiplicities != original.multiplicities
205 || back.control_points.len() != original.control_points.len()
206 {
207 return Err(GeomError::Degenerate(
208 "reduced surface does not refine onto the original knots".to_owned(),
209 ));
210 }
211 for (a, b) in back.control_points.iter().zip(&original.control_points) {
212 bound = bound.max(a.distance(*b));
213 }
214 reduced.push(candidate);
215 }
216 if bound.is_nan() || bound > tolerance {
217 return Err(GeomError::Degenerate(format!(
218 "u degree is not reducible within tolerance: deviation {bound:.3e} exceeds {tolerance:.3e}"
219 )));
220 }
221 Ok(BoundedSurface {
222 surface: assemble_columns(surface, &reduced)?,
223 deviation_upper_bound: bound,
224 })
225}
226
227pub fn reduce_surface_degree_v(
233 surface: &BSplineSurface,
234 tolerance: Scalar,
235) -> GeomResult<BoundedSurface> {
236 let mut result = reduce_surface_degree_u(&transpose(surface), tolerance)?;
237 result.surface = transpose(&result.surface);
238 Ok(result)
239}
240
241pub fn iso_curve_at_u(surface: &BSplineSurface, parameter: Scalar) -> GeomResult<BSplineCurve3> {
251 bspline_jet(surface, 0.0, 0.0)?;
252 let degree = usize::from(surface.u_degree);
253 let knots = expand(&surface.u_knots, &surface.u_multiplicities);
254 let count = surface.control_points.len();
255 let (lo, hi) = (knots[degree], knots[count]);
256 if !(parameter >= lo && parameter <= hi) {
257 return Err(GeomError::InvalidInput(format!(
258 "u = {parameter} is outside the domain [{lo}, {hi}]"
259 )));
260 }
261 let span = find_span(&knots, count, degree, parameter);
262 let basis = basis_functions(&knots, span, degree, parameter);
263 let rows = surface.control_points[0].len();
264 let mut points = Vec::with_capacity(rows);
265 let mut weights = Vec::with_capacity(rows);
266 for v in 0..rows {
267 let mut h = [0.0; 4];
268 for (k, &n) in basis.iter().enumerate() {
269 let i = span - degree + k;
270 let p = surface.control_points[i][v];
271 let w = surface.weights.as_ref().map_or(1.0, |rows| rows[i][v]);
272 h[0] += n * w * p.x;
273 h[1] += n * w * p.y;
274 h[2] += n * w * p.z;
275 h[3] += n * w;
276 }
277 if surface.weights.is_some() {
278 if !(h[3].is_finite() && h[3] > 0.0) {
279 return Err(GeomError::Degenerate(
280 "iso-curve weight is not positive and finite".to_owned(),
281 ));
282 }
283 points.push(Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
284 weights.push(h[3]);
285 } else {
286 points.push(Point3::new(h[0], h[1], h[2]));
287 }
288 }
289 Ok(BSplineCurve {
290 degree: surface.v_degree,
291 control_points: points,
292 knots: surface.v_knots.clone(),
293 multiplicities: surface.v_multiplicities.clone(),
294 weights: surface.weights.as_ref().map(|_| weights),
295 knot_spec: surface.knot_spec,
296 closed: surface.v_closed,
297 self_intersect: surface.self_intersect,
298 })
299}
300
301pub fn iso_curve_at_v(surface: &BSplineSurface, parameter: Scalar) -> GeomResult<BSplineCurve3> {
308 iso_curve_at_u(&transpose(surface), parameter)
309}
310
311fn remove_once(
314 columns: &[BSplineCurve<Homogeneous>],
315 parameter: Scalar,
316 rational: bool,
317) -> Option<(Vec<BSplineCurve<Homogeneous>>, Scalar)> {
318 let mut candidate = Vec::with_capacity(columns.len());
319 let mut refined = Vec::with_capacity(columns.len());
320 for column in columns {
321 let removed = crate::degree::remove(column, parameter, |h| *h, |h| h).ok()?;
322 if rational
325 && removed
326 .control_points
327 .iter()
328 .any(|h| !(h[3].is_finite() && h[3] > 0.0))
329 {
330 return None;
331 }
332 let back = crate::transform::insert(&removed, parameter, |h| *h, |h| h).ok()?;
335 if back.control_points.len() != column.control_points.len() {
336 return None;
337 }
338 candidate.push(removed);
339 refined.push(back);
340 }
341 let bound = net_deviation(columns, &refined, rational)?;
342 Some((candidate, bound))
343}
344
345fn net_deviation(
355 first: &[BSplineCurve<Homogeneous>],
356 second: &[BSplineCurve<Homogeneous>],
357 rational: bool,
358) -> Option<Scalar> {
359 let pairs = || {
360 first
361 .iter()
362 .zip(second)
363 .flat_map(|(a, b)| a.control_points.iter().zip(&b.control_points))
364 };
365 if !rational {
366 return Some(
367 pairs()
368 .map(|(a, b)| {
369 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
370 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
371 })
372 .fold(0.0, Scalar::max),
373 );
374 }
375 let euclid = |h: &Homogeneous| [h[0] / h[3], h[1] / h[3], h[2] / h[3]];
376 let mut min = [Scalar::INFINITY; 3];
377 let mut max = [Scalar::NEG_INFINITY; 3];
378 for (_, b) in pairs() {
379 let p = euclid(b);
380 for k in 0..3 {
381 min[k] = min[k].min(p[k]);
382 max[k] = max[k].max(p[k]);
383 }
384 }
385 let c = [0, 1, 2].map(|k| 0.5 * (min[k] + max[k]));
386 let radius = pairs()
387 .map(|(_, b)| {
388 let p = euclid(b);
389 ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2) + (p[2] - c[2]).powi(2)).sqrt()
390 })
391 .fold(0.0, Scalar::max);
392 let mut worst: Scalar = 0.0;
393 let mut least_weight = Scalar::INFINITY;
394 for (a, b) in pairs() {
395 least_weight = least_weight.min(a[3]);
396 let d = [0, 1, 2].map(|k| (a[k] - c[k] * a[3]) - (b[k] - c[k] * b[3]));
397 let term = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() + radius * (a[3] - b[3]).abs();
398 worst = worst.max(term);
399 }
400 if least_weight.is_nan() || least_weight <= 0.0 {
401 return None;
402 }
403 let bound = worst / least_weight;
404 bound.is_finite().then_some(bound)
405}
406
407fn bezier_form(curve: &BSplineCurve3) -> GeomResult<BSplineCurve3> {
410 let degree = u32::from(curve.degree);
411 let mut refined = curve.clone();
412 let interior = &curve.knots[1..curve.knots.len() - 1];
413 for &knot in interior {
414 loop {
415 let index = refined
416 .knots
417 .iter()
418 .position(|&k| k == knot)
419 .expect("insertion keeps the knot");
420 if refined.multiplicities[index] >= degree {
421 break;
422 }
423 refined = crate::transform::insert_knot3(&refined, knot)?;
424 }
425 }
426 Ok(refined)
427}
428
429fn homogeneous_columns(surface: &BSplineSurface) -> Vec<BSplineCurve<Homogeneous>> {
431 (0..surface.control_points[0].len())
432 .map(|v| BSplineCurve {
433 degree: surface.u_degree,
434 control_points: surface
435 .control_points
436 .iter()
437 .enumerate()
438 .map(|(u, row)| {
439 let p = row[v];
440 let w = surface.weights.as_ref().map_or(1.0, |rows| rows[u][v]);
441 [w * p.x, w * p.y, w * p.z, w]
442 })
443 .collect(),
444 knots: surface.u_knots.clone(),
445 multiplicities: surface.u_multiplicities.clone(),
446 weights: None,
447 knot_spec: surface.knot_spec,
448 closed: surface.u_closed,
449 self_intersect: surface.self_intersect,
450 })
451 .collect()
452}
453
454fn from_homogeneous_columns(
456 template: &BSplineSurface,
457 columns: &[BSplineCurve<Homogeneous>],
458) -> GeomResult<BSplineSurface> {
459 let rational = template.weights.is_some();
460 let u_count = columns[0].control_points.len();
461 let mut control_points = vec![Vec::with_capacity(columns.len()); u_count];
462 let mut weights = vec![Vec::with_capacity(columns.len()); u_count];
463 for column in columns {
464 for (u, h) in column.control_points.iter().enumerate() {
465 if rational {
466 if !(h[3].is_finite() && h[3] > 0.0) {
467 return Err(GeomError::Degenerate(
468 "a weight is not positive and finite".to_owned(),
469 ));
470 }
471 control_points[u].push(Point3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
472 weights[u].push(h[3]);
473 } else {
474 control_points[u].push(Point3::new(h[0], h[1], h[2]));
476 }
477 }
478 }
479 Ok(BSplineSurface {
480 u_degree: columns[0].degree,
481 v_degree: template.v_degree,
482 control_points,
483 u_knots: columns[0].knots.clone(),
484 u_multiplicities: columns[0].multiplicities.clone(),
485 v_knots: template.v_knots.clone(),
486 v_multiplicities: template.v_multiplicities.clone(),
487 weights: rational.then_some(weights),
488 knot_spec: template.knot_spec,
489 u_closed: template.u_closed,
490 v_closed: template.v_closed,
491 self_intersect: template.self_intersect,
492 })
493}
494
495fn assemble_columns(
498 template: &BSplineSurface,
499 columns: &[BSplineCurve3],
500) -> GeomResult<BSplineSurface> {
501 let first = &columns[0];
502 if columns.iter().any(|c| {
503 c.degree != first.degree
504 || c.knots != first.knots
505 || c.multiplicities != first.multiplicities
506 || c.control_points.len() != first.control_points.len()
507 }) {
508 return Err(GeomError::Degenerate(
509 "columns disagree on their knot vector".to_owned(),
510 ));
511 }
512 let u_count = first.control_points.len();
513 let control_points = (0..u_count)
514 .map(|u| columns.iter().map(|c| c.control_points[u]).collect())
515 .collect();
516 let weights = template.weights.as_ref().map(|_| {
517 (0..u_count)
518 .map(|u| {
519 columns
520 .iter()
521 .map(|c| c.weights.as_ref().map_or(1.0, |w| w[u]))
522 .collect()
523 })
524 .collect()
525 });
526 Ok(BSplineSurface {
527 u_degree: first.degree,
528 v_degree: template.v_degree,
529 control_points,
530 u_knots: first.knots.clone(),
531 u_multiplicities: first.multiplicities.clone(),
532 v_knots: template.v_knots.clone(),
533 v_multiplicities: template.v_multiplicities.clone(),
534 weights,
535 knot_spec: template.knot_spec,
536 u_closed: template.u_closed,
537 v_closed: template.v_closed,
538 self_intersect: template.self_intersect,
539 })
540}
541
542fn transpose(surface: &BSplineSurface) -> BSplineSurface {
544 BSplineSurface {
545 u_degree: surface.v_degree,
546 v_degree: surface.u_degree,
547 control_points: flip(&surface.control_points),
548 u_knots: surface.v_knots.clone(),
549 u_multiplicities: surface.v_multiplicities.clone(),
550 v_knots: surface.u_knots.clone(),
551 v_multiplicities: surface.u_multiplicities.clone(),
552 weights: surface.weights.as_deref().map(flip),
553 knot_spec: surface.knot_spec,
554 u_closed: surface.v_closed,
555 v_closed: surface.u_closed,
556 self_intersect: surface.self_intersect,
557 }
558}
559
560fn flip<T: Copy>(net: &[Vec<T>]) -> Vec<Vec<T>> {
561 (0..net[0].len())
562 .map(|v| net.iter().map(|row| row[v]).collect())
563 .collect()
564}
565
566fn expand(knots: &[Scalar], multiplicities: &[u32]) -> Vec<Scalar> {
567 let mut out = Vec::new();
568 for (&k, &m) in knots.iter().zip(multiplicities) {
569 out.extend(core::iter::repeat_n(k, m as usize));
570 }
571 out
572}
573
574fn find_span(knots: &[Scalar], count: usize, degree: usize, t: Scalar) -> usize {
577 if t >= knots[count] {
578 let mut span = count - 1;
579 while span > degree && knots[span] == knots[count] {
580 span -= 1;
581 }
582 return span;
583 }
584 let mut span = degree;
585 while span + 1 < count && knots[span + 1] <= t {
586 span += 1;
587 }
588 span
589}
590
591fn basis_functions(knots: &[Scalar], span: usize, degree: usize, t: Scalar) -> Vec<Scalar> {
594 let mut n = vec![0.0; degree + 1];
595 let mut left = vec![0.0; degree + 1];
596 let mut right = vec![0.0; degree + 1];
597 n[0] = 1.0;
598 for j in 1..=degree {
599 left[j] = t - knots[span + 1 - j];
600 right[j] = knots[span + j] - t;
601 let mut saved = 0.0;
602 for r in 0..j {
603 let temp = n[r] / (right[r + 1] + left[j - r]);
604 n[r] = saved + right[r + 1] * temp;
605 saved = left[j - r] * temp;
606 }
607 n[j] = saved;
608 }
609 n
610}