1use geop_core_math::{
7 geop_error::{GeopError, GeopResult},
8 scalars::Scalar,
9 vector::Vector,
10};
11
12use super::NurbCurve;
13use crate::{
14 aabb::compute_aabb,
15 knot_insertion::{insert, is_clamped_at},
16 spline::interior_knots,
17};
18
19impl<const D: usize> NurbCurve<D> {
20 pub fn with_unit_domain(&self) -> GeopResult<Self> {
25 let (t0, t1) = self.domain();
26 let exactly = |t: Scalar, x: Scalar| t.is_subset_of(x) && x.is_subset_of(t);
27 if exactly(t0, Scalar::ZERO) && exactly(t1, Scalar::ONE) {
28 return Ok(self.clone());
29 }
30 let span = t1.sub(t0);
31 let knots = self
32 .knot_vector
33 .iter()
34 .map(|&k| k.sub(t0).div(span))
35 .collect::<GeopResult<Vec<Scalar>>>()?;
36 NurbCurve::try_new(self.degree, self.control_points.clone(), knots)
37 }
38
39 pub fn elevate_degree(&self) -> GeopResult<Self> {
47 let p = self.degree;
48 let n = self.control_points.len();
49 if !(is_clamped_at(&self.knot_vector, p, true)
50 && is_clamped_at(&self.knot_vector, p, false))
51 {
52 return Err(GeopError::new(format!(
53 "elevate_degree: the knot vector {:?} is not clamped at both ends",
54 self.knot_vector
55 )));
56 }
57 let interior = interior_knots(&self.knot_vector, p, n);
58 let mut knots = self.knot_vector.clone();
59 let mut rows = [self.control_points.clone()];
60 for &(u, m) in &interior {
61 if m > p {
62 return Err(GeopError::new(format!(
63 "elevate_degree: the knot {u:?} has multiplicity {m}, more than the degree {p}"
64 )));
65 }
66 for _ in m..p {
67 insert(&mut knots, &mut rows, p, u);
68 }
69 }
70 let [pts] = rows;
71 let pieces = interior.len() + 1;
72 debug_assert_eq!(pts.len(), p * pieces + 1);
73 let mut out: Vec<Vector<D>> = Vec::with_capacity((p + 1) * pieces + 1);
74 out.push(pts[0]);
75 for piece in pts.windows(p + 1).step_by(p.max(1)) {
76 for i in 1..=p {
77 let alpha = Scalar::from_ratio(i as i64, (p + 1) as i64)?;
78 out.push(Vector::interpolate(&piece[i], &piece[i - 1], alpha));
79 }
80 out.push(piece[p]);
81 }
82 let (start, end) = self.domain();
83 let mut new_knots = vec![start; p + 2];
84 for &(u, _) in &interior {
85 new_knots.extend(std::iter::repeat_n(u, p + 1));
86 }
87 new_knots.extend(std::iter::repeat_n(end, p + 2));
88 NurbCurve::try_new(p + 1, out, new_knots)
89 }
90
91 pub fn with_unit_end_weights(&self) -> GeopResult<Self> {
103 let p = self.degree;
104 let n = self.control_points.len();
105 let weight = |i: usize| self.control_points[i][D - 1];
106 if weight(0).is_exactly(Scalar::ONE) && weight(n - 1).is_exactly(Scalar::ONE) {
107 return Ok(self.clone());
108 }
109 if n != p + 1 {
110 return Err(GeopError::new(format!(
111 "with_unit_end_weights: a curve of {n} control points and degree {p} is not a single span, and its end weights {:?}, {:?} are not one",
112 weight(0),
113 weight(n - 1)
114 )));
115 }
116 let c = (weight(0).to_f64() / weight(n - 1).to_f64()).powf(1.0 / p as f64);
117 let control_points = (0..n)
118 .map(|i| {
119 let factor = if i == 0 {
120 Scalar::ONE.div(weight(0))?
121 } else if i == n - 1 {
122 Scalar::ONE.div(weight(n - 1))?
123 } else {
124 Scalar::from_f64(c.powi(i as i32)).div(weight(0))?
125 };
126 Ok(self.control_points[i].prod_scalar(factor))
127 })
128 .collect::<GeopResult<Vec<_>>>()?;
129 NurbCurve::try_new(p, control_points, self.knot_vector.clone())
130 }
131
132 pub fn compatible(curves: &[Self]) -> GeopResult<Vec<Self>> {
141 let Some(first) = curves.first() else {
142 return Ok(Vec::new());
143 };
144 let (start, end) = first.domain();
145 let degree = curves.iter().map(|c| c.degree).max().unwrap_or(0);
146 let mut out = Vec::with_capacity(curves.len());
147 for curve in curves {
148 let (s, e) = curve.domain();
149 if !(s.could_be_equal(start) && e.could_be_equal(end)) {
150 return Err(GeopError::new(format!(
151 "compatible: domains ({s:?}, {e:?}) and ({start:?}, {end:?}) differ"
152 )));
153 }
154 let mut curve = curve.clone();
155 for _ in curve.degree..degree {
157 curve = curve.elevate_degree()?;
158 }
159 out.push(curve);
160 }
161
162 let mut merged: Vec<(Scalar, usize)> = Vec::new();
163 for curve in &out {
164 for (u, m) in interior_knots(&curve.knot_vector, degree, curve.control_points.len()) {
165 match merged.iter_mut().find(|(v, _)| v.could_be_equal(u)) {
166 Some((v, k)) => {
167 *v = v.union(u);
168 *k = (*k).max(m);
169 }
170 None => merged.push((u, m)),
171 }
172 }
173 }
174 for curve in &mut out {
175 let mut knots = curve.knot_vector.clone();
176 let mut rows = [curve.control_points.clone()];
177 for &(u, m) in &merged {
178 let have = interior_knots(&knots, degree, rows[0].len())
179 .iter()
180 .find(|(v, _)| v.could_be_equal(u))
181 .map_or(0, |&(_, k)| k);
182 for _ in have..m {
183 insert(&mut knots, &mut rows, degree, u);
184 }
185 }
186 let [pts] = rows;
187 curve.control_points = pts;
188 curve.knot_vector = knots;
189 }
190
191 let len = out[0].knot_vector.len();
193 if out.iter().any(|c| c.knot_vector.len() != len) {
194 return Err(GeopError::new(format!(
195 "compatible: knot vectors of different lengths after merging: {:?}",
196 out.iter()
197 .map(|c| c.knot_vector.clone())
198 .collect::<Vec<_>>()
199 )));
200 }
201 let common: Vec<Scalar> = (0..len)
202 .map(|k| {
203 out.iter()
204 .map(|c| c.knot_vector[k])
205 .reduce(|a, b| a.union(b))
206 .expect("at least one curve")
207 })
208 .collect();
209 for curve in &mut out {
210 curve.knot_vector = common.clone();
211 curve.aabb = compute_aabb(&curve.control_points);
212 }
213 Ok(out)
214 }
215
216 pub fn join(curves: &[Self]) -> GeopResult<Self> {
220 let n = curves.len();
221 if n == 1 {
222 return curves[0].with_unit_domain()?.with_unit_end_weights();
223 }
224 let count = Scalar::from_f64(n as f64);
225 join_placed(
226 curves,
227 (Scalar::ZERO, Scalar::ONE),
228 |i, k| k.add(Scalar::from_f64(i as f64)).div(count),
229 |i| Scalar::from_ratio((i + 1) as i64, n as i64),
230 )
231 }
232
233 pub fn join_at(curves: &[Self], breaks: &[Scalar]) -> GeopResult<Self> {
243 let n = curves.len();
244 if breaks.len() != n + 1 || !breaks.windows(2).all(|w| w[0].definitely_less(w[1])) {
245 return Err(GeopError::new(format!(
246 "join_at: {n} curves need {} strictly increasing breaks, not {breaks:?}",
247 n + 1
248 )));
249 }
250 join_placed(
251 curves,
252 (breaks[0], breaks[n]),
253 |i, k| Ok(breaks[i].add(k.mul(breaks[i + 1].sub(breaks[i])))),
254 |i| Ok(breaks[i + 1]),
255 )
256 }
257
258 pub fn reparametrized(&self, from: &[Scalar], to: &[Scalar]) -> GeopResult<Self> {
275 let n = from.len();
276 let ctx = |e: GeopError| {
277 e.with_context(format!(
278 "NurbCurve::reparametrized(from={from:?}, to={to:?}): domain={:?}",
279 self.domain()
280 ))
281 };
282 if n < 2 || to.len() != n {
283 return Err(ctx(GeopError::new(format!(
284 "{n} parameters to map onto {} others",
285 to.len()
286 ))));
287 }
288 let (t0, t1) = self.domain();
289 let mut from = from.to_vec();
290 if from[0].could_be_equal(t0) {
291 from[0] = t0;
292 }
293 if from[n - 1].could_be_equal(t1) {
294 from[n - 1] = t1;
295 }
296 let increasing = |x: &[Scalar]| x.windows(2).all(|w| w[0].definitely_less(w[1]));
297 if !increasing(&from) || !increasing(to) {
298 return Err(ctx(GeopError::new(
299 "the parameters must be strictly increasing",
300 )));
301 }
302 let mut rest = self.sub_curve(from[0], from[n - 1]).map_err(ctx)?;
303 let mut pieces = Vec::with_capacity(n - 1);
304 for &t in &from[1..n - 1] {
305 let (left, right) = rest.split(t).map_err(ctx)?;
306 pieces.push(left);
307 rest = right;
308 }
309 pieces.push(rest);
310 let rational = self
311 .control_points
312 .iter()
313 .any(|p| !p[D - 1].is_exactly(Scalar::ONE));
314 let mut curves = Vec::new();
315 let mut breaks = vec![to[0]];
316 for (i, mut piece) in pieces.into_iter().enumerate() {
317 let parts = if rational {
318 piece.bezier_pieces().map_err(ctx)?
319 } else {
320 for cp in &mut piece.control_points {
323 cp[D - 1] = Scalar::ONE;
324 }
325 vec![piece]
326 };
327 let scale = to[i + 1]
328 .sub(to[i])
329 .div(from[i + 1].sub(from[i]))
330 .map_err(ctx)?;
331 let last = parts.len() - 1;
332 for (k, part) in parts.into_iter().enumerate() {
333 breaks.push(if k == last {
334 to[i + 1]
335 } else {
336 to[i].add(part.domain().1.sub(from[i]).mul(scale)).sharpen()
337 });
338 curves.push(part);
339 }
340 }
341 NurbCurve::join_at(&curves, &breaks).map_err(ctx)
342 }
343}
344
345fn join_placed<const D: usize>(
349 curves: &[NurbCurve<D>],
350 (start, end): (Scalar, Scalar),
351 place: impl Fn(usize, Scalar) -> GeopResult<Scalar>,
352 joint: impl Fn(usize) -> GeopResult<Scalar>,
353) -> GeopResult<NurbCurve<D>> {
354 let n = curves.len();
355 if n == 0 {
356 return Err(GeopError::new("join: no curves to join"));
357 }
358 let mut pieces = curves
359 .iter()
360 .map(|c| c.with_unit_domain()?.with_unit_end_weights())
361 .collect::<GeopResult<Vec<_>>>()?;
362 let degree = pieces.iter().map(|c| c.degree).max().unwrap_or(0);
363 for piece in &mut pieces {
364 for _ in piece.degree..degree {
366 *piece = piece.elevate_degree()?;
367 }
368 }
369 let mut points: Vec<Vector<D>> = Vec::new();
370 let mut knots = vec![start; degree + 1];
371 for (i, piece) in pieces.iter().enumerate() {
372 let len = piece.control_points.len();
373 if let Some(end) = points.last_mut() {
374 let start = &piece.control_points[0];
375 if !end.could_be_equal(start) {
376 return Err(GeopError::new(format!(
377 "join: curve {i} starts at {start:?}, not where the one before ends, {end:?}"
378 )));
379 }
380 *end = end.union(start);
381 points.extend_from_slice(&piece.control_points[1..]);
382 } else {
383 points.extend_from_slice(&piece.control_points);
384 }
385 for &k in &piece.knot_vector[degree + 1..len] {
386 knots.push(place(i, k)?);
387 }
388 if i + 1 < n {
389 knots.extend(std::iter::repeat_n(joint(i)?, degree));
390 }
391 }
392 knots.extend(std::iter::repeat_n(end, degree + 1));
393 NurbCurve::try_new(degree, points, knots)
394}
395
396#[cfg(test)]
397mod tests {
398 use crate::nurb_curve::{NurbCurve, NurbCurve3D};
399 use crate::test_fixtures::{line, weighted};
400 use geop_core_math::{scalars::Scalar, vector::Vector3};
401
402 fn quarter_arc() -> NurbCurve3D {
403 let f = Scalar::from_f64;
404 NurbCurve::try_new(
405 2,
406 vec![
407 weighted(1., 0., 0., 1.),
408 weighted(1., 1., 0., std::f64::consts::SQRT_2 / 2.0),
409 weighted(0., 1., 0., 1.),
410 ],
411 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
412 )
413 .unwrap()
414 }
415
416 fn cubic_spline() -> NurbCurve3D {
417 let f = Scalar::from_f64;
418 NurbCurve::try_new(
419 3,
420 vec![
421 weighted(0., 0., 0., 1.),
422 weighted(1., 2., 0., 1.),
423 weighted(2., -1., 1., 1.),
424 weighted(3., 1., 0., 1.),
425 weighted(4., 0., 2., 1.),
426 ],
427 vec![
428 f(0.),
429 f(0.),
430 f(0.),
431 f(0.),
432 f(0.5),
433 f(1.),
434 f(1.),
435 f(1.),
436 f(1.),
437 ],
438 )
439 .unwrap()
440 }
441
442 fn assert_same_curve(a: &NurbCurve3D, b: &NurbCurve3D) {
443 for i in 0..=16 {
444 let t = Scalar::from_ratio(i, 16).unwrap();
445 let (pa, pb) = (a.evaluate(t).unwrap(), b.evaluate(t).unwrap());
446 assert!(pa.could_be_equal(&pb), "at {t:?}: {pa:?} vs {pb:?}");
447 }
448 }
449
450 #[test]
453 fn elevate_degree_keeps_the_curve() {
454 for curve in [quarter_arc(), cubic_spline(), line([0.; 3], [2., 1., 3.])] {
455 let raised = curve.elevate_degree().unwrap();
456 assert_eq!(raised.degree, curve.degree + 1);
457 assert_same_curve(&curve, &raised);
458 let twice = raised.elevate_degree().unwrap();
459 assert_same_curve(&curve, &twice);
460 }
461 }
462
463 #[test]
466 fn compatible_curves_share_degree_and_knots() {
467 let curves = [line([0.; 3], [2., 1., 3.]), quarter_arc(), cubic_spline()];
468 let out = NurbCurve::compatible(&curves).unwrap();
469 for (before, after) in curves.iter().zip(&out) {
470 assert_eq!(after.degree, 3);
471 assert_eq!(after.control_points.len(), out[0].control_points.len());
472 for (a, b) in after.knot_vector.iter().zip(&out[0].knot_vector) {
473 assert!(a.is_subset_of(*b) && b.is_subset_of(*a));
474 }
475 assert_same_curve(before, after);
476 }
477 }
478
479 #[test]
481 fn with_unit_domain_rescales_the_knots() {
482 let curve = cubic_spline();
483 let (_, right) = curve.split(Scalar::from_f64(0.25)).unwrap();
484 let unit = right.with_unit_domain().unwrap();
485 let (t0, t1) = unit.domain();
486 assert!(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE));
487 let mid = unit.evaluate(Scalar::from_f64(0.5)).unwrap();
488 assert!(mid.could_be_equal(&curve.evaluate(Scalar::from_f64(0.625)).unwrap()));
489 }
490
491 #[test]
494 fn unit_end_weights_keep_the_curve() {
495 let (left, _) = quarter_arc().split(Scalar::from_f64(0.5)).unwrap();
496 let left = left.with_unit_domain().unwrap();
497 let unit = left.with_unit_end_weights().unwrap();
498 for cp in [unit.control_points[0], unit.control_points[2]] {
499 assert!(cp[3].could_be_equal(Scalar::ONE));
500 }
501 for i in 0..=8 {
504 let p = unit.evaluate(Scalar::from_ratio(i, 8).unwrap()).unwrap();
505 assert!(p.norm_sq().could_be_equal(Scalar::ONE), "{p:?}");
506 }
507 assert!(
508 unit.evaluate(Scalar::ONE)
509 .unwrap()
510 .could_be_equal(&left.evaluate(Scalar::ONE).unwrap())
511 );
512 }
513
514 #[test]
518 fn join_lays_curves_end_to_end() {
519 let f = Scalar::from_f64;
520 let on = NurbCurve::try_new(
521 1,
522 vec![weighted(0., 1., 0., 1.), weighted(-1., 1., 0., 1.)],
523 vec![f(0.), f(0.), f(1.), f(1.)],
524 )
525 .unwrap();
526 let arc = quarter_arc();
527 let joined = NurbCurve::join(&[arc.clone(), on.clone()]).unwrap();
528 assert_eq!(joined.degree, 2);
529 for i in 0..=8 {
530 let t = Scalar::from_ratio(i, 8).unwrap();
531 let half = |k: f64| t.add(f(k)).div(Scalar::TWO).unwrap();
532 let (a, b) = (
533 joined.evaluate(half(0.0)).unwrap(),
534 arc.evaluate(t).unwrap(),
535 );
536 assert!(a.could_be_equal(&b), "{a:?} vs {b:?}");
537 let (a, b) = (joined.evaluate(half(1.0)).unwrap(), on.evaluate(t).unwrap());
538 assert!(a.could_be_equal(&b), "{a:?} vs {b:?}");
539 }
540 assert!(NurbCurve::join(&[arc, line([0.; 3], [2., 1., 3.])]).is_err());
541 }
542
543 #[test]
547 fn reparametrized_maps_pieces() {
548 let f = Scalar::from_f64;
549 let from = [f(0.1), f(0.3), f(1.0)];
550 let to = [f(0.0), f(0.6), f(1.0)];
551 for curve in [quarter_arc(), cubic_spline()] {
552 let mapped = curve.reparametrized(&from, &to).unwrap();
553 let (t0, t1) = mapped.domain();
554 assert!(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE));
555 for (a, b) in from.iter().zip(&to) {
556 let (p, q) = (curve.evaluate(*a).unwrap(), mapped.evaluate(*b).unwrap());
557 assert!(p.could_be_equal(&q), "{p:?} vs {q:?}");
558 }
559 let joint = mapped
560 .control_points
561 .iter()
562 .find(|cp| {
563 Vector3::from_array([cp[0], cp[1], cp[2]])
564 .prod_scalar(Scalar::ONE.div(cp[3]).unwrap())
565 .could_be_equal(&curve.evaluate(from[1]).unwrap())
566 })
567 .expect("a control point at the joint");
568 assert!(joint[3].could_be_equal(Scalar::ONE), "{joint:?}");
569 if curve.degree == 2 {
570 for i in 0..=8 {
571 let p = mapped.evaluate(Scalar::from_ratio(i, 8).unwrap()).unwrap();
572 assert!(p.norm_sq().could_be_equal(Scalar::ONE), "{p:?}");
573 }
574 }
575 }
576 }
577}