1use geop_core_math::{
11 geop_error::{GeopError, GeopResult},
12 scalars::Scalar,
13 solvers::{
14 linear::solve_pivoted,
15 repeat::{Step, repeat},
16 },
17 vector::{Vector, Vector3, Vector4},
18};
19
20use super::NurbSurface3D;
21use std::sync::Arc;
22
23use crate::{
24 nurb_curve::{NurbCurve, NurbCurve3D, basis_funs, bezier_knots},
25 search::{MAX_NODES, min_subdivision_size},
26 shape::Plane,
27 spline::{breakpoints, find_span},
28};
29
30fn greville(knots: &[Scalar], degree: usize, n: usize) -> GeopResult<Vec<Scalar>> {
35 let p = Scalar::from_i64(degree as i64);
36 (0..n)
37 .map(|i| {
38 knots[i + 1..=i + degree]
39 .iter()
40 .fold(Scalar::ZERO, |sum, &k| sum.add(k))
41 .div(p)
42 })
43 .collect()
44}
45
46fn compatible_pair(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<(NurbCurve3D, NurbCurve3D)> {
49 let mut pair = NurbCurve::compatible(&[a.with_unit_domain()?, b.with_unit_domain()?])?;
50 let b = pair.pop().expect("two curves");
51 let a = pair.pop().expect("two curves");
52 Ok((a, b))
53}
54
55pub(super) const SIDES: usize = 4;
59
60impl NurbSurface3D {
61 pub fn ruled(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self> {
66 let (a, b) = compatible_pair(a, b)?;
67 Self::ruled_compatible(&a, &b)
68 }
69
70 pub fn ruled_compatible(a: &NurbCurve3D, b: &NurbCurve3D) -> GeopResult<Self> {
76 if a.degree != b.degree
77 || a.control_points.len() != b.control_points.len()
78 || a.knot_vector.len() != b.knot_vector.len()
79 {
80 return Err(GeopError::new(format!(
81 "NurbSurface::ruled_compatible: the curves are not alike: {a:?} and {b:?}"
82 )));
83 }
84 let control_points = a
85 .control_points
86 .iter()
87 .zip(&b.control_points)
88 .flat_map(|(p, q)| [*p, *q])
89 .collect();
90 NurbSurface3D::try_new(
91 a.degree,
92 1,
93 control_points,
94 a.knot_vector.clone(),
95 bezier_knots(1),
96 )
97 }
98
99 pub fn bilinear(p00: Vector3, p10: Vector3, p11: Vector3, p01: Vector3) -> GeopResult<Self> {
103 NurbSurface3D::try_new(
104 1,
105 1,
106 [p00, p01, p10, p11]
107 .map(|p| p.homogeneous(Scalar::ONE))
108 .to_vec(),
109 bezier_knots(1),
110 bezier_knots(1),
111 )
112 }
113
114 pub fn coons(sides: [&NurbCurve3D; SIDES]) -> GeopResult<Self> {
135 let unit = |c: &NurbCurve3D| c.with_unit_domain()?.with_unit_end_weights();
136 let bottom = unit(sides[0])?;
137 let right = unit(sides[1])?;
138 let top = unit(&sides[2].reverse())?;
139 let left = unit(&sides[3].reverse())?;
140 let first = |c: &NurbCurve3D| c.start();
141 let last = |c: &NurbCurve3D| c.end();
142 for (k, (end, start)) in [
143 (last(&bottom)?, first(&right)?),
144 (last(&right)?, last(&top)?),
145 (first(&top)?, last(&left)?),
146 (first(&left)?, first(&bottom)?),
147 ]
148 .into_iter()
149 .enumerate()
150 {
151 if !end.could_be_equal(&start) {
152 return Err(GeopError::new(format!(
153 "NurbSurface::coons: side {k} ends at {end:?}, but side {} starts at {start:?}",
154 (k + 1) % SIDES
155 )));
156 }
157 }
158 let (bottom, top) = compatible_pair(&bottom, &top)?;
159 let (left, right) = compatible_pair(&left, &right)?;
160 let (nu, nv) = (bottom.control_points.len(), left.control_points.len());
161 let eta = greville(&bottom.knot_vector, bottom.degree, nu)?;
162 let xi = greville(&left.knot_vector, left.degree, nv)?;
163 let (b, t) = (&bottom.control_points, &top.control_points);
164 let (l, r) = (&left.control_points, &right.control_points);
165 let p00 = b[0].union(&l[0]);
166 let p10 = b[nu - 1].union(&r[0]);
167 let p01 = t[0].union(&l[nv - 1]);
168 let p11 = t[nu - 1].union(&r[nv - 1]);
169 let rational = [b, t, l, r]
170 .iter()
171 .any(|row| row.iter().any(|p| !p[3].is_exactly(Scalar::ONE)));
172
173 let mut control_points = Vec::with_capacity(nu * nv);
174 for i in 0..nu {
175 for j in 0..nv {
176 let mut p = match (i, j) {
177 (0, 0) => p00,
178 (0, _) if j == nv - 1 => p01,
179 (_, 0) if i == nu - 1 => p10,
180 _ if i == nu - 1 && j == nv - 1 => p11,
181 (_, 0) => b[i],
182 (0, _) => l[j],
183 _ if j == nv - 1 => t[i],
184 _ if i == nu - 1 => r[j],
185 _ => {
186 let across = Vector4::interpolate(&b[i], &t[i], xi[j]);
187 let along = Vector4::interpolate(&l[j], &r[j], eta[i]);
188 let corners = Vector4::interpolate(
189 &Vector4::interpolate(&p00, &p01, xi[j]),
190 &Vector4::interpolate(&p10, &p11, xi[j]),
191 eta[i],
192 );
193 across.add(&along).sub(&corners)
194 }
195 };
196 if !rational {
197 p[3] = Scalar::ONE;
200 }
201 control_points.push(p);
202 }
203 }
204 NurbSurface3D::try_new(
205 bottom.degree,
206 left.degree,
207 control_points,
208 bottom.knot_vector.clone(),
209 left.knot_vector.clone(),
210 )
211 }
212
213 pub fn elevate_degree(&self, along_u: bool) -> GeopResult<Self> {
217 let (nu, nv) = (self.num_u, self.num_v);
218 let (degree, knots, rows, len) = if along_u {
219 (self.degree_u, &self.knot_vector_u, nv, nu)
220 } else {
221 (self.degree_v, &self.knot_vector_v, nu, nv)
222 };
223 let index = |row: usize, k: usize| if along_u { k * nv + row } else { row * nv + k };
224 let raised = (0..rows)
225 .map(|row| {
226 let points = (0..len)
227 .map(|k| self.control_points[index(row, k)])
228 .collect();
229 NurbCurve::try_new(degree, points, knots.clone())?.elevate_degree()
230 })
231 .collect::<GeopResult<Vec<_>>>()?;
232 let new_len = raised[0].control_points.len();
233 let new_knots = raised[0].knot_vector.clone();
234 let (num_u, num_v) = if along_u {
235 (new_len, nv)
236 } else {
237 (nu, new_len)
238 };
239 let control_points = (0..num_u)
240 .flat_map(|i| (0..num_v).map(move |j| (i, j)))
241 .map(|(i, j)| {
242 if along_u {
243 raised[j].control_points[i]
244 } else {
245 raised[i].control_points[j]
246 }
247 })
248 .collect();
249 if along_u {
250 NurbSurface3D::try_new(
251 degree + 1,
252 self.degree_v,
253 control_points,
254 new_knots,
255 self.knot_vector_v.clone(),
256 )
257 } else {
258 NurbSurface3D::try_new(
259 self.degree_u,
260 degree + 1,
261 control_points,
262 self.knot_vector_u.clone(),
263 new_knots,
264 )
265 }
266 }
267
268 pub fn extended(&self, along_u: bool, at_end: bool, by: Scalar) -> GeopResult<Self> {
274 let (nu, nv) = (self.num_u, self.num_v);
275 let (degree, knots, rows, len) = if along_u {
276 (self.degree_u, &self.knot_vector_u, nv, nu)
277 } else {
278 (self.degree_v, &self.knot_vector_v, nu, nv)
279 };
280 let index = |row: usize, k: usize| if along_u { k * nv + row } else { row * nv + k };
281 let longer = (0..rows)
282 .map(|row| {
283 let points = (0..len)
284 .map(|k| self.control_points[index(row, k)])
285 .collect();
286 NurbCurve::try_new(degree, points, knots.clone())?.extended(at_end, by)
287 })
288 .collect::<GeopResult<Vec<_>>>()?;
289 let new_len = longer[0].control_points.len();
290 let new_knots = longer[0].knot_vector.clone();
291 let (num_u, num_v) = if along_u {
292 (new_len, nv)
293 } else {
294 (nu, new_len)
295 };
296 let control_points = (0..num_u)
297 .flat_map(|i| (0..num_v).map(move |j| (i, j)))
298 .map(|(i, j)| {
299 if along_u {
300 longer[j].control_points[i]
301 } else {
302 longer[i].control_points[j]
303 }
304 })
305 .collect();
306 if along_u {
307 NurbSurface3D::try_new(
308 degree,
309 self.degree_v,
310 control_points,
311 new_knots,
312 self.knot_vector_v.clone(),
313 )
314 } else {
315 NurbSurface3D::try_new(
316 self.degree_u,
317 degree,
318 control_points,
319 self.knot_vector_u.clone(),
320 new_knots,
321 )
322 }
323 }
324
325 pub fn tangent_to(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Self> {
357 let curved = |k: usize| sides[k].as_ref().is_some_and(Neighbour::tilted);
358 let mut base = self.clone();
359 let mut strays = String::new();
360 for _ in 0..=MOST_REFINEMENTS {
361 let patch = base.tangent_rows(sides, &|_, _| false)?;
362 match patch.misses(sides)?.into_iter().next() {
363 None => return Ok(patch),
364 Some(stray) => strays = stray.what,
365 }
366 let along = |even: bool| (0..SIDES).any(|k| curved(k) && (k % 2 == 0) == even);
367 base = base.refined([1 + usize::from(along(true)), 1 + usize::from(along(false))])?;
368 }
369 Err(GeopError::new(format!(
370 "NurbSurface::tangent_to: refined {MOST_REFINEMENTS} times along its curved neighbours, the patch still does not leave them at {TILT:e} radians: {strays}"
371 )))
372 }
373
374 pub(super) fn tangent_rows(
380 &self,
381 sides: &[Option<Neighbour<'_>>; SIDES],
382 kept: &dyn Fn(usize, usize) -> bool,
383 ) -> GeopResult<Self> {
384 let rows_needed = |a: usize, b: usize| {
389 if sides[a].is_some() || sides[b].is_some() {
390 4
391 } else {
392 0
393 }
394 };
395 let mut surface = self.clone();
396 for (along_u, needed) in [(false, rows_needed(0, 2)), (true, rows_needed(1, 3))] {
400 repeat(needed + 1, || -> GeopResult<_> {
401 let rows = if along_u {
402 surface.num_u
403 } else {
404 surface.num_v
405 };
406 if rows >= needed {
407 return Ok(Step::Done(()));
408 }
409 surface = surface.elevate_degree(along_u)?;
410 Ok(Step::Again)
411 })?
412 .done("raising the patch's degree for its tangent planes")?;
413 }
414 let (nu, nv) = (surface.num_u, surface.num_v);
415 let at = |i: usize, j: usize| i * nv + j;
416 let mut planes: [Vec<(Vector3, Vector3)>; SIDES] = Default::default();
420 for (k, side) in sides.iter().enumerate() {
421 let Some(side) = side else { continue };
422 let rows = SideRows::of(&surface, k);
423 for s in rows.abscissae(&surface)? {
424 let (point, along) = rows.point_along(&surface, s)?;
425 planes[k].push(side.at(&point, &along)?);
426 }
427 if side.flat() {
428 let plane = Plane::try_new(surface.evaluate_corner(&rows)?, planes[k][0].0)?;
429 for i in 0..rows.len() {
430 let p = surface.control_points[rows.index(i).0].dehomogenized()?;
431 if !plane.signed_distance(&p).could_be_equal(Scalar::ZERO) {
432 return Err(GeopError::new(format!(
433 "NurbSurface::tangent_to: side {k} leaves the plane of its flat neighbour at control point {p:?}"
434 )));
435 }
436 }
437 }
438 }
439 let next_row = |k: usize, i: usize, j: usize| -> Option<(usize, (usize, usize))> {
442 match k {
443 0 if j == 1 => Some((i, (i, 0))),
444 1 if i == nu - 2 => Some((j, (nu - 1, j))),
445 2 if j == nv - 2 => Some((i, (i, nv - 1))),
446 3 if i == 1 => Some((j, (0, j))),
447 _ => None,
448 }
449 };
450 let original = surface.control_points.clone();
451 let mut shared: [Vec<bool>; SIDES] = Default::default();
454 for k in 0..SIDES {
455 shared[k] = vec![false; planes[k].len()];
456 }
457 for i in 0..nu {
458 for j in 0..nv {
459 type Constraint = (usize, usize, Plane, Plane, Vector3, Vector3);
464 let mut constraints: Vec<Constraint> = Vec::new();
465 for k in 0..SIDES {
466 let Some((position, (bi, bj))) = next_row(k, i, j) else {
467 continue;
468 };
469 let Some(side) = &sides[k] else { continue };
470 let (normal, away) = planes[k][position];
471 let base = original[at(bi, bj)].dehomogenized()?;
472 let leant = match side.tilted() {
473 false => normal,
474 true => normal.sub(&away.normalize()?.prod_scalar(Scalar::from_f64(TILT))),
475 };
476 constraints.push((
477 k,
478 position,
479 Plane::try_new(base, normal)?,
480 Plane::try_new(base, leant)?,
481 away,
482 base,
483 ));
484 }
485 if constraints.is_empty() {
486 continue;
487 }
488 let keep = kept(i, j);
489 if constraints.len() == 2 || keep {
490 for (k, position, ..) in &constraints {
491 shared[*k][*position] = true;
492 }
493 }
494 let on_border = i == 0 || j == 0 || i == nu - 1 || j == nv - 1;
495 let cp = original[at(i, j)];
496 let p = cp.dehomogenized()?;
497 let moved = if on_border {
498 for (k, _, plane, ..) in &constraints {
501 if !plane.signed_distance(&p).could_be_equal(Scalar::ZERO) {
502 return Err(GeopError::new(format!(
503 "NurbSurface::tangent_to: the side meeting side {k} at its corner leaves the face's tangent plane there, at {p:?}: the patch cannot be tangent to the face and keep that side"
504 )));
505 }
506 }
507 p
508 } else if keep {
509 p
510 } else {
511 match constraints.as_slice() {
512 [(_, _, _, plane, ..)] => plane.project(&p),
513 [(_, _, _, a, ..), (_, _, _, b, ..)] => {
514 if a.normal
515 .prod_cross(&b.normal)
516 .norm_sq()
517 .could_be_equal(Scalar::ZERO)
518 {
519 a.project(&p)
521 } else {
522 a.intersect_plane(b)?.project(&p)
523 }
524 }
525 _ => {
526 return Err(GeopError::new(format!(
527 "NurbSurface::tangent_to: control point ({i}, {j}) is in the rows of {} sides",
528 constraints.len()
529 )));
530 }
531 }
532 };
533 for (k, _, _, _, away, base) in &constraints {
534 if !moved
535 .sub(base)
536 .prod_dot(away)
537 .definitely_greater(Scalar::ZERO)
538 {
539 return Err(GeopError::new(format!(
540 "NurbSurface::tangent_to: at control point ({i}, {j}) the patch does not leave side {k} away from the face it continues: it would fold back over it"
541 )));
542 }
543 }
544 if !on_border && !keep {
545 surface.control_points[at(i, j)] = moved.homogeneous(cp[3]);
546 }
547 }
548 }
549 for (k, side) in sides.iter().enumerate() {
550 if side.as_ref().is_some_and(|s| !s.flat()) {
551 surface.lean(k, &planes[k], &shared[k])?;
552 }
553 }
554 surface.recompute_aabb();
555 Ok(surface)
556 }
557
558 fn evaluate_corner(&self, rows: &SideRows) -> GeopResult<Vector3> {
560 self.control_points[rows.index(0).0].dehomogenized()
561 }
562
563 fn lean(&mut self, k: usize, planes: &[(Vector3, Vector3)], shared: &[bool]) -> GeopResult<()> {
576 let rows = SideRows::of(self, k);
577 let (degree, knots) = rows.knots(self);
578 let len = rows.len();
579 let abscissae = rows.abscissae(self)?;
580 let free: Vec<usize> = (1..len - 1).filter(|&i| !shared[i]).collect();
581 if free.is_empty() {
582 return Ok(());
583 }
584 let point = |i: usize, row: usize| -> GeopResult<(Vector3, Scalar)> {
585 let cp = self.control_points[if row == 0 {
586 rows.index(i).0
587 } else {
588 rows.index(i).1
589 }];
590 Ok((cp.dehomogenized()?, cp[3]))
591 };
592 let mut a = Vec::with_capacity(free.len());
593 let mut rhs = Vec::with_capacity(free.len());
594 for &j in &free {
595 let s = abscissae[j];
596 let (u, v) = rows.uv(self, s);
597 let on_side = self.evaluate(u, v)?;
598 let normal = planes[j].0;
599 let span = find_span(degree, knots, len - 1, s)?;
600 let funs = basis_funs(span, s, degree, knots);
601 let mut into = Vector3::zero();
602 for (f, &n) in funs.iter().enumerate() {
603 let i = span - degree + f;
604 let ((p0, w0), (p1, w1)) = (point(i, 0)?, point(i, 1)?);
605 let term = p1
606 .sub(&on_side)
607 .prod_scalar(w1)
608 .sub(&p0.sub(&on_side).prod_scalar(w0));
609 into = into.add(&term.prod_scalar(n));
610 }
611 let mut row = vec![Scalar::ZERO; free.len()];
612 for (f, &n) in funs.iter().enumerate() {
613 let i = span - degree + f;
614 if let Some(c) = free.iter().position(|&x| x == i) {
615 row[c] = n.mul(point(i, 1)?.1).mul(planes[i].0.prod_dot(&normal));
616 }
617 }
618 a.push(row);
619 let target = Scalar::from_f64(TILT).mul(into.norm());
620 rhs.push(Vector::from_array([target.sub(into.prod_dot(&normal))]));
621 }
622 let moves = solve_pivoted(a, rhs).map_err(|e| {
623 e.with_context(format!(
624 "NurbSurface::tangent_to: leaning the row next to side {k} off its curved neighbour"
625 ))
626 })?;
627 let moved = (free.iter().zip(&moves))
628 .map(|(&i, by)| {
629 let (p, w) = point(i, 1)?;
630 Ok((
631 i,
632 p.add(&planes[i].0.prod_scalar(by[0]))
633 .sharpen()
634 .homogeneous(w),
635 ))
636 })
637 .collect::<GeopResult<Vec<_>>>()?;
638 for (i, cp) in moved {
639 self.control_points[rows.index(i).1] = cp;
640 }
641 Ok(())
642 }
643
644 pub(super) fn strays(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Vec<Stray>> {
670 let mut strays = Vec::new();
671 for (k, side) in sides.iter().enumerate() {
672 let Some(side) = side.as_ref().filter(|s| s.tilted()) else {
673 continue;
674 };
675 let rows = SideRows::of(self, k);
676 let (degree, knots) = rows.knots(self);
677 let breaks = breakpoints(knots, degree, rows.len());
678 let ends = (breaks[0], breaks[breaks.len() - 1]);
679 let mut seed = None;
680 for pair in breaks.windows(2) {
681 let mut stack = vec![(pair[0], pair[1], 0usize)];
682 while let Some((lo, hi, depth)) = stack.pop() {
683 let why = match self.leaves(&rows, side, (lo, hi), ends, &mut seed)? {
684 Leaving::Proven => continue,
685 Leaving::Unsure(_) if depth < MOST_HALVINGS => {
686 let mid = lo.add(hi).div(Scalar::TWO)?.sharpen();
687 stack.push((mid, hi, depth + 1));
688 stack.push((lo, mid, depth + 1));
689 continue;
690 }
691 Leaving::Unsure(why) => {
692 format!("cannot be shown to leave it as it should: {why}")
693 }
694 Leaving::Strays(why) => why,
695 };
696 strays.push(Stray {
697 at: lo.add(hi).div(Scalar::TWO)?,
698 what: format!("along side {k} over [{lo:?}, {hi:?}] {why}"),
699 });
700 break;
701 }
702 }
703 }
704 Ok(strays)
705 }
706
707 pub(super) fn misses(&self, sides: &[Option<Neighbour<'_>>; SIDES]) -> GeopResult<Vec<Stray>> {
714 let tilt = Scalar::from_f64(2.0 * TILT);
715 let mut seen = Vec::new();
716 for (k, side) in sides.iter().enumerate() {
717 let Some(side) = side.as_ref().filter(|s| s.tilted()) else {
718 continue;
719 };
720 let rows = SideRows::of(self, k);
721 let abscissae = rows.abscissae(self)?;
722 for pair in abscissae.windows(2) {
723 for s in [pair[0], pair[0].add(pair[1]).div(Scalar::TWO)?.sharpen()] {
724 let (u, v) = rows.uv(self, s);
725 let at = rows.jet(&self.partials(u, v, 2)?)?;
726 let (normal, away) = side.at(&at.point, &rows.round(&at.along))?;
727 let angle = at.into.prod_dot(&normal).div(at.into.norm())?;
728 let away = at.into.prod_dot(&away).definitely_greater(Scalar::ZERO);
729 let corner = s.is_exactly(abscissae[0]);
730 let leaves_as_it_should = away
731 && angle.definitely_greater(Scalar::ZERO)
732 && angle.definitely_less(tilt);
733 if !(corner || leaves_as_it_should) {
734 seen.push(Stray {
735 at: s,
736 what: format!(
737 "along side {k} at {s:?}, at {:?}, it leaves the face at {angle:?} radians towards its normal{}",
738 at.point,
739 if away { "" } else { ", not away from it" },
740 ),
741 });
742 }
743 }
744 }
745 }
746 if seen.is_empty() {
747 return self.strays(sides);
748 }
749 Ok(seen)
750 }
751
752 fn leaves(
757 &self,
758 rows: &SideRows,
759 side: &Neighbour<'_>,
760 (lo, hi): (Scalar, Scalar),
761 ends: (Scalar, Scalar),
762 seed: &mut Option<(Scalar, Scalar)>,
763 ) -> GeopResult<Leaving> {
764 let stretch = lo.union(hi);
765 let jet = |s: Scalar| -> GeopResult<Jet> {
766 let (u, v) = rows.uv(self, s);
767 rows.jet(&self.partials(u, v, 2)?)
768 };
769 let over = jet(stretch)?;
770 let near = match *seed {
771 Some(near) => near,
772 None => side.foot(&jet(lo.add(hi).div(Scalar::TWO)?.sharpen())?.point)?,
773 };
774 let unsure = |point: &Vector3| {
775 Leaving::Unsure(format!(
776 "no foot point on the face proven for the side at {point:?}"
777 ))
778 };
779 let Some(feet) = side.carried.enclose_foot(&over.point, near)? else {
780 return Ok(unsure(&over.point));
781 };
782 let middle = (feet.0.midpoint(), feet.1.midpoint());
783 *seed = Some(middle);
784 let (n, turn) = side.carried.normal_turning(feet.0, feet.1, &over.along)?;
785 let (centre, rising) = if lo.is_exactly(ends.0) {
788 (lo, Some(true))
789 } else if hi.is_exactly(ends.1) {
790 (hi, Some(false))
791 } else {
792 (lo.add(hi).div(Scalar::TWO)?.sharpen(), None)
793 };
794 let at = jet(centre)?;
795 let Some(foot) = side.carried.enclose_foot(&at.point, middle)? else {
796 return Ok(unsure(&at.point));
797 };
798 let (n_at, _) = side.carried.normal_turning(foot.0, foot.1, &at.along)?;
799 let f_at = at.into.prod_dot(&n_at);
800 let slope = over.turn.prod_dot(&n).add(over.into.prod_dot(&turn));
801 let f = f_at.add(slope.mul(stretch.sub(centre)));
802 let tilt = Scalar::from_f64(2.0 * TILT);
803 let within = tilt.mul(over.into.norm()).sub(f);
805 let away = over.into.prod_dot(&side.away(&n, &rows.round(&over.along)));
806 let away_at = at.into.prod_dot(&side.away(&n_at, &rows.round(&at.along)));
807 if f_at.definitely_less(Scalar::ZERO)
808 || tilt
809 .mul(at.into.norm())
810 .sub(f_at)
811 .definitely_less(Scalar::ZERO)
812 || away_at.definitely_less(Scalar::ZERO)
813 {
814 return Ok(Leaving::Strays(format!(
815 "at {centre:?}, at {:?}, it leaves the face at {:?} radians towards its normal, where it should at more than nothing and less than {tilt:?}{}",
816 at.point,
817 f_at.div(at.into.norm())?,
818 if away_at.definitely_less(Scalar::ZERO) {
819 ", and not away from it"
820 } else {
821 ""
822 }
823 )));
824 }
825 let positive = match rising {
826 Some(rising) if !f_at.definitely_greater(Scalar::ZERO) => {
827 if rising {
828 slope.definitely_greater(Scalar::ZERO)
829 } else {
830 slope.definitely_less(Scalar::ZERO)
831 }
832 }
833 _ => f.definitely_greater(Scalar::ZERO),
834 };
835 if positive
836 && within.definitely_greater(Scalar::ZERO)
837 && away.definitely_greater(Scalar::ZERO)
838 {
839 Ok(Leaving::Proven)
840 } else {
841 Ok(Leaving::Unsure(format!(
842 "over it the patch leaves the face at {:?} radians towards its normal, turning at {slope:?}",
843 f.div(over.into.norm())?
844 )))
845 }
846 }
847}
848
849enum Leaving {
851 Proven,
853 Strays(String),
855 Unsure(String),
857}
858
859struct Jet {
863 point: Vector3,
864 along: Vector3,
865 into: Vector3,
866 turn: Vector3,
867}
868
869const MOST_HALVINGS: usize = 14;
873
874pub(super) struct Stray {
877 pub at: Scalar,
879 pub what: String,
881}
882
883pub const TILT: f64 = 1e-6;
886
887pub(super) const MOST_REFINEMENTS: usize = 6;
891
892#[derive(Clone, Debug)]
896pub struct Neighbour<'a> {
897 surface: &'a NurbSurface3D,
898 carried: Arc<NurbSurface3D>,
902 along: bool,
903 plane: Option<Plane>,
904 joined: bool,
907}
908
909impl<'a> Neighbour<'a> {
910 pub fn new(surface: &'a NurbSurface3D, along: bool) -> GeopResult<Self> {
915 let quarter = |(lo, hi): (Scalar, Scalar)| {
918 Scalar::from_f64(2f64.powi((hi.sub(lo).to_f64() / 4.0).log2().floor() as i32))
919 };
920 let mut carried = surface.clone();
921 for along_u in [true, false] {
922 let by = quarter(if along_u {
923 surface.domain_u()
924 } else {
925 surface.domain_v()
926 });
927 for at_end in [false, true] {
928 carried = carried.extended(along_u, at_end, by)?;
929 }
930 }
931 Ok(Self {
932 surface,
933 carried: Arc::new(carried),
934 along,
935 plane: surface.as_plane()?,
936 joined: false,
937 })
938 }
939
940 pub fn joined(surface: &'a NurbSurface3D, along: bool) -> GeopResult<Self> {
948 Ok(Self {
949 joined: true,
950 ..Self::new(surface, along)?
951 })
952 }
953
954 pub(super) fn flat(&self) -> bool {
957 self.plane.is_some()
958 }
959
960 pub(super) fn tilted(&self) -> bool {
963 !self.flat() && !self.joined
964 }
965
966 pub(super) fn at(&self, point: &Vector3, along: &Vector3) -> GeopResult<(Vector3, Vector3)> {
970 let normal = match &self.plane {
971 Some(plane) => plane.normal,
972 None => {
973 let (u, v) = self.foot(point)?;
974 self.surface.normal(u, v)?
975 }
976 };
977 Ok((normal, self.away(&normal, along)))
978 }
979
980 pub(super) fn turning(
983 &self,
984 point: &Vector3,
985 along: &Vector3,
986 ) -> GeopResult<(Vector3, Vector3)> {
987 match &self.plane {
988 Some(plane) => Ok((plane.normal, Vector3::zero())),
989 None => {
990 let (u, v) = self.foot(point)?;
991 self.surface.normal_turning(u, v, along)
992 }
993 }
994 }
995
996 pub(super) fn away(&self, normal: &Vector3, along: &Vector3) -> Vector3 {
1000 if self.along {
1001 along.prod_cross(normal)
1002 } else {
1003 normal.prod_cross(along)
1004 }
1005 }
1006
1007 fn foot(&self, point: &Vector3) -> GeopResult<(Scalar, Scalar)> {
1009 self.surface
1010 .locate(point, MAX_NODES, min_subdivision_size())?
1011 .ok_or_else(|| {
1012 GeopError::new(format!(
1013 "Neighbour: the side does not lie on the face's surface at {point:?}"
1014 ))
1015 })
1016 }
1017}
1018
1019struct SideRows {
1022 k: usize,
1023 nu: usize,
1024 nv: usize,
1025}
1026
1027impl SideRows {
1028 fn of(surface: &NurbSurface3D, k: usize) -> Self {
1029 Self {
1030 k,
1031 nu: surface.num_u,
1032 nv: surface.num_v,
1033 }
1034 }
1035
1036 fn along_u(&self) -> bool {
1038 self.k.is_multiple_of(2)
1039 }
1040
1041 fn len(&self) -> usize {
1043 if self.along_u() { self.nu } else { self.nv }
1044 }
1045
1046 fn index(&self, i: usize) -> (usize, usize) {
1049 let (nu, nv) = (self.nu, self.nv);
1050 let at = |a: usize, b: usize| a * nv + b;
1051 match self.k {
1052 0 => (at(i, 0), at(i, 1)),
1053 1 => (at(nu - 1, i), at(nu - 2, i)),
1054 2 => (at(i, nv - 1), at(i, nv - 2)),
1055 _ => (at(0, i), at(1, i)),
1056 }
1057 }
1058
1059 fn knots<'s>(&self, surface: &'s NurbSurface3D) -> (usize, &'s [Scalar]) {
1061 if self.along_u() {
1062 (surface.degree_u, &surface.knot_vector_u)
1063 } else {
1064 (surface.degree_v, &surface.knot_vector_v)
1065 }
1066 }
1067
1068 fn abscissae(&self, surface: &NurbSurface3D) -> GeopResult<Vec<Scalar>> {
1070 let (degree, knots) = self.knots(surface);
1071 greville(knots, degree, self.len())
1072 }
1073
1074 fn jet(&self, d: &[Vec<Vector3>]) -> GeopResult<Jet> {
1077 let (su, sv, suv) = (d[1][0], d[0][1], d[1][1]);
1078 let (along, into, turn) = match self.k {
1079 0 => (su, sv, suv),
1080 1 => (sv, su.neg(), suv.neg()),
1081 2 => (su, sv.neg(), suv.neg()),
1082 _ => (sv, su, suv),
1083 };
1084 Ok(Jet {
1085 point: d[0][0],
1086 along,
1087 into,
1088 turn,
1089 })
1090 }
1091
1092 fn round(&self, along: &Vector3) -> Vector3 {
1095 if self.k < 2 { *along } else { along.neg() }
1096 }
1097
1098 fn point_along(&self, surface: &NurbSurface3D, s: Scalar) -> GeopResult<(Vector3, Vector3)> {
1101 let (u, v) = self.uv(surface, s);
1102 let (du, dv) = surface.derivatives(u, v)?;
1103 let along = match self.k {
1104 0 => du,
1105 1 => dv,
1106 2 => du.neg(),
1107 _ => dv.neg(),
1108 };
1109 Ok((surface.evaluate(u, v)?, along))
1110 }
1111
1112 fn uv(&self, surface: &NurbSurface3D, s: Scalar) -> (Scalar, Scalar) {
1114 let ((u0, u1), (v0, v1)) = (surface.domain_u(), surface.domain_v());
1115 match self.k {
1116 0 => (s, v0),
1117 1 => (u1, s),
1118 2 => (s, v1),
1119 _ => (u0, s),
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use crate::test_fixtures::{line, weighted};
1127 use geop_core_math::{scalars::Scalar, vector::Vector3};
1128
1129 use crate::{
1130 nurb_curve::{NurbCurve, NurbCurve3D},
1131 nurb_surface::{Neighbour, NurbSurface3D, TILT},
1132 };
1133
1134 fn v(x: f64, y: f64, z: f64) -> Vector3 {
1135 Vector3::from_array([x, y, z].map(Scalar::from_f64))
1136 }
1137
1138 fn rising_arc(x0: f64) -> NurbCurve3D {
1142 let f = Scalar::from_f64;
1143 NurbCurve::try_new(
1144 2,
1145 vec![
1146 weighted(x0, 0., 0., 1.),
1147 weighted(x0, 1., 0., std::f64::consts::SQRT_2 / 2.0),
1148 weighted(x0, 1., 1., 1.),
1149 ],
1150 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
1151 )
1152 .unwrap()
1153 }
1154
1155 fn wavy(lift: f64) -> NurbCurve3D {
1158 let f = Scalar::from_f64;
1159 NurbCurve::try_new(
1160 3,
1161 vec![
1162 weighted(0., 0., 0., 1.),
1163 weighted(0.3, -0.2, 0.4 * lift, 1.),
1164 weighted(0.6, 0.3, -0.2 * lift, 1.),
1165 weighted(0.8, -0.1, 0.3 * lift, 1.),
1166 weighted(1., 0., 0., 1.),
1167 ],
1168 vec![
1169 f(0.),
1170 f(0.),
1171 f(0.),
1172 f(0.),
1173 f(0.5),
1174 f(1.),
1175 f(1.),
1176 f(1.),
1177 f(1.),
1178 ],
1179 )
1180 .unwrap()
1181 }
1182
1183 fn assert_on(surface: &NurbSurface3D, curve: &NurbCurve3D, side: usize) {
1184 for k in 0..=8 {
1185 let s = Scalar::from_ratio(k, 8).unwrap();
1186 let (u, v) = match side {
1187 0 => (s, Scalar::ZERO),
1188 1 => (Scalar::ONE, s),
1189 2 => (Scalar::ONE.sub(s), Scalar::ONE),
1190 _ => (Scalar::ZERO, Scalar::ONE.sub(s)),
1191 };
1192 let on_surface = surface.evaluate(u, v).unwrap();
1193 let unit = curve.with_unit_domain().unwrap();
1194 let on_curve = unit.evaluate(s).unwrap();
1195 assert!(
1196 on_surface.could_be_equal(&on_curve),
1197 "side {side} at {s:?}: {on_surface:?} vs {on_curve:?}"
1198 );
1199 }
1200 }
1201
1202 #[test]
1206 fn coons_interpolates_its_sides() {
1207 let sides = [
1208 wavy(1.0),
1209 line([1., 0., 0.], [1., 1., 1.]),
1210 line([1., 1., 1.], [0., 1., 1.]),
1211 rising_arc(0.).reverse(),
1212 ];
1213 let surface = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1214 let nv = surface.num_v;
1216 for (i, cp) in wavy(1.0).control_points.iter().enumerate() {
1217 let row = surface.control_points[i * nv];
1218 assert!(row.could_be_equal(cp), "{i}: {row:?} vs {cp:?}");
1219 }
1220 for (k, side) in sides.iter().enumerate() {
1223 assert_on(&surface, side, k);
1224 }
1225 }
1226
1227 #[test]
1229 fn coons_of_a_square_is_flat() {
1230 let c = [[0., 0., 0.], [2., 0., 0.], [2., 1., 0.], [0., 1., 0.]];
1231 let sides: Vec<_> = (0..4).map(|k| line(c[k], c[(k + 1) % 4])).collect();
1232 let surface = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1233 let plane = surface.as_plane().unwrap().expect("flat");
1234 assert!(plane.normal.could_be_equal(&v(0., 0., 1.)), "{plane:?}");
1235 }
1236
1237 #[test]
1239 fn coons_needs_a_closed_loop() {
1240 let a = line([0., 0., 0.], [1., 0., 0.]);
1241 let b = line([1., 0., 0.], [1., 1., 0.]);
1242 let c = line([1., 1., 0.], [0., 1., 0.]);
1243 let d = line([0., 1.5, 0.], [0., 0., 0.]);
1244 assert!(NurbSurface3D::coons([&a, &b, &c, &d]).is_err());
1245 }
1246
1247 #[test]
1249 fn ruled_runs_along_both() {
1250 let a = wavy(1.0);
1251 let b = rising_arc(2.0);
1252 let surface = NurbSurface3D::ruled(&a, &b).unwrap();
1253 for k in 0..=8 {
1254 let s = Scalar::from_ratio(k, 8).unwrap();
1255 let pa = a.evaluate(s).unwrap();
1256 let pb = b.evaluate(s).unwrap();
1257 assert!(
1258 surface
1259 .evaluate(s, Scalar::ZERO)
1260 .unwrap()
1261 .could_be_equal(&pa)
1262 );
1263 assert!(
1264 surface
1265 .evaluate(s, Scalar::ONE)
1266 .unwrap()
1267 .could_be_equal(&pb)
1268 );
1269 }
1270 }
1271
1272 #[test]
1277 fn tangent_to_a_plane() {
1278 let top = {
1281 let f = Scalar::from_f64;
1282 NurbCurve::try_new(
1283 3,
1284 vec![
1285 weighted(0., 1., 1., 1.),
1286 weighted(0.3, 1., 1.5, 1.),
1287 weighted(0.6, 1., 0.6, 1.),
1288 weighted(0.8, 1., 1.3, 1.),
1289 weighted(1., 1., 1., 1.),
1290 ],
1291 [0., 0., 0., 0., 0.5, 1., 1., 1., 1.].map(f).to_vec(),
1292 )
1293 .unwrap()
1294 .reverse()
1295 };
1296 let sides = [wavy(0.0), rising_arc(1.), top, rising_arc(0.).reverse()];
1297 let coons = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1298 let bends = (0..=8).any(|k| {
1299 let u = Scalar::from_ratio(k, 8).unwrap();
1300 let n = coons.normal(u, Scalar::ZERO).unwrap();
1301 !n.prod_cross(&v(0., 0., 1.))
1302 .norm_sq()
1303 .could_be_equal(Scalar::ZERO)
1304 });
1305 assert!(bends, "the Coons patch alone is not tangent to the plane");
1306 let floor = NurbSurface3D::bilinear(
1307 v(-1., -1., 0.),
1308 v(2., -1., 0.),
1309 v(2., 2., 0.),
1310 v(-1., 2., 0.),
1311 )
1312 .unwrap();
1313 let flat = || Some(Neighbour::new(&floor, false).unwrap());
1316 let surface = coons.tangent_to(&[flat(), None, None, None]).unwrap();
1317 for (k, side) in sides.iter().enumerate() {
1318 assert_on(&surface, side, k);
1319 }
1320 for k in 0..=8 {
1321 let u = Scalar::from_ratio(k, 8).unwrap();
1322 let n = surface.normal(u, Scalar::ZERO).unwrap();
1323 assert!(
1324 n.prod_cross(&v(0., 0., 1.))
1325 .norm_sq()
1326 .could_be_equal(Scalar::ZERO),
1327 "at {u:?}: {n:?}"
1328 );
1329 }
1330 let steep = [
1332 line([0., 0., 0.], [1., 0., 0.]),
1333 line([1., 0., 0.], [1., 1., 1.]),
1334 line([1., 1., 1.], [0., 1., 1.]),
1335 line([0., 1., 1.], [0., 0., 0.]),
1336 ];
1337 let coons = NurbSurface3D::coons([&steep[0], &steep[1], &steep[2], &steep[3]]).unwrap();
1338 assert!(coons.tangent_to(&[flat(), None, None, None]).is_err());
1339 }
1340
1341 #[test]
1344 fn bilinear_has_its_corners() {
1345 let corners = [
1346 v(0.3, -1.2, 2.5),
1347 v(4.1, 0.7, -0.3),
1348 v(2.2, 3.3, 1.1),
1349 v(-1.5, 2.0, 0.6),
1350 ];
1351 let surface =
1352 NurbSurface3D::bilinear(corners[0], corners[1], corners[2], corners[3]).unwrap();
1353 for (p, (u, w)) in corners.iter().zip([(0, 0), (1, 0), (1, 1), (0, 1)]) {
1354 let at = surface
1355 .evaluate(Scalar::from_i64(u), Scalar::from_i64(w))
1356 .unwrap();
1357 assert!(at.could_be_equal(p), "at ({u}, {w}): {at:?}, not {p:?}");
1358 }
1359 }
1360
1361 #[test]
1367 fn leaving_a_cylinder_is_proven() {
1368 let cylinder = NurbSurface3D::coons([
1369 &line([0., 0., 0.], [1., 0., 0.]),
1370 &rising_arc(1.),
1371 &line([1., 1., 1.], [0., 1., 1.]),
1372 &rising_arc(0.).reverse(),
1373 ])
1374 .unwrap();
1375 let sides = [
1376 rising_arc(1.),
1377 line([1., 1., 1.], [2., 1., 1.]),
1378 line([2., 1., 1.], [2., 0., 0.]),
1379 line([2., 0., 0.], [1., 0., 0.]),
1380 ];
1381 let plain = NurbSurface3D::coons([&sides[0], &sides[1], &sides[2], &sides[3]]).unwrap();
1382 let neighbours = [
1384 Some(Neighbour::new(&cylinder, true).unwrap()),
1385 None,
1386 None,
1387 None,
1388 ];
1389 let strays = plain.strays(&neighbours).unwrap();
1390 assert!(!strays.is_empty(), "the Coons patch is not tangent");
1391 let tangent = plain.tangent_to(&neighbours).unwrap();
1392 let strays = tangent.strays(&neighbours).unwrap();
1393 assert!(
1394 strays.is_empty(),
1395 "{:?}",
1396 strays.iter().map(|s| &s.what).collect::<Vec<_>>()
1397 );
1398 for i in 1..64 {
1400 let u = Scalar::from_ratio(i, 64).unwrap();
1401 let p = tangent.evaluate(u, Scalar::ZERO).unwrap();
1402 let out = v(0., p[1].to_f64(), p[2].to_f64() - 1.)
1403 .normalize()
1404 .unwrap();
1405 let (_, into) = tangent.derivatives(u, Scalar::ZERO).unwrap();
1406 let angle = into.prod_dot(&out).neg().div(into.norm()).unwrap().to_f64();
1407 assert!(angle > 0. && angle < 2. * TILT, "at {u:?}: {angle:e}");
1408 }
1409 }
1410}