1use geop_core_math::{
21 geop_error::{GeopError, GeopResult},
22 scalars::Scalar,
23};
24
25use super::{NurbSurface3D, net::Side};
26
27pub(super) struct Closing {
32 pub side: Side,
33 pub apex: Option<Scalar>,
34}
35
36const CLOSING_STEPS: usize = 64;
40
41impl NurbSurface3D {
42 pub fn offset(&self, distance: Scalar) -> GeopResult<Option<Self>> {
56 if distance.is_zero() {
57 return Ok(Some(self.clone()));
58 }
59 let ctx =
60 |e: GeopError| e.with_context(format!("NurbSurface::offset(distance={distance:?})"));
61 match self.closed(distance).map_err(ctx)? {
62 Some((surface, closing)) => surface.fitted_offset(distance, Some(&closing)),
63 None => self.fitted_offset(distance, None).map_err(|e| {
64 let turning: Vec<_> = (Side::ALL.iter())
67 .map(|side| {
68 let ((lo, hi), (a, b)) = if side.u_fixed {
69 (self.domain_u(), self.domain_v())
70 } else {
71 (self.domain_v(), self.domain_u())
72 };
73 let x = if side.at_end { hi } else { lo };
74 let middle = a.add(b).div(Scalar::TWO)?.sharpen();
75 self.row_turning(side.u_fixed, middle, x, distance, false)
76 })
77 .collect();
78 e.with_context(format!(
79 "closing nowhere inside over u={:?}, v={:?}: its rows turn by {turning:?} at its sides",
80 self.domain_u(),
81 self.domain_v()
82 ))
83 }),
84 }
85 }
86
87 fn row_turning(
94 &self,
95 across_u: bool,
96 middle: Scalar,
97 x: Scalar,
98 distance: Scalar,
99 flipped: bool,
100 ) -> GeopResult<Scalar> {
101 let (u, v) = if across_u { (x, middle) } else { (middle, x) };
102 let s = self.partials(u, v, 2)?;
103 let (along, bend) = if across_u {
104 (s[0][1], s[0][2])
105 } else {
106 (s[1][0], s[2][0])
107 };
108 let normal = self.normal(u, v)?;
109 let normal = if flipped { normal.neg() } else { normal };
110 Ok(along
111 .prod_dot(&along)
112 .sub(distance.mul(normal.prod_dot(&bend))))
113 }
114
115 fn closed(&self, distance: Scalar) -> GeopResult<Option<(Self, Closing)>> {
119 let apexes: Vec<Side> = Side::ALL
120 .into_iter()
121 .filter(|s| self.is_apex(s.u_fixed, !s.at_end))
122 .collect();
123 let (side, cut, longer) = match apexes.as_slice() {
124 [] => match self.closes_inside(distance)? {
125 Some((side, cut)) => (side, cut, None),
126 None => return Ok(None),
127 },
128 [side] => {
129 let (cut, longer) = self.closes_at_apex(*side, distance)?;
130 (*side, cut, longer)
131 }
132 _ => {
133 return Err(GeopError::new(format!(
134 "the surface has apexes at {apexes:?}: offsetting more than one is not supported"
135 )));
136 }
137 };
138 let across_u = side.u_fixed;
139 let ((lo, hi), (other_lo, other_hi)) = if across_u {
140 (self.domain_u(), self.domain_v())
141 } else {
142 (self.domain_v(), self.domain_u())
143 };
144 let far = if side.at_end { lo } else { hi };
145 let (surface, apex_inside) = match &longer {
146 Some((longer, apex)) => (longer, Some(*apex)),
147 None => (self, None),
148 };
149 let (c0, c1) = if side.at_end { (far, cut) } else { (cut, far) };
150 let whole = (other_lo, other_hi);
151 let piece = if across_u {
152 surface.sub_surface((c0, c1), whole)?
153 } else {
154 surface.sub_surface(whole, (c0, c1))?
155 };
156 let scale = hi.sub(lo).div(c1.sub(c0))?;
158 let map = |k: Scalar| lo.add(k.sub(c0).mul(scale));
159 let relabel = |knots: &[Scalar], degree: usize| -> Vec<Scalar> {
160 let len = knots.len();
161 knots
162 .iter()
163 .enumerate()
164 .map(|(i, &k)| {
165 if i <= degree {
166 lo
167 } else if i >= len - degree - 1 {
168 hi
169 } else {
170 map(k)
171 }
172 })
173 .collect()
174 };
175 let (knots_u, knots_v) = if across_u {
176 (
177 relabel(&piece.knot_vector_u, piece.degree_u),
178 piece.knot_vector_v.clone(),
179 )
180 } else {
181 (
182 piece.knot_vector_u.clone(),
183 relabel(&piece.knot_vector_v, piece.degree_v),
184 )
185 };
186 let apex_inside =
190 match apex_inside {
191 Some(apex) => {
192 let (old, new) = if across_u {
193 (&piece.knot_vector_u, &knots_u)
194 } else {
195 (&piece.knot_vector_v, &knots_v)
196 };
197 let i = old.iter().position(|k| k.could_be_equal(apex)).ok_or_else(|| {
198 GeopError::new(format!(
199 "the apex at {apex:?} is no knot of the surface carried on through it"
200 ))
201 })?;
202 Some(new[i])
203 }
204 None => None,
205 };
206 let closed = Self::try_new(
207 piece.degree_u,
208 piece.degree_v,
209 piece.control_points,
210 knots_u,
211 knots_v,
212 )?;
213 Ok(Some((
214 closed,
215 Closing {
216 side,
217 apex: apex_inside,
218 },
219 )))
220 }
221
222 fn closes_inside(&self, distance: Scalar) -> GeopResult<Option<(Side, Scalar)>> {
231 let mut found: Option<(Side, Scalar)> = None;
232 for across_u in [true, false] {
233 let ((lo, hi), (other_lo, other_hi)) = if across_u {
234 (self.domain_u(), self.domain_v())
235 } else {
236 (self.domain_v(), self.domain_u())
237 };
238 let middle = other_lo.add(other_hi).div(Scalar::TWO)?.sharpen();
239 let turning = |x: Scalar| self.row_turning(across_u, middle, x, distance, false);
240 let (at_lo, at_hi) = (turning(lo)?, turning(hi)?);
241 let positive = |k: Scalar| k.definitely_greater(Scalar::ZERO);
242 let negative = |k: Scalar| k.definitely_less(Scalar::ZERO);
243 let at_end = if positive(at_lo) && negative(at_hi) {
244 true
245 } else if positive(at_hi) && negative(at_lo) {
246 false
247 } else {
248 continue;
249 };
250 let (mut good, mut bad, mut k_good, mut k_bad) = if at_end {
254 (lo, hi, at_lo, at_hi)
255 } else {
256 (hi, lo, at_hi, at_lo)
257 };
258 let mut kept: Option<bool> = None;
259 for _ in 0..CLOSING_STEPS {
260 let step = k_good.mul(bad.sub(good)).div(k_good.sub(k_bad))?;
261 let mut next = good.add(step).sharpen();
262 let between = |x: Scalar| {
263 let (a, b) = if good.definitely_less(bad) {
264 (good, bad)
265 } else {
266 (bad, good)
267 };
268 x.definitely_greater(a) && x.definitely_less(b)
269 };
270 if !between(next) {
271 next = good.add(bad).div(Scalar::TWO)?.sharpen();
272 if !between(next) {
273 break;
274 }
275 }
276 let k = turning(next)?;
277 if k.could_be_equal(Scalar::ZERO) {
278 good = next;
279 break;
280 }
281 if positive(k) {
282 (good, k_good) = (next, k);
283 if kept == Some(false) {
284 k_bad = k_bad.div(Scalar::TWO)?;
285 }
286 kept = Some(false);
287 } else {
288 (bad, k_bad) = (next, k);
289 if kept == Some(true) {
290 k_good = k_good.div(Scalar::TWO)?;
291 }
292 kept = Some(true);
293 }
294 }
295 let side = Side {
299 u_fixed: across_u,
300 at_end,
301 };
302 if let Some((first, _)) = found {
303 return Err(GeopError::new(format!(
304 "the offset by {distance:?} closes inside the surface along {first:?} and along {side:?}: not supported"
305 )));
306 }
307 found = Some((side, good));
308 }
309 Ok(found)
310 }
311
312 fn closes_at_apex(
319 &self,
320 side: Side,
321 distance: Scalar,
322 ) -> GeopResult<(Scalar, Option<(Self, Scalar)>)> {
323 let across_u = side.u_fixed;
324 let ((lo, hi), (other_lo, other_hi), breaks) = if across_u {
325 (self.domain_u(), self.domain_v(), self.breakpoints_u())
326 } else {
327 (self.domain_v(), self.domain_u(), self.breakpoints_v())
328 };
329 let (apex, far) = if side.at_end { (hi, lo) } else { (lo, hi) };
330 let n = breaks.len();
333 let room = if side.at_end {
334 breaks[n - 1].sub(breaks[n - 2])
335 } else {
336 breaks[1].sub(breaks[0])
337 }
338 .sharpen();
339 let longer = self.extended(across_u, side.at_end, room)?;
340 let middle = other_lo.add(other_hi).div(Scalar::TWO)?.sharpen();
341 let closes = |x: Scalar| -> GeopResult<Scalar> {
342 let past = if side.at_end {
344 x.definitely_greater(apex)
345 } else {
346 x.definitely_less(apex)
347 };
348 longer
349 .row_turning(across_u, middle, x, distance, past)?
350 .div(x.sub(apex))
351 };
352 let inward = if side.at_end { room.neg() } else { room };
353 let mut a = apex.add(inward.div(Scalar::from_i64(4))?).sharpen();
354 let mut b = apex.add(inward.div(Scalar::TWO)?).sharpen();
355 let (mut ka, mut kb) = (closes(a)?, closes(b)?);
356 let (reach_lo, reach_hi) = if side.at_end {
357 (far, apex.add(room))
358 } else {
359 (apex.sub(room), far)
360 };
361 for _ in 0..CLOSING_STEPS {
362 if kb.could_be_equal(Scalar::ZERO) {
363 break;
364 }
365 let slope = kb.sub(ka).div(b.sub(a))?;
366 let next = b.sub(kb.div(slope)?).sharpen();
367 if next.could_be_equal(b) {
368 break;
369 }
370 if !next.definitely_greater(reach_lo) || !next.definitely_less(reach_hi) {
371 return Err(GeopError::new(format!(
372 "the offset by {distance:?} closes past the apex at {apex:?} farther than {room:?}, or not before the far side {far:?}: at {next:?}"
373 )));
374 }
375 if next.could_be_equal(apex) {
376 return Err(GeopError::new(format!(
377 "the offset by {distance:?} closes at the apex itself, {apex:?}"
378 )));
379 }
380 (a, ka) = (b, kb);
381 b = next;
382 kb = closes(b)?;
383 }
384 let inside = if side.at_end {
386 b.definitely_less(apex)
387 } else {
388 b.definitely_greater(apex)
389 };
390 Ok((b, (!inside).then_some((longer, apex))))
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use crate::test_fixtures::knots;
397 use geop_core_math::{
398 scalars::Scalar,
399 vector::{Vector3, Vector4},
400 };
401
402 use super::*;
403
404 const R2: f64 = std::f64::consts::FRAC_1_SQRT_2;
405
406 fn h(x: f64, y: f64, z: f64, w: f64) -> Vector4 {
407 Vector4::from_array([x * w, y * w, z * w, w].map(Scalar::from_f64))
408 }
409
410 fn assert_offset(surface: &NurbSurface3D, offset: &NurbSurface3D, distance: f64) {
413 let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
414 for a in 0..=4 {
415 for b in 0..=4 {
416 let u = Scalar::interpolate(u0, u1, Scalar::from_f64(a as f64 / 4.0));
417 let v = Scalar::interpolate(v0, v1, Scalar::from_f64(b as f64 / 4.0));
418 let expected = surface.evaluate(u, v).unwrap().add(
419 &surface
420 .normal(u, v)
421 .unwrap()
422 .prod_scalar(Scalar::from_f64(distance)),
423 );
424 let got = offset.evaluate(u, v).unwrap();
425 assert!(
426 got.could_be_equal(&expected),
427 "at ({a}, {b}): {got:?}, expected {expected:?}"
428 );
429 }
430 }
431 }
432
433 fn assert_exact(surface: &NurbSurface3D, offset: &NurbSurface3D, distance: f64) {
437 assert_offset(surface, offset, distance);
438 assert_eq!(
439 (offset.degree_u, offset.degree_v, offset.num_u, offset.num_v),
440 (
441 surface.degree_u,
442 surface.degree_v,
443 surface.num_u,
444 surface.num_v
445 )
446 );
447 for cp in &offset.control_points {
448 for k in 0..4 {
449 assert!(cp[k].width().to_f64() < 2e-13, "{cp:?}");
450 }
451 }
452 }
453
454 #[test]
455 fn planes_move_along_their_normal() {
456 let flat = NurbSurface3D::try_new(
457 1,
458 1,
459 vec![
460 h(0., 0., 1., 1.),
461 h(0., 2., 1.5, 1.),
462 h(1., 0., 1., 1.),
463 h(1., 2., 1.5, 1.),
464 ],
465 knots(&[0., 0., 1., 1.]),
466 knots(&[0., 0., 1., 1.]),
467 )
468 .unwrap();
469 assert_exact(
470 &flat,
471 &flat.offset(Scalar::from_f64(0.3)).unwrap().unwrap(),
472 0.3,
473 );
474 assert_exact(
475 &flat,
476 &flat.offset(Scalar::from_f64(-0.3)).unwrap().unwrap(),
477 -0.3,
478 );
479 }
480
481 fn quarter_cylinder() -> NurbSurface3D {
484 let ring = [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)];
485 let cps = ring
486 .iter()
487 .flat_map(|&(x, y, w)| [h(x, y, 0., w), h(x, y, 3., w)])
488 .collect();
489 NurbSurface3D::try_new(
490 2,
491 1,
492 cps,
493 knots(&[0., 0., 0., 1., 1., 1.]),
494 knots(&[0., 0., 1., 1.]),
495 )
496 .unwrap()
497 }
498
499 #[test]
500 fn cylinders_change_radius() {
501 let cylinder = quarter_cylinder();
502 for d in [0.5, -0.5, -1.5] {
503 assert_exact(
504 &cylinder,
505 &cylinder.offset(Scalar::from_f64(d)).unwrap().unwrap(),
506 d,
507 );
508 }
509 assert!(cylinder.offset(Scalar::from_f64(-2.5)).unwrap().is_none());
510 assert!(cylinder.offset(Scalar::from_f64(-2.0)).unwrap().is_none());
514 }
515
516 #[test]
519 fn cones_move_along_their_normal() {
520 let ring = [(1., 0., 1.), (1., 1., R2), (0., 1., 1.)];
521 let cps = ring
522 .iter()
523 .flat_map(|&(x, y, w)| [h(2. * x, 2. * y, 0., w), h(x, y, 1., w)])
524 .collect();
525 let cone = NurbSurface3D::try_new(
526 2,
527 1,
528 cps,
529 knots(&[0., 0., 0., 1., 1., 1.]),
530 knots(&[0., 0., 1., 1.]),
531 )
532 .unwrap();
533 assert_exact(
534 &cone,
535 &cone.offset(Scalar::from_f64(-0.25)).unwrap().unwrap(),
536 -0.25,
537 );
538 }
539
540 #[test]
543 fn spheres_change_radius() {
544 let meridian = [(2., 0., 1.), (2., 2., R2), (0., 2., 1.)];
545 let ring = [(1., 0., 1.), (1., 1., R2), (0., 1., 1.)];
546 let cps = ring
547 .iter()
548 .flat_map(|&(x, y, wr)| {
549 meridian
550 .iter()
551 .map(move |&(r, z, wm)| h(r * x, r * y, z, wr * wm))
552 })
553 .collect();
554 let sphere = NurbSurface3D::try_new(
555 2,
556 2,
557 cps,
558 knots(&[0., 0., 0., 1., 1., 1.]),
559 knots(&[0., 0., 0., 1., 1., 1.]),
560 )
561 .unwrap();
562 assert_exact(
564 &sphere,
565 &sphere.offset(Scalar::from_f64(0.5)).unwrap().unwrap(),
566 0.5,
567 );
568 assert_exact(
569 &sphere,
570 &sphere.offset(Scalar::from_f64(-0.5)).unwrap().unwrap(),
571 -0.5,
572 );
573 }
574
575 #[test]
578 fn tori_change_tube_radius() {
579 let tube = [(4., 0., 1.), (4., 1., R2), (3., 1., 1.)];
580 let ring = [(1., 0., 1.), (1., 1., R2), (0., 1., 1.)];
581 let cps = ring
582 .iter()
583 .flat_map(|&(x, y, wr)| {
584 tube.iter()
585 .map(move |&(r, z, wt)| h(r * x, r * y, z, wr * wt))
586 })
587 .collect();
588 let torus = NurbSurface3D::try_new(
589 2,
590 2,
591 cps,
592 knots(&[0., 0., 0., 1., 1., 1.]),
593 knots(&[0., 0., 0., 1., 1., 1.]),
594 )
595 .unwrap();
596 assert_exact(
597 &torus,
598 &torus.offset(Scalar::from_f64(0.25)).unwrap().unwrap(),
599 0.25,
600 );
601 assert_exact(
602 &torus,
603 &torus.offset(Scalar::from_f64(-0.25)).unwrap().unwrap(),
604 -0.25,
605 );
606 }
607
608 #[test]
613 fn a_cones_apex_goes_to_the_offset_cones_apex() {
614 let cone = crate::test_fixtures::revolved(1., 0.);
615 for d in [0.25, -0.25] {
616 let offset = cone.offset(Scalar::from_f64(d)).unwrap().unwrap();
617 let rim =
618 |s: &NurbSurface3D, u: f64| s.evaluate(Scalar::from_f64(u), Scalar::ONE).unwrap();
619 let shift = rim(&offset, 0.5).sub(&rim(&cone, 0.5));
620 let pole = offset.pole_at(Scalar::ZERO, Scalar::ZERO).unwrap();
621 let up = shift[2].definitely_greater(Scalar::ZERO);
623 let z = 1. + if up { 0.25 } else { -0.25 } * std::f64::consts::SQRT_2;
624 let expected = Vector3::from_array([0., 0., z].map(Scalar::from_f64));
625 assert!(pole.could_be_equal(&expected), "{d}: {pole:?}");
626 for u in [0., 0.5, 1.] {
627 let (u, v) = (Scalar::from_f64(u), Scalar::ONE);
628 let expected = cone
629 .evaluate(u, v)
630 .unwrap()
631 .add(&cone.normal(u, v).unwrap().prod_scalar(Scalar::from_f64(d)));
632 assert!(
633 offset.evaluate(u, v).unwrap().could_be_equal(&expected),
634 "{d}: {u:?}"
635 );
636 }
637 }
638 }
639
640 #[test]
646 fn a_cone_cut_off_short_of_its_tip_closes_inside() {
647 let ring = [(1., 0., 1.), (1., 1., R2), (0., 1., 1.)];
648 let cps = ring
649 .iter()
650 .flat_map(|&(x, y, w)| [h(x, y, 0., w), h(0.1 * x, 0.1 * y, 2., w)])
651 .collect();
652 let cone = NurbSurface3D::try_new(
653 2,
654 1,
655 cps,
656 knots(&[0., 0., 0., 1., 1., 1.]),
657 knots(&[0., 0., 1., 1.]),
658 )
659 .unwrap();
660 let sin = 0.45 / (1f64 + 0.45 * 0.45).sqrt();
661 let apex = Vector3::from_array([0., 0., 2. + 0.1 / 0.45 - 0.2 / sin].map(Scalar::from_f64));
662 let mut closed = 0;
663 for d in [0.2, -0.2] {
664 let offset = cone.offset(Scalar::from_f64(d)).unwrap().unwrap();
665 match offset.pole_at(Scalar::ZERO, Scalar::ONE) {
666 Some(pole) => {
667 assert!(pole.could_be_equal(&apex), "{d}: {pole:?} vs {apex:?}");
668 closed += 1;
669 }
670 None => assert_exact(&cone, &offset, d),
671 }
672 }
673 assert_eq!(closed, 1);
674 }
675
676 #[test]
679 fn freeform_surfaces_are_fitted() {
680 let mut bent = quarter_cylinder();
681 bent.control_points[2] = h(2.5, 2., 0., R2);
682 assert_offset(
683 &bent,
684 &bent.offset(Scalar::from_f64(0.1)).unwrap().unwrap(),
685 0.1,
686 );
687 }
688}