1use geop_core_math::{
63 geop_error::{GeopError, GeopResult},
64 scalars::Scalar,
65 vector::{Vector, Vector3},
66};
67
68use super::{
69 NurbSurface3D,
70 net::{Net, Side, cross, dot, normal_field, poles, product, scale},
71 offset::Closing,
72};
73use crate::{nurb_curve::solve_interpolation_system, spline::breakpoints};
74
75const OFFSET_DRIFT: f64 = 1e-6;
81
82const MAX_OFFSET_PIECES: usize = 32;
85
86const MAX_OFFSET_BOXES: usize = 1 << 14;
89
90const REGULAR_SPLITS: usize = 6;
96
97const STALLED_SPLITS: usize = 3;
102
103const INVERSE_DEGREE: usize = 3;
106
107const BOUND_SPLITS: usize = 4;
112
113struct Fit {
118 w: Net<1>,
119 m: Net<3>,
120 e: Net<3>,
121}
122
123struct Shape {
129 w: Net<1>,
130 m: Net<3>,
131 first: [Net<1>; 3],
132 second: [Net<1>; 3],
133 twist: Option<Net<1>>,
134}
135
136fn divide<const D: usize>(net: &Net<D>, side: Side, times: usize) -> GeopResult<Net<D>> {
139 let mut out = net.clone();
140 for _ in 0..times {
141 out = out.divided(side)?.ok_or_else(|| {
142 GeopError::new(format!(
143 "a polynomial of the surface does not vanish along its pole at {side:?}"
144 ))
145 })?;
146 }
147 Ok(out)
148}
149
150fn parts(patch: &NurbSurface3D) -> GeopResult<(Net<3>, Net<1>, Net<3>, Vec<Side>)> {
154 let (a, w) = Net::of(patch, true)?.split();
155 let sides = poles(patch)?;
156 let mut m = normal_field(patch)?;
157 for &side in &sides {
158 m = divide(&m, side, 1)?;
159 }
160 Ok((a, w, m, sides))
161}
162
163fn inverse_length(m: &Net<3>) -> GeopResult<Option<(Net<1>, Scalar)>> {
167 let squared = m.product(m, dot)?;
168 let Some(g) = inverse_root(&squared)? else {
169 return Ok(None);
170 };
171 let r = g.product(&g, product)?.product(&squared, product)?.hull()[0];
172 Ok(r.definitely_greater(Scalar::ZERO).then_some((g, r)))
173}
174
175fn inverse_root(squared: &Net<1>) -> GeopResult<Option<Net<1>>> {
180 let n = INVERSE_DEGREE;
181 let grid = |(lo, hi): (Scalar, Scalar)| -> GeopResult<Vec<Scalar>> {
182 (0..=n)
183 .map(|k| match k {
184 0 => Ok(lo),
187 k if k == n => Ok(hi),
188 k => Ok(Scalar::fraction(
189 lo,
190 hi,
191 Scalar::from_ratio(k as i64, n as i64)?,
192 )),
193 })
194 .collect()
195 };
196 let knots = |(lo, hi): (Scalar, Scalar)| {
197 let mut k = vec![lo; n + 1];
198 k.extend(vec![hi; n + 1]);
199 k
200 };
201 let (gu, gv) = (grid(squared.u)?, grid(squared.v)?);
202 let (ku, kv) = (knots(squared.u), knots(squared.v));
203 let mut rows = Vec::with_capacity(n + 1);
204 for &u in &gu {
205 let mut values = Vec::with_capacity(n + 1);
206 for &v in &gv {
207 let q = squared.value(u, v)?[0];
208 if !q.definitely_greater(Scalar::ZERO) {
209 return Ok(None);
210 }
211 values.push(Vector::from_array([Scalar::ONE.div(q.sqrt()?)?]));
212 }
213 rows.push(solve_interpolation_system(&gv, &kv, n, &values)?);
214 }
215 let mut c = vec![vec![Vector::zero(); n + 1]; n + 1];
216 for j in 0..=n {
217 let column: Vec<Vector<1>> = rows.iter().map(|row| row[j]).collect();
218 for (i, x) in solve_interpolation_system(&gu, &ku, n, &column)?
219 .into_iter()
220 .enumerate()
221 {
222 c[i][j] = Vector::from_array([x[0].sharpen()]);
223 }
224 }
225 Ok(Some(Net {
226 c,
227 u: squared.u,
228 v: squared.v,
229 }))
230}
231
232fn side_factor(side: Side, like: &Net<1>) -> Net<1> {
235 let one = Vector::from_array([Scalar::ONE]);
236 let zero = Vector::from_array([Scalar::ZERO]);
237 let across = if side.at_end {
238 vec![one, zero]
239 } else {
240 vec![zero, one]
241 };
242 let c = if side.u_fixed {
243 across.into_iter().map(|x| vec![x]).collect()
244 } else {
245 vec![across]
246 };
247 Net {
248 c,
249 u: like.u,
250 v: like.v,
251 }
252}
253
254impl Fit {
255 fn new(surface: &NurbSurface3D, shift: &NurbSurface3D) -> GeopResult<Self> {
256 let (_, w, m, _) = parts(surface)?;
257 let (e, _) = Net::of(shift, false)?.split();
258 Ok(Fit { w, m, e })
259 }
260
261 fn missed(
265 &self,
266 distance: Scalar,
267 u: (Scalar, Scalar),
268 v: (Scalar, Scalar),
269 ) -> GeopResult<Option<Scalar>> {
270 let w = self.w.restricted(u, v)?;
271 let m = self.m.restricted(u, v)?;
272 let e = self.e.restricted(u, v)?;
273 let along = e.product(&m, dot)?.hull()[0].mul(distance);
274 if !along.definitely_greater(Scalar::ZERO) {
275 return Ok(None);
276 }
277 let w_squared = w.product(&w, product)?;
278 let across = e.product(&m, cross)?;
279 let Some(b_squared) = ratio_bound(
280 &across.product(&across, dot)?,
281 &w_squared.product(&m.product(&m, dot)?, product)?,
282 )?
283 else {
284 return Ok(None);
285 };
286 let Some(length) = ratio_bound(
287 &e.product(&e, dot)?
288 .minus(&w_squared.scaled(distance.mul(distance)))?,
289 &w_squared,
290 )?
291 else {
292 return Ok(None);
293 };
294 let b = b_squared.sqrt()?;
295 let along = length.add(b_squared).div(distance.abs())?;
296 Ok(Some(b.add(along).upper()))
297 }
298}
299
300fn ratio_bound(num: &Net<1>, den: &Net<1>) -> GeopResult<Option<Scalar>> {
308 let (p, q) = (
309 num.c.len().max(den.c.len()) - 1,
310 num.c[0].len().max(den.c[0].len()) - 1,
311 );
312 let (num, den) = (num.raised(p, q)?, den.raised(p, q)?);
313 let mut bound = Scalar::ZERO;
314 for (a, d) in num.c.iter().flatten().zip(den.c.iter().flatten()) {
315 if !d[0].definitely_greater(Scalar::ZERO) {
316 return Ok(None);
317 }
318 bound = bound.max(a[0].abs().upper().div(d[0])?.upper());
319 }
320 Ok(Some(bound))
321}
322
323impl Shape {
324 fn new(patch: &NurbSurface3D) -> GeopResult<Self> {
325 let (a, w, m, sides) = parts(patch)?;
326 let along = |t: bool| -> GeopResult<Net<3>> {
328 w.product(&a.derivative(t)?, scale)?
329 .minus(&w.derivative(t)?.product(&a, scale)?)
330 };
331 let (p_u, p_v) = (along(true)?, along(false)?);
332 let across = |s: bool, t: bool| -> GeopResult<Net<1>> {
333 let a_st = a.derivative(s)?.derivative(t)?;
334 let w_st = w.derivative(s)?.derivative(t)?;
335 w.product(&a_st, scale)?
336 .minus(&w_st.product(&a, scale)?)?
337 .product(&m, dot)
338 };
339 let mut first = [
340 p_u.product(&p_u, dot)?,
341 p_u.product(&p_v, dot)?,
342 p_v.product(&p_v, dot)?,
343 ];
344 let mut second = [
345 across(true, true)?,
346 across(true, false)?,
347 across(false, false)?,
348 ];
349 let mut twist: Option<Net<1>> = None;
353 for &side in &sides {
354 let t = if side.u_fixed { 2 } else { 0 };
355 let once = divide(&second[t], side, 1)?;
356 let (twice, times) = match once.divided(side)? {
357 Some(twice) => (twice, 2),
358 None => (once, 1),
359 };
360 second[t] = twice;
361 first[t] = divide(&first[t], side, times)?;
362 second[1] = second[1].divided(side)?.ok_or_else(|| {
363 GeopError::new(format!(
364 "the surface twists at its apex at {side:?}: its offset is not regular there"
365 ))
366 })?;
367 first[1] = divide(&first[1], side, 1)?;
368 if times == 1 {
369 let factor = side_factor(side, &first[1]);
371 twist = Some(match twist {
372 Some(f) => f.product(&factor, product)?,
373 None => factor,
374 });
375 }
376 }
377 Ok(Shape {
378 w,
379 m,
380 first,
381 second,
382 twist,
383 })
384 }
385
386 fn regular(
394 &self,
395 distance: Scalar,
396 u: (Scalar, Scalar),
397 v: (Scalar, Scalar),
398 closes: Option<Side>,
399 flipped: bool,
400 ) -> GeopResult<(Option<bool>, f64)> {
401 let w = self.w.restricted(u, v)?;
402 let m = self.m.restricted(u, v)?;
403 let Some((fitted, r)) = inverse_length(&m)? else {
406 return Ok((None, f64::INFINITY));
407 };
408 let factor = distance.div(r.sqrt()?)?;
409 let c = w.product(&w, product)?.product(&fitted, product)?;
410 let g = (0..3)
411 .map(|i| {
412 self.first[i].restricted(u, v)?.minus(
413 &c.product(&self.second[i].restricted(u, v)?, product)?
414 .scaled(factor),
415 )
416 })
417 .collect::<GeopResult<Vec<_>>>()?;
418 let twist = self
419 .twist
420 .as_ref()
421 .map(|t| t.restricted(u, v))
422 .transpose()?;
423 let (t, s) = match closes {
424 Some(side) if side.u_fixed => (2, 0),
425 _ => (0, 2),
426 };
427 let verdict = |diagonal: Scalar, det: Scalar| {
430 let det = if flipped { det.neg() } else { det };
431 let size = diagonal
432 .abs()
433 .upper()
434 .to_f64()
435 .max(det.abs().upper().to_f64());
436 let said = if diagonal.definitely_greater(Scalar::ZERO)
437 && det.definitely_greater(Scalar::ZERO)
438 {
439 Some(true)
440 } else if diagonal.definitely_less(Scalar::ZERO) || det.definitely_less(Scalar::ZERO) {
441 Some(false)
442 } else {
443 None
444 };
445 (said, size)
446 };
447 let squared = g[1].product(&g[1], product)?;
448 let squared = match &twist {
449 Some(t) => squared.product(t, product)?,
450 None => squared,
451 };
452 let det = g[0].product(&g[2], product)?.minus(&squared)?.hull()[0];
453 let said = verdict(g[s].hull()[0], det);
454 let Some(side) = closes.filter(|_| said.0.is_none()) else {
455 return Ok(said);
456 };
457 let across = side.u_fixed;
465 let (eu, ev) = match (side.u_fixed, side.at_end) {
466 (true, false) => ((self.w.u.0, u.1), v),
467 (true, true) => ((u.0, self.w.u.1), v),
468 (false, false) => (u, (self.w.v.0, v.1)),
469 (false, true) => (u, (v.0, self.w.v.1)),
470 };
471 let w = self.w.restricted(eu, ev)?;
472 let m = self.m.restricted(eu, ev)?;
473 let Some((fitted, _)) = inverse_length(&m)? else {
476 return Ok((None, f64::INFINITY));
477 };
478 let r = fitted
479 .product(&fitted, product)?
480 .product(&m.product(&m, dot)?, product)?;
481 let rho = Scalar::ONE.div(r.hull()[0].sqrt()?)?;
482 let drho = rho
483 .mul(rho)
484 .mul(rho)
485 .mul(r.derivative(across)?.hull()[0])
486 .div(Scalar::TWO)?
487 .neg();
488 let c = w.product(&w, product)?.product(&fitted, product)?;
489 let derivative = |i: usize| -> GeopResult<Scalar> {
490 let k = c.product(&self.second[i].restricted(eu, ev)?, product)?;
491 Ok(self.first[i]
492 .restricted(eu, ev)?
493 .derivative(across)?
494 .minus(&k.derivative(across)?.scaled(distance.mul(rho)))?
495 .minus(&k.scaled(distance.mul(drho)))?
496 .hull()[0])
497 };
498 let x = if side.at_end {
499 Scalar::ONE.neg()
500 } else {
501 Scalar::ONE
502 };
503 let ts = g[1].hull()[0];
504 let ts = match &twist {
505 Some(f) => ts.mul(f.hull()[0]),
506 None => ts,
507 };
508 let det = x.mul(
509 derivative(t)?
510 .mul(g[s].hull()[0])
511 .sub(derivative(1)?.mul(ts)),
512 );
513 Ok(verdict(g[s].hull()[0], det))
514 }
515}
516
517enum Said<T, W> {
520 Is(T),
521 Split(W),
522}
523
524fn over_boxes<T, S, W: Clone>(
530 surface: &NurbSurface3D,
531 what: &str,
532 splits: usize,
533 mut prepare: impl FnMut(((Scalar, Scalar), (Scalar, Scalar))) -> GeopResult<S>,
534 mut decide: impl FnMut(
535 &S,
536 (Scalar, Scalar),
537 (Scalar, Scalar),
538 bool,
539 Option<&W>,
540 ) -> GeopResult<Said<T, W>>,
541 mut take: impl FnMut(T, (Scalar, Scalar), (Scalar, Scalar)),
542) -> GeopResult<()> {
543 let bu = breakpoints(&surface.knot_vector_u, surface.degree_u, surface.num_u);
544 let bv = breakpoints(&surface.knot_vector_v, surface.degree_v, surface.num_v);
545 let half = Scalar::from_f64(0.5);
546 let mut boxes = 0usize;
547 for su in bu.windows(2) {
548 for sv in bv.windows(2) {
549 let span = ((su[0], su[1]), (sv[0], sv[1]));
550 let piece = prepare(span)
551 .map_err(|e| e.with_context(format!("the span u={:?}, v={:?}", span.0, span.1)))?;
552 let mut stack: Vec<(_, _, usize, Option<W>)> = vec![(span.0, span.1, 0usize, None)];
553 while let Some((u, v, depth, parent)) = stack.pop() {
554 boxes += 1;
555 if boxes > MAX_OFFSET_BOXES {
556 return Err(GeopError::new(format!(
557 "{what} was not decided within {MAX_OFFSET_BOXES} boxes; the last one u={u:?}, v={v:?}"
558 )));
559 }
560 let last = depth >= splits;
561 let found = match decide(&piece, u, v, last, parent.as_ref())
562 .map_err(|e| e.with_context(format!("{what} over u={u:?}, v={v:?}")))?
563 {
564 Said::Is(said) => {
565 take(said, u, v);
566 continue;
567 }
568 Said::Split(found) => found,
569 };
570 if last {
571 return Err(GeopError::new(format!(
572 "{what} could not be decided over u={u:?}, v={v:?}, split {splits} times"
573 )));
574 }
575 let (cu, cv) = (
576 Scalar::fraction(u.0, u.1, half),
577 Scalar::fraction(v.0, v.1, half),
578 );
579 for a in [(u.0, cu), (cu, u.1)] {
580 for b in [(v.0, cv), (cv, v.1)] {
581 stack.push((a, b, depth + 1, Some(found.clone())));
582 }
583 }
584 }
585 }
586 }
587 Ok(())
588}
589
590struct Offsetting<'a> {
594 surface: &'a NurbSurface3D,
595 distance: Scalar,
596 closing: Option<&'a Closing>,
597}
598
599impl Offsetting<'_> {
600 fn flipped(&self, u: Scalar, v: Scalar) -> bool {
604 let Some(Closing {
605 side,
606 apex: Some(apex),
607 }) = self.closing
608 else {
609 return false;
610 };
611 let x = if side.u_fixed { u } else { v };
612 if side.at_end {
613 x.definitely_greater(*apex)
614 } else {
615 x.definitely_less(*apex)
616 }
617 }
618
619 fn distance_over(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> GeopResult<Scalar> {
622 Ok(if self.flipped_over(u, v)? {
623 self.distance.neg()
624 } else {
625 self.distance
626 })
627 }
628
629 fn flipped_over(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> GeopResult<bool> {
631 let mid = |(a, b): (Scalar, Scalar)| a.add(b).div(Scalar::TWO);
632 Ok(self.flipped(mid(u)?, mid(v)?))
633 }
634
635 fn shift(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3> {
639 if let Some(Closing {
640 side,
641 apex: Some(apex),
642 }) = self.closing
643 {
644 let x = if side.u_fixed { u } else { v };
645 if x.could_be_equal(*apex) {
646 let s = self.surface.partials(u, v, 2)?;
649 let limit = if side.u_fixed {
650 s[1][0].prod_cross(&s[1][1])
651 } else {
652 s[1][1].prod_cross(&s[0][1])
653 };
654 let limit = if side.at_end { limit.neg() } else { limit };
657 return Ok(limit.normalize()?.prod_scalar(self.distance));
658 }
659 }
660 let normal = self.surface.normal(u, v)?;
661 Ok(normal.prod_scalar(if self.flipped(u, v) {
662 self.distance.neg()
663 } else {
664 self.distance
665 }))
666 }
667
668 fn closes_over(&self, u: (Scalar, Scalar), v: (Scalar, Scalar)) -> Option<Side> {
670 let side = self.closing?.side;
671 let (x, (lo, hi)) = if side.u_fixed {
672 (u, self.surface.domain_u())
673 } else {
674 (v, self.surface.domain_v())
675 };
676 let touches = if side.at_end {
677 x.1.could_be_equal(hi)
678 } else {
679 x.0.could_be_equal(lo)
680 };
681 touches.then_some(side)
682 }
683}
684
685impl NurbSurface3D {
686 pub fn has_normal_everywhere(&self) -> GeopResult<bool> {
692 let mut everywhere = true;
693 over_boxes(
694 self,
695 "whether the surface has a normal",
696 REGULAR_SPLITS,
697 |(u, v)| Ok(parts(&self.sub_surface(u, v)?)?.2),
698 |m, u, v, last, _: Option<&()>| {
699 let m = m.restricted(u, v)?;
700 let squared = m.product(&m, dot)?.hull()[0];
701 Ok(if squared.definitely_greater(Scalar::ZERO) {
702 Said::Is(true)
703 } else if last {
704 Said::Is(false)
705 } else {
706 Said::Split(())
707 })
708 },
709 |has, _, _| everywhere &= has,
710 )?;
711 Ok(everywhere)
712 }
713
714 pub(super) fn fitted_offset(
721 &self,
722 distance: Scalar,
723 closing: Option<&Closing>,
724 ) -> GeopResult<Option<Self>> {
725 let ctx = |e: GeopError| e.with_context(format!("fitted_offset(distance={distance:?})"));
726 let offsetting = Offsetting {
727 surface: self,
728 distance,
729 closing,
730 };
731 let (mut regular, mut collapsed, mut undecided) = (None, None, None);
734 over_boxes(
735 self,
736 "whether the offset is regular",
737 REGULAR_SPLITS,
738 |(u, v)| {
739 Ok((
740 Shape::new(&self.sub_surface(u, v)?)?,
741 offsetting.distance_over(u, v)?,
742 offsetting.flipped_over(u, v)?,
743 offsetting.closes_over(u, v),
744 ))
745 },
746 |(shape, distance, flipped, closes), u, v, last, parent: Option<&(f64, usize)>| {
747 let (said, size) = shape.regular(*distance, u, v, *closes, *flipped)?;
748 let stalled = match parent {
751 Some(&(p, n)) if p <= 2.0 * size => n + 1,
752 _ => 0,
753 };
754 Ok(match said {
755 Some(said) => Said::Is(Some(said)),
756 None if last || stalled >= STALLED_SPLITS => Said::Is(None),
763 None => Said::Split((size, stalled)),
764 })
765 },
766 |said, u, v| match said {
767 Some(true) => regular = Some((u, v)),
768 Some(false) => collapsed = Some((u, v)),
769 None => undecided = Some((u, v)),
770 },
771 )
772 .map_err(ctx)?;
773 match (regular, collapsed, undecided) {
774 (Some(at), Some(not), _) => {
775 return Err(ctx(GeopError::new(format!(
776 "the offset by {distance:?} folds over itself: it is regular over u={:?}, v={:?}, but passes a centre of curvature over u={:?}, v={:?}",
777 at.0, at.1, not.0, not.1
778 ))));
779 }
780 (Some(at), None, Some((u, v))) => {
781 return Err(ctx(GeopError::new(format!(
782 "the offset by {distance:?} could fold over itself over u={u:?}, v={v:?}: a factor 1 − d κ could be zero there, and it is regular over u={:?}, v={:?}",
783 at.0, at.1
784 ))));
785 }
786 (None, ..) => return Ok(None),
788 _ => {}
789 }
790 let own = self.fitted_shift(&offsetting).map_err(ctx)?;
796 let (net, shift) = if own.fits(&offsetting).map_err(ctx)? {
797 (self.clone(), own)
798 } else {
799 let mut base = self.clone();
800 while base.degree_u < 3 {
801 base = base.elevate_degree(true).map_err(ctx)?;
802 }
803 while base.degree_v < 3 {
804 base = base.elevate_degree(false).map_err(ctx)?;
805 }
806 let mut pieces = 1;
807 loop {
808 let net = base.refined([pieces, pieces]).map_err(ctx)?;
809 let shift = net.fitted_shift(&offsetting).map_err(ctx)?;
810 if pieces < MAX_OFFSET_PIECES && !shift.fits(&offsetting).map_err(ctx)? {
811 pieces *= 2;
812 continue;
813 }
814 break (net, shift);
815 }
816 };
817 let drift = Scalar::from_f64(OFFSET_DRIFT);
818 let mut bounds = Vec::new();
819 over_boxes(
820 &shift,
821 "the bound on what the fit misses",
822 BOUND_SPLITS,
823 |(u, v)| {
824 Ok((
825 Fit::new(&self.sub_surface(u, v)?, &shift.sub_surface(u, v)?)?,
826 offsetting.distance_over(u, v)?,
827 ))
828 },
829 |(fit, distance), u, v, last, _: Option<&()>| {
833 Ok(
834 match fit
835 .missed(*distance, u, v)?
836 .filter(|p| last || !p.definitely_greater(drift))
837 {
838 Some(missed) => Said::Is(missed),
839 None => Said::Split(()),
840 },
841 )
842 },
843 |missed, u, v| bounds.push((u, v, missed)),
844 )
845 .map_err(|e| ctx(e).with_context(format!("the shift fitted on {net:?}")))?;
846 let offset = net.shifted(&shift, &bounds);
847 if let Some(closing) = closing
848 && !poles(&offset)?.contains(&closing.side)
849 {
850 return Err(ctx(GeopError::new(format!(
851 "the offset of the apex does not close in a point along its side {:?}: the offsets of the surface round it do not meet in one point",
852 closing.side
853 ))));
854 }
855 Ok(Some(offset))
856 }
857
858 fn fits(&self, offsetting: &Offsetting) -> GeopResult<bool> {
862 let drift = Scalar::from_f64(OFFSET_DRIFT / 2.);
863 let middles = |knots: &[Scalar], degree: usize, n: usize| -> GeopResult<Vec<Scalar>> {
864 let b = breakpoints(knots, degree, n);
865 b.windows(2)
866 .flat_map(|w| [1, 2, 3].map(|k| (w[0], w[1], k)))
867 .map(|(a, b, k)| Ok(Scalar::fraction(a, b, Scalar::from_ratio(k, 4)?)))
868 .collect()
869 };
870 for u in middles(&self.knot_vector_u, self.degree_u, self.num_u)? {
871 for v in middles(&self.knot_vector_v, self.degree_v, self.num_v)? {
872 let e = self.evaluate(u, v)?.sub(&offsetting.shift(u, v)?);
873 if (0..3).any(|k| e[k].abs().definitely_greater(drift)) {
874 return Ok(false);
875 }
876 }
877 }
878 Ok(true)
879 }
880
881 fn fitted_shift(&self, offsetting: &Offsetting) -> GeopResult<Self> {
887 let greville = |knots: &[Scalar], degree: usize, n: usize| -> GeopResult<Vec<Scalar>> {
888 (0..n)
889 .map(|i| {
890 let sum = knots[i + 1..=i + degree]
891 .iter()
892 .fold(Scalar::ZERO, |a, &k| a.add(k));
893 let mean = sum.div(Scalar::from_i64(degree as i64))?.sharpen();
896 Ok(mean.max(knots[degree]).min(knots[n]))
897 })
898 .collect()
899 };
900 let (nu, nv) = (self.num_u, self.num_v);
901 let gu = greville(&self.knot_vector_u, self.degree_u, nu)?;
902 let gv = greville(&self.knot_vector_v, self.degree_v, nv)?;
903 let mut rows = Vec::with_capacity(nu);
905 for &u in &gu {
906 let values = gv
907 .iter()
908 .map(|&v| {
909 let shift = offsetting.shift(u, v).map_err(|e| {
910 e.with_context(format!("the shift at the Greville point {:?}", (u, v)))
911 })?;
912 Ok(shift.prod_scalar(self.weight(u, v)?))
913 })
914 .collect::<GeopResult<Vec<Vector3>>>()?;
915 rows.push(solve_interpolation_system(
916 &gv,
917 &self.knot_vector_v,
918 self.degree_v,
919 &values,
920 )?);
921 }
922 let mut shifts = vec![Vector3::zero(); nu * nv];
923 for j in 0..nv {
924 let column: Vec<Vector3> = rows.iter().map(|row| row[j]).collect();
925 let solved =
926 solve_interpolation_system(&gu, &self.knot_vector_u, self.degree_u, &column)?;
927 for (i, s) in solved.into_iter().enumerate() {
928 shifts[i * nv + j] = s;
929 }
930 }
931 let control_points = shifts
932 .iter()
933 .zip(&self.control_points)
934 .map(|(s, cp)| {
935 Vector::from_array([s[0].sharpen(), s[1].sharpen(), s[2].sharpen(), cp[3]])
936 })
937 .collect();
938 NurbSurface3D::try_new(
939 self.degree_u,
940 self.degree_v,
941 control_points,
942 self.knot_vector_u.clone(),
943 self.knot_vector_v.clone(),
944 )
945 }
946
947 #[allow(clippy::type_complexity)]
955 fn shifted(
956 &self,
957 shift: &NurbSurface3D,
958 bounds: &[((Scalar, Scalar), (Scalar, Scalar), Scalar)],
959 ) -> Self {
960 let support = |knots: &[Scalar], degree: usize, i: usize, (a, b): (Scalar, Scalar)| {
961 knots[i].definitely_less(b) && a.definitely_less(knots[i + degree + 1])
962 };
963 let control_points = self
964 .control_points
965 .iter()
966 .zip(&shift.control_points)
967 .enumerate()
968 .map(|(index, (cp, s))| {
969 let (i, j) = (index / self.num_v, index % self.num_v);
970 let pad = bounds
971 .iter()
972 .filter(|(u, v, _)| {
973 support(&self.knot_vector_u, self.degree_u, i, *u)
974 && support(&self.knot_vector_v, self.degree_v, j, *v)
975 })
976 .fold(Scalar::ZERO, |pad, (_, _, missed)| pad.max(*missed));
977 let w = cp[3];
978 let mut out = *cp;
979 let d = pad.mul(w);
980 for k in 0..3 {
981 let moved = cp[k].add(s[k]);
982 out[k] = moved.sub(d).union(moved.add(d));
983 }
984 out
985 })
986 .collect();
987 let mut out = self.clone();
988 out.control_points = control_points;
989 out.recompute_aabb();
990 out
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use geop_core_math::{scalars::Scalar, vector::Vector4};
997
998 use super::*;
999 use crate::test_fixtures::{hom, knots};
1000
1001 fn bump(height: f64) -> NurbSurface3D {
1004 let z = |i: usize, j: usize| {
1005 let edge = |k: usize| k == 0 || k == 3;
1006 if edge(i) || edge(j) { 0. } else { height }
1007 };
1008 let control_points: Vec<Vector4> = (0..4)
1009 .flat_map(|i| (0..4).map(move |j| (i, j)))
1010 .map(|(i, j)| hom(i as f64 / 3., j as f64 / 3., z(i, j), 1.))
1011 .collect();
1012 let k = knots(&[0., 0., 0., 0., 1., 1., 1., 1.]);
1013 NurbSurface3D::try_new(3, 3, control_points, k.clone(), k).unwrap()
1014 }
1015
1016 fn assert_encloses(surface: &NurbSurface3D, offset: &NurbSurface3D, distance: f64, width: f64) {
1020 for a in 0..=7 {
1021 for b in 0..=7 {
1022 let (u, v) = (
1023 Scalar::from_ratio(a, 7).unwrap(),
1024 Scalar::from_ratio(b, 7).unwrap(),
1025 );
1026 let expected = surface.evaluate(u, v).unwrap().add(
1027 &surface
1028 .normal(u, v)
1029 .unwrap()
1030 .prod_scalar(Scalar::from_f64(distance)),
1031 );
1032 let got = offset.evaluate(u, v).unwrap();
1033 assert!(
1034 got.could_be_equal(&expected),
1035 "at ({a}, {b}): {got:?}, expected {expected:?}"
1036 );
1037 }
1038 }
1039 for cp in &offset.control_points {
1040 for k in 0..3 {
1041 assert!(cp[k].width().to_f64() < width, "{cp:?}");
1042 }
1043 }
1044 }
1045
1046 #[test]
1051 fn the_fit_converges() {
1052 let surface = bump(0.5);
1053 let distance = Scalar::from_f64(0.1);
1054 let error = |pieces: usize| {
1055 let net = surface.refined([pieces, pieces]).unwrap();
1056 let offsetting = Offsetting {
1057 surface: &surface,
1058 distance,
1059 closing: None,
1060 };
1061 let shift = net.fitted_shift(&offsetting).unwrap();
1062 let mut worst: f64 = 0.;
1063 for a in 0..=13 {
1064 for b in 0..=13 {
1065 let (u, v) = (
1066 Scalar::from_ratio(a, 13).unwrap(),
1067 Scalar::from_ratio(b, 13).unwrap(),
1068 );
1069 let e = shift
1070 .evaluate(u, v)
1071 .unwrap()
1072 .sub(&surface.normal(u, v).unwrap().prod_scalar(distance));
1073 worst = (0..3).map(|c| e[c].abs().to_f64()).fold(worst, f64::max);
1074 }
1075 }
1076 worst
1077 };
1078 let errors: Vec<f64> = [1, 2, 4, 8].map(error).to_vec();
1079 assert!(errors.windows(2).all(|w| w[1] < w[0] / 3.), "{errors:?}");
1080 }
1081
1082 #[test]
1084 fn a_free_form_bump_is_offset_both_ways() {
1085 let surface = bump(0.1);
1086 for d in [0.05, -0.05] {
1087 let offset = surface.offset(Scalar::from_f64(d)).unwrap().unwrap();
1088 assert_encloses(&surface, &offset, d, 5e-5);
1089 }
1090 }
1091
1092 #[test]
1095 #[ignore = "slow: 7 s — run with `cargo test -- --ignored`; `a_free_form_bump_is_offset_both_ways` is the fast case"]
1096 fn a_steep_free_form_bump_is_offset_both_ways() {
1097 let surface = bump(0.5);
1098 for d in [0.1, -0.1] {
1099 let offset = surface.offset(Scalar::from_f64(d)).unwrap().unwrap();
1100 assert_encloses(&surface, &offset, d, 5e-5);
1101 }
1102 }
1103
1104 #[test]
1107 fn a_rational_patch_is_fitted() {
1108 let r2 = std::f64::consts::FRAC_1_SQRT_2;
1109 let ring = [(2., 0., 1.), (2., 2., r2), (0., 2., 1.)];
1110 let tube = [(1., 0., 1.), (1., 1., r2), (0., 1., 1.)];
1111 let control_points: Vec<Vector4> = ring
1112 .iter()
1113 .flat_map(|&(x, y, wr)| {
1114 tube.iter().map(move |&(r, z, wt)| {
1115 let (x, y) = (x * (1. + r) / 2., y * (1. + r) / 2.);
1116 let w = wr * wt;
1117 hom(x * w, y * w, z * w, w)
1118 })
1119 })
1120 .collect();
1121 let k = knots(&[0., 0., 0., 1., 1., 1.]);
1122 let torus = NurbSurface3D::try_new(2, 2, control_points, k.clone(), k).unwrap();
1123 for d in [0.25, -0.25] {
1124 let fitted = torus
1125 .fitted_offset(Scalar::from_f64(d), None)
1126 .unwrap()
1127 .unwrap();
1128 assert_encloses(&torus, &fitted, d, 5e-5);
1129 }
1130 }
1131
1132 #[test]
1137 fn a_patch_with_a_pole_is_fitted() {
1138 let sphere = crate::test_fixtures::sphere_octant();
1139 for d in [0.2, -0.2] {
1140 let fitted = sphere
1141 .fitted_offset(Scalar::from_f64(d), None)
1142 .unwrap()
1143 .unwrap();
1144 assert_encloses(&sphere, &fitted, d, 5e-5);
1145 }
1146 }
1147
1148 #[test]
1152 fn a_sphere_offset_past_its_centre_collapses() {
1153 let sphere = crate::test_fixtures::sphere_octant();
1154 assert!(
1155 sphere
1156 .fitted_offset(Scalar::from_f64(-1.5), None)
1157 .unwrap()
1158 .is_none()
1159 );
1160 }
1161
1162 #[test]
1165 fn an_offset_past_a_centre_of_curvature_is_refused() {
1166 let error = bump(2.).offset(Scalar::from_f64(-0.5)).unwrap_err();
1167 assert!(error.root_message().contains("fold"), "{error:?}");
1168 }
1169}