1use axiolid_contracts::{GeomError, GeomResult};
21use axiolid_core::{Point2, Point3, Scalar};
22use axiolid_curve::{BSplineCurve, BSplineCurve2, BSplineCurve3};
23use axiolid_evaluate::curve::{bspline_jet2, bspline_jet3};
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct BoundedResult<C> {
28 pub curve: C,
30 pub deviation_upper_bound: Scalar,
36}
37
38pub fn elevate_degree2(curve: &BSplineCurve2) -> GeomResult<BSplineCurve2> {
44 bspline_jet2(curve, 0.0)?;
45 let segments = crate::transform::bezier_segments2(curve)?;
46 let extract = |p: &Point2| [p.x, p.y];
47 let rebuild = |c: [Scalar; 2]| Point2::new(c[0], c[1]);
48 assemble(curve, &segments, extract, rebuild)
49}
50
51pub fn elevate_degree3(curve: &BSplineCurve3) -> GeomResult<BSplineCurve3> {
53 bspline_jet3(curve, 0.0)?;
54 let segments = crate::transform::bezier_segments3(curve)?;
55 let extract = |p: &Point3| [p.x, p.y, p.z];
56 let rebuild = |c: [Scalar; 3]| Point3::new(c[0], c[1], c[2]);
57 assemble(curve, &segments, extract, rebuild)
58}
59
60fn elevate_bezier<const N: usize>(
67 points: &[[Scalar; N]],
68 weights: &[Scalar],
69) -> (Vec<[Scalar; N]>, Vec<Scalar>) {
70 let p = points.len() - 1;
71 let elevated = p + 2;
72 let mut out_points = Vec::with_capacity(elevated);
73 let mut out_weights = Vec::with_capacity(elevated);
74
75 out_points.push(points[0]);
76 out_weights.push(weights[0]);
77 for i in 1..=p {
78 let alpha = i as Scalar / (p as Scalar + 1.0);
79 let mut coordinate = [0.0; N];
80 for (axis, value) in coordinate.iter_mut().enumerate() {
81 *value = alpha * points[i - 1][axis] * weights[i - 1]
83 + (1.0 - alpha) * points[i][axis] * weights[i];
84 }
85 let weight = alpha * weights[i - 1] + (1.0 - alpha) * weights[i];
86 for value in &mut coordinate {
87 *value /= weight;
88 }
89 out_points.push(coordinate);
90 out_weights.push(weight);
91 }
92 out_points.push(points[p]);
93 out_weights.push(weights[p]);
94 (out_points, out_weights)
95}
96
97fn assemble<const N: usize, P: Clone>(
106 curve: &BSplineCurve<P>,
107 segments: &[BSplineCurve<P>],
108 coordinates: impl Fn(&P) -> [Scalar; N],
109 point: impl Fn([Scalar; N]) -> P,
110) -> GeomResult<BSplineCurve<P>> {
111 let elevated_degree = curve.degree.checked_add(1).ok_or_else(|| {
112 GeomError::InvalidInput("degree elevation would overflow the degree".to_owned())
113 })?;
114
115 let mut control_points: Vec<P> = Vec::new();
116 let mut weights: Vec<Scalar> = Vec::new();
117 for (index, segment) in segments.iter().enumerate() {
118 let points: Vec<[Scalar; N]> = segment.control_points.iter().map(&coordinates).collect();
119 let segment_weights = segment
120 .weights
121 .clone()
122 .unwrap_or_else(|| vec![1.0; points.len()]);
123 let (elevated_points, elevated_weights) = elevate_bezier(&points, &segment_weights);
124
125 let skip = usize::from(index > 0);
127 for (coordinate, weight) in elevated_points.into_iter().zip(elevated_weights).skip(skip) {
128 control_points.push(point(coordinate));
129 weights.push(weight);
130 }
131 }
132
133 let clamped = u32::from(elevated_degree) + 1;
137 let internal = u32::from(elevated_degree);
138 let mut knots: Vec<Scalar> = Vec::with_capacity(segments.len() + 1);
139 let mut multiplicities: Vec<u32> = Vec::with_capacity(segments.len() + 1);
140
141 for (index, segment) in segments.iter().enumerate() {
142 let first = *segment
143 .knots
144 .first()
145 .ok_or_else(|| GeomError::InvalidInput("bezier segment has no knots".to_owned()))?;
146 if index == 0 {
147 knots.push(first);
148 multiplicities.push(clamped);
149 }
150 let last = *segment
151 .knots
152 .last()
153 .ok_or_else(|| GeomError::InvalidInput("bezier segment has no knots".to_owned()))?;
154 knots.push(last);
155 multiplicities.push(if index + 1 == segments.len() {
156 clamped
157 } else {
158 internal
159 });
160 }
161
162 let weights = curve.weights.as_ref().map(|_| weights);
166
167 Ok(BSplineCurve {
168 degree: elevated_degree,
169 control_points,
170 knots,
171 multiplicities,
172 weights,
173 knot_spec: curve.knot_spec,
174 closed: curve.closed,
175 self_intersect: curve.self_intersect,
176 })
177}
178
179pub fn remove_knot2(
192 curve: &BSplineCurve2,
193 parameter: Scalar,
194 tolerance: Scalar,
195) -> GeomResult<BoundedResult<BSplineCurve2>> {
196 bspline_jet2(curve, parameter)?;
197 let candidate = remove(
198 curve,
199 parameter,
200 |p| [p.x, p.y],
201 |c| Point2::new(c[0], c[1]),
202 )?;
203 let deviation = deviation2(curve, &candidate)?;
204 accept(candidate, deviation, tolerance)
205}
206
207pub fn remove_knot3(
209 curve: &BSplineCurve3,
210 parameter: Scalar,
211 tolerance: Scalar,
212) -> GeomResult<BoundedResult<BSplineCurve3>> {
213 bspline_jet3(curve, parameter)?;
214 let candidate = remove(
215 curve,
216 parameter,
217 |p| [p.x, p.y, p.z],
218 |c| Point3::new(c[0], c[1], c[2]),
219 )?;
220 let deviation = deviation3(curve, &candidate)?;
221 accept(candidate, deviation, tolerance)
222}
223
224fn accept<C>(curve: C, deviation: Scalar, tolerance: Scalar) -> GeomResult<BoundedResult<C>> {
226 if !tolerance.is_finite() || tolerance < 0.0 {
227 return Err(GeomError::InvalidInput(
228 "tolerance must be finite and non-negative".to_owned(),
229 ));
230 }
231 if deviation > tolerance {
232 return Err(GeomError::Degenerate(format!(
233 "knot is not removable within tolerance: deviation {deviation:.3e} exceeds {tolerance:.3e}"
234 )));
235 }
236 Ok(BoundedResult {
237 curve,
238 deviation_upper_bound: deviation,
239 })
240}
241
242const DEVIATION_SAMPLES: usize = 128;
247
248fn deviation2(original: &BSplineCurve2, candidate: &BSplineCurve2) -> GeomResult<Scalar> {
249 let (lo, hi) = domain(original);
250 let mut worst: Scalar = 0.0;
251 for index in 0..=DEVIATION_SAMPLES {
252 let t = lo + (hi - lo) * (index as Scalar / DEVIATION_SAMPLES as Scalar);
253 let a = bspline_jet2(original, t)?.point;
254 let b = bspline_jet2(candidate, t)?.point;
255 worst = worst.max((a - b).length());
256 }
257 Ok(worst)
258}
259
260fn deviation3(original: &BSplineCurve3, candidate: &BSplineCurve3) -> GeomResult<Scalar> {
261 let (lo, hi) = domain(original);
262 let mut worst: Scalar = 0.0;
263 for index in 0..=DEVIATION_SAMPLES {
264 let t = lo + (hi - lo) * (index as Scalar / DEVIATION_SAMPLES as Scalar);
265 let a = bspline_jet3(original, t)?.point;
266 let b = bspline_jet3(candidate, t)?.point;
267 worst = worst.max((a - b).length());
268 }
269 Ok(worst)
270}
271
272fn domain<P>(curve: &BSplineCurve<P>) -> (Scalar, Scalar) {
274 let mut expanded = Vec::new();
275 for (&knot, &multiplicity) in curve.knots.iter().zip(&curve.multiplicities) {
276 expanded.extend(core::iter::repeat_n(knot, multiplicity as usize));
277 }
278 let lo = expanded[usize::from(curve.degree)];
279 let hi = expanded[curve.control_points.len()];
280 (lo, hi)
281}
282
283pub(crate) fn remove<const N: usize, P: Clone>(
291 curve: &BSplineCurve<P>,
292 parameter: Scalar,
293 coordinates: impl Fn(&P) -> [Scalar; N],
294 point: impl Fn([Scalar; N]) -> P,
295) -> GeomResult<BSplineCurve<P>> {
296 if !parameter.is_finite() {
297 return Err(GeomError::InvalidInput(
298 "knot parameter must be finite".to_owned(),
299 ));
300 }
301 if curve.weights.is_some() {
302 return Err(GeomError::Unsupported {
303 backend: axiolid_contracts::BackendId::new("nurbs"),
304 operation: axiolid_contracts::Operation::CurveEvaluation,
305 });
306 }
307
308 let index = curve
309 .knots
310 .iter()
311 .position(|&k| k == parameter)
312 .ok_or_else(|| GeomError::InvalidInput("knot is not present in the curve".to_owned()))?;
313 if index == 0 || index + 1 == curve.knots.len() {
314 return Err(GeomError::InvalidInput(
315 "endpoint knots bound the domain and cannot be removed".to_owned(),
316 ));
317 }
318
319 let degree = usize::from(curve.degree);
320 let expanded = expand_local(curve);
321 let span = expanded
323 .iter()
324 .rposition(|&k| k == parameter)
325 .ok_or_else(|| GeomError::InvalidInput("knot vanished during expansion".to_owned()))?;
326 let multiplicity = curve.multiplicities[index] as usize;
327
328 let ord = degree + 1;
332 let first = span - degree;
333 let last = span - multiplicity;
334 let points: Vec<[Scalar; N]> = curve.control_points.iter().map(&coordinates).collect();
335
336 let mut temp: Vec<[Scalar; N]> = vec![[0.0; N]; last + 2 - first];
337 temp[0] = points[first - 1];
338 temp[last + 1 - first] = points[last + 1];
339
340 let (mut i, mut j) = (first, last);
341 let (mut ii, mut jj) = (1_usize, last - first);
342 while j > i {
343 let alfi = (parameter - expanded[i]) / (expanded[i + ord] - expanded[i]);
344 let alfj = (parameter - expanded[j]) / (expanded[j + ord] - expanded[j]);
345 for axis in 0..N {
346 temp[ii][axis] = (points[i][axis] - (1.0 - alfi) * temp[ii - 1][axis]) / alfi;
347 temp[jj][axis] = (points[j][axis] - alfj * temp[jj + 1][axis]) / (1.0 - alfj);
348 }
349 i += 1;
350 ii += 1;
351 j -= 1;
352 jj -= 1;
353 }
354
355 let mut result = points.clone();
359 let (mut i, mut j) = (first, last);
360 while j > i {
361 result[i] = temp[i - first + 1];
362 result[j] = temp[j - first + 1];
363 i += 1;
364 j -= 1;
365 }
366 if j == i {
367 result[i] = temp[i - first + 1];
368 }
369 result.remove(last);
370 let points = result;
371
372 let mut knots = curve.knots.clone();
373 let mut multiplicities = curve.multiplicities.clone();
374 multiplicities[index] -= 1;
375 if multiplicities[index] == 0 {
376 knots.remove(index);
377 multiplicities.remove(index);
378 }
379
380 Ok(BSplineCurve {
381 degree: curve.degree,
382 control_points: points.into_iter().map(&point).collect(),
383 knots,
384 multiplicities,
385 weights: None,
386 knot_spec: curve.knot_spec,
387 closed: curve.closed,
388 self_intersect: curve.self_intersect,
389 })
390}
391
392fn expand_local<P>(curve: &BSplineCurve<P>) -> Vec<Scalar> {
393 let mut expanded = Vec::new();
394 for (&knot, &multiplicity) in curve.knots.iter().zip(&curve.multiplicities) {
395 expanded.extend(core::iter::repeat_n(knot, multiplicity as usize));
396 }
397 expanded
398}
399
400pub fn reduce_degree2(
410 curve: &BSplineCurve2,
411 tolerance: Scalar,
412) -> GeomResult<BoundedResult<BSplineCurve2>> {
413 bspline_jet2(curve, 0.0)?;
414 let segments = crate::transform::bezier_segments2(curve)?;
415 let candidate = reduce(
416 curve,
417 &segments,
418 |p: &Point2| [p.x, p.y],
419 |c: [Scalar; 2]| Point2::new(c[0], c[1]),
420 )?;
421 let deviation = deviation2(curve, &candidate)?;
422 accept(candidate, deviation, tolerance)
423}
424
425pub fn reduce_degree3(
427 curve: &BSplineCurve3,
428 tolerance: Scalar,
429) -> GeomResult<BoundedResult<BSplineCurve3>> {
430 bspline_jet3(curve, 0.0)?;
431 let segments = crate::transform::bezier_segments3(curve)?;
432 let candidate = reduce(
433 curve,
434 &segments,
435 |p: &Point3| [p.x, p.y, p.z],
436 |c: [Scalar; 3]| Point3::new(c[0], c[1], c[2]),
437 )?;
438 let deviation = deviation3(curve, &candidate)?;
439 accept(candidate, deviation, tolerance)
440}
441
442fn reduce_bezier<const N: usize>(points: &[[Scalar; N]]) -> Vec<[Scalar; N]> {
448 let p = points.len() - 1;
449 let reduced = p;
450 let mut forward: Vec<[Scalar; N]> = vec![[0.0; N]; reduced];
451 let mut backward: Vec<[Scalar; N]> = vec![[0.0; N]; reduced];
452
453 forward[0] = points[0];
454 for i in 1..reduced {
455 let alpha = i as Scalar / p as Scalar;
456 for axis in 0..N {
457 forward[i][axis] = (points[i][axis] - alpha * forward[i - 1][axis]) / (1.0 - alpha);
458 }
459 }
460
461 backward[reduced - 1] = points[p];
462 for i in (0..reduced - 1).rev() {
463 let alpha = (i + 1) as Scalar / p as Scalar;
464 for axis in 0..N {
465 backward[i][axis] =
466 (points[i + 1][axis] - (1.0 - alpha) * backward[i + 1][axis]) / alpha;
467 }
468 }
469
470 (0..reduced)
471 .map(|i| {
472 let mut blended = [0.0; N];
473 for (axis, value) in blended.iter_mut().enumerate() {
474 *value = 0.5 * (forward[i][axis] + backward[i][axis]);
475 }
476 blended
477 })
478 .collect()
479}
480
481fn reduce<const N: usize, P: Clone>(
483 curve: &BSplineCurve<P>,
484 segments: &[BSplineCurve<P>],
485 coordinates: impl Fn(&P) -> [Scalar; N],
486 point: impl Fn([Scalar; N]) -> P,
487) -> GeomResult<BSplineCurve<P>> {
488 if curve.degree < 2 {
489 return Err(GeomError::InvalidInput(
490 "degree 1 cannot be reduced further and stay a curve".to_owned(),
491 ));
492 }
493 if curve.weights.is_some() {
494 return Err(GeomError::Unsupported {
495 backend: axiolid_contracts::BackendId::new("nurbs"),
496 operation: axiolid_contracts::Operation::CurveEvaluation,
497 });
498 }
499 let reduced_degree = curve.degree - 1;
500
501 let mut control_points: Vec<P> = Vec::new();
502 for (index, segment) in segments.iter().enumerate() {
503 let points: Vec<[Scalar; N]> = segment.control_points.iter().map(&coordinates).collect();
504 let lowered = reduce_bezier(&points);
505 let skip = usize::from(index > 0);
506 for coordinate in lowered.into_iter().skip(skip) {
507 control_points.push(point(coordinate));
508 }
509 }
510
511 let clamped = u32::from(reduced_degree) + 1;
512 let internal = u32::from(reduced_degree);
513 let mut knots: Vec<Scalar> = Vec::with_capacity(segments.len() + 1);
514 let mut multiplicities: Vec<u32> = Vec::with_capacity(segments.len() + 1);
515 for (index, segment) in segments.iter().enumerate() {
516 let first = *segment
517 .knots
518 .first()
519 .ok_or_else(|| GeomError::InvalidInput("bezier segment has no knots".to_owned()))?;
520 if index == 0 {
521 knots.push(first);
522 multiplicities.push(clamped);
523 }
524 let last = *segment
525 .knots
526 .last()
527 .ok_or_else(|| GeomError::InvalidInput("bezier segment has no knots".to_owned()))?;
528 knots.push(last);
529 multiplicities.push(if index + 1 == segments.len() {
530 clamped
531 } else {
532 internal
533 });
534 }
535
536 Ok(BSplineCurve {
537 degree: reduced_degree,
538 control_points,
539 knots,
540 multiplicities,
541 weights: None,
542 knot_spec: curve.knot_spec,
543 closed: curve.closed,
544 self_intersect: curve.self_intersect,
545 })
546}