1use geop_core_math::{
2 combinatorics::binomial,
3 geop_error::{GeopError, GeopResult, WithContext},
4 scalars::Scalar,
5 vector::{Vector, Vector3},
6};
7
8use super::{NurbSurface, evaluate::corner_nearest};
9use crate::{
10 knot_insertion::is_clamped_at,
11 nurb_curve::dehomogenize,
12 spline::{
13 centered, de_boor, find_span, find_spans, hodograph, homogeneous_derivatives,
14 rational_derivatives,
15 },
16};
17
18impl NurbSurface<4> {
19 #[allow(clippy::type_complexity)]
28 fn homogeneous_partials(
29 &self,
30 u: Scalar,
31 v: Scalar,
32 n: usize,
33 ) -> GeopResult<(Vec<Vector<4>>, Vec<Vector<4>>, Option<Vector<4>>)> {
34 let (p, q) = (self.degree_u, self.degree_v);
35 let (ku, kv) = (&self.knot_vector_u, &self.knot_vector_v);
36 let nv = self.num_v;
37 let span_u = find_span(p, ku, self.num_u - 1, u)?;
38 let span_v = find_span(q, kv, nv - 1, v)?;
39 let local: Vec<Vector<4>> = (span_u - p..=span_u)
43 .flat_map(|i| (span_v - q..=span_v).map(move |j| i * nv + j))
44 .map(|k| self.control_points[k])
45 .collect();
46 let at = corner_nearest(self, (span_u, u), (span_v, v));
47 let (cp, _) = centered(&local, at);
48 let at = |i: usize, j: usize| cp[i * (q + 1) + j];
49 let mixed = n >= 2;
50 let (rows, rows_v): (Vec<Vector<4>>, Vec<Vector<4>>) = (0..=p)
52 .map(|i| {
53 let local: Vec<_> = (0..=q).map(|j| at(i, j)).collect();
54 let d = homogeneous_derivatives(q, kv, &local, span_v, v, usize::from(mixed));
55 (d[0], d.get(1).copied().unwrap_or_else(Vector::zero))
56 })
57 .unzip();
58 let cols: Vec<Vector<4>> = (0..=q)
59 .map(|j| {
60 let local: Vec<_> = (0..=p).map(|i| at(i, j)).collect();
61 homogeneous_derivatives(p, ku, &local, span_u, u, 0)[0]
62 })
63 .collect();
64 Ok((
65 homogeneous_derivatives(p, ku, &rows, span_u, u, n),
66 homogeneous_derivatives(q, kv, &cols, span_v, v, n),
67 mixed.then(|| homogeneous_derivatives(p, ku, &rows_v, span_u, u, 1)[1]),
68 ))
69 }
70
71 pub fn partials(&self, u: Scalar, v: Scalar, order: usize) -> GeopResult<Vec<Vec<Vector3>>> {
93 let (p, q) = (self.degree_u, self.degree_v);
94 let us = find_spans(p, &self.knot_vector_u, self.num_u - 1, u)?;
95 let vs = find_spans(q, &self.knot_vector_v, self.num_v - 1, v)?;
96 let mut out: Option<Vec<Vec<Vector3>>> = None;
97 for &u in &us {
98 for &v in &vs {
99 let piece = self.partials_in(u, v, order)?;
100 out = Some(match out {
101 None => piece,
102 Some(all) => (all.iter().zip(&piece))
103 .map(|(a, b)| a.iter().zip(b).map(|(a, b)| a.union(b)).collect())
104 .collect(),
105 });
106 }
107 }
108 Ok(out.expect("a span at least"))
109 }
110
111 fn partials_in(
114 &self,
115 (span_u, u): (usize, Scalar),
116 (span_v, v): (usize, Scalar),
117 order: usize,
118 ) -> GeopResult<Vec<Vec<Vector3>>> {
119 let (p, q) = (self.degree_u, self.degree_v);
120 let (ku, kv) = (&self.knot_vector_u, &self.knot_vector_v);
121 let nv = self.num_v;
122 let local: Vec<Vector<4>> = (span_u - p..=span_u)
123 .flat_map(|i| (span_v - q..=span_v).map(move |j| i * nv + j))
124 .map(|k| self.control_points[k])
125 .collect();
126 let at = corner_nearest(self, (span_u, u), (span_v, v));
127 let (cp, origin) = centered(&local, at);
128 let (base_u, base_v) = (span_u - p, span_v - q);
129 let mut along_v: Vec<Vec<Vec<Vector<4>>>> = vec![
131 (0..=p)
132 .map(|i| cp[i * (q + 1)..(i + 1) * (q + 1)].to_vec())
133 .collect(),
134 ];
135 for l in 1..=order.min(q) {
136 let previous = &along_v[l - 1];
137 along_v.push(
138 previous
139 .iter()
140 .map(|row| hodograph(q, kv, row, base_v, l))
141 .collect(),
142 );
143 }
144 let mut a = vec![vec![Vector::<4>::zero(); order + 1]; order + 1];
147 for k in 0..=order {
148 for l in 0..=order - k {
149 if k > p || l > q {
150 continue;
151 }
152 let mut net: Vec<Vec<Vector<4>>> = along_v[l].clone();
153 for kk in 1..=k {
154 let columns: Vec<Vec<Vector<4>>> = (0..=q - l)
155 .map(|j| {
156 let column: Vec<Vector<4>> = net.iter().map(|row| row[j]).collect();
157 hodograph(p, ku, &column, base_u, kk)
158 })
159 .collect();
160 net = (0..columns[0].len())
161 .map(|i| columns.iter().map(|c| c[i]).collect())
162 .collect();
163 }
164 let rows: Vec<Vector<4>> = net
165 .iter()
166 .map(|row| de_boor(q - l, &kv[base_v + l..], row, v, q - l))
167 .collect();
168 a[k][l] = de_boor(p - k, &ku[base_u + k..], &rows, u, p - k);
169 }
170 }
171 let w = |k: usize, l: usize| a[k][l][3];
172 let inverse = Scalar::ONE.div(w(0, 0))?;
173 let mut s: Vec<Vec<Vector3>> = vec![Vec::new(); order + 1];
174 for k in 0..=order {
175 for l in 0..=order - k {
176 let mut value = Vector3::from_array([a[k][l][0], a[k][l][1], a[k][l][2]]);
177 let term =
178 |f: i64, wv: Scalar, sv: &Vector3| sv.prod_scalar(Scalar::from_i64(f).mul(wv));
179 for j in 1..=l {
180 value = value.sub(&term(binomial(l, j), w(0, j), &s[k][l - j]));
181 }
182 for i in 1..=k {
183 value = value.sub(&term(binomial(k, i), w(i, 0), &s[k - i][l]));
184 for j in 1..=l {
185 value = value.sub(&term(
186 binomial(k, i) * binomial(l, j),
187 w(i, j),
188 &s[k - i][l - j],
189 ));
190 }
191 }
192 s[k].push(value.prod_scalar(inverse));
193 }
194 }
195 s[0][0] = s[0][0].add(&Vector3::from_array([origin[0], origin[1], origin[2]]));
196 Ok(s)
197 }
198
199 pub fn derivatives(&self, u: Scalar, v: Scalar) -> GeopResult<(Vector3, Vector3)> {
201 let (du, dv, _) = self.homogeneous_partials(u, v, 1)?;
202 Ok((rational_derivatives(&du)?[1], rational_derivatives(&dv)?[1]))
203 }
204
205 pub fn second_derivatives(&self, u: Scalar, v: Scalar) -> GeopResult<[Vector3; 3]> {
211 let (du, dv, a_uv) = self.homogeneous_partials(u, v, 2)?;
212 let a_uv = a_uv.expect("the second order takes in the mixed partial");
213 let (cu, cv) = (rational_derivatives(&du)?, rational_derivatives(&dv)?);
214 let (w, w_u, w_v, w_uv) = (du[0][3], du[1][3], dv[1][3], a_uv[3]);
215 let mut s_uv = Vector3::zero();
216 for c in 0..3 {
217 s_uv[c] = a_uv[c]
218 .sub(w_uv.mul(cu[0][c]))
219 .sub(w_u.mul(cv[1][c]))
220 .sub(w_v.mul(cu[1][c]))
221 .div(w)?;
222 }
223 Ok([cu[2], s_uv, cv[2]])
224 }
225
226 fn boundary_rows(&self, u_fixed: bool, first: bool) -> Option<[Vec<Vector<4>>; 2]> {
231 let (nu, nv) = (self.num_u, self.num_v);
232 let (knots, degree, len) = if u_fixed {
233 (&self.knot_vector_u, self.degree_u, nu)
234 } else {
235 (&self.knot_vector_v, self.degree_v, nv)
236 };
237 if len < 2 || !is_clamped_at(knots, degree, first) {
238 return None;
239 }
240 let (edge, next) = if first { (0, 1) } else { (len - 1, len - 2) };
241 Some([
242 self.control_row(u_fixed, edge),
243 self.control_row(u_fixed, next),
244 ])
245 }
246
247 pub(crate) fn pole(&self, u_fixed: bool, first: bool) -> Option<(Vector3, Vec<Vector<4>>)> {
252 let [edge, next] = self.boundary_rows(u_fixed, first)?;
253 let edge = dehomogenize::<4, 3>(&edge);
254 let shared = edge.iter().fold(edge[0], |shared, p| {
255 Vector3::from_array(std::array::from_fn(|k| shared[k].intersect(p[k])))
256 });
257 edge.iter()
258 .all(|p| p.could_be_equal(&shared))
259 .then_some((shared, next))
260 }
261
262 pub(crate) fn rows_touching(&self, u: Scalar, v: Scalar) -> Vec<(bool, bool)> {
264 let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
265 [
266 (false, v, v0, true),
267 (false, v, v1, false),
268 (true, u, u0, true),
269 (true, u, u1, false),
270 ]
271 .into_iter()
272 .filter(|&(_, t, end, _)| t.could_be_equal(end))
273 .map(|(u_fixed, _, _, first)| (u_fixed, first))
274 .collect()
275 }
276
277 pub fn pole_at(&self, u: Scalar, v: Scalar) -> Option<Vector3> {
280 self.rows_touching(u, v)
281 .into_iter()
282 .find_map(|(u_fixed, first)| self.pole(u_fixed, first))
283 .map(|(pole, _)| pole)
284 }
285
286 fn pole_normal(&self, u_fixed: bool, first: bool) -> Option<GeopResult<Vector3>> {
299 let (pole, next) = self.pole(u_fixed, first)?;
300 let spokes: Vec<Vector3> = dehomogenize::<4, 3>(&next)
301 .iter()
302 .map(|x| x.sub(&pole))
303 .collect();
304 let turning = spokes.windows(2).fold(Vector3::zero(), |acc: Vector3, e| {
305 acc.add(&e[0].prod_cross(&e[1]))
306 });
307 let result = turning.normalize().and_then(|t| {
308 if !spokes
309 .iter()
310 .all(|e| e.prod_dot(&t).could_be_equal(Scalar::ZERO))
311 {
312 return Err(GeopError::new(format!(
313 "the spokes {spokes:?} are not coplanar: an apex has no single normal"
314 )));
315 }
316 Ok(if u_fixed == first { t } else { t.neg() })
321 });
322 Some(result.with_context(&|e: GeopError| {
323 e.with_context(format!(
324 "NurbSurface::pole_normal(pole={pole:?}, collapsed {} row at the {})",
325 if u_fixed { "u" } else { "v" },
326 if first { "start" } else { "end" }
327 ))
328 }))
329 }
330
331 pub(crate) fn is_apex(&self, u_fixed: bool, first: bool) -> bool {
335 matches!(self.pole_normal(u_fixed, first), Some(Err(_)))
336 }
337
338 pub fn normal(&self, u: Scalar, v: Scalar) -> GeopResult<Vector3> {
357 let (du, dv) = self.derivatives(u, v)?;
358 let regular = du
359 .prod_cross(&dv)
360 .normalize()
361 .with_context(&|e: GeopError| {
362 e.with_context(format!(
363 "NurbSurface::normal({:?}): S_u = {du:?}, S_v = {dv:?}",
364 (u, v)
365 ))
366 });
367 if regular.is_ok() {
368 return regular;
369 }
370 let rows = self.rows_touching(u, v);
371 for &(u_fixed, first) in &rows {
372 if let Some(normal) = self.pole_normal(u_fixed, first) {
373 return normal;
374 }
375 }
376 for &(u_fixed, u_first) in rows.iter().filter(|(u_fixed, _)| *u_fixed) {
378 for &(_, v_first) in rows.iter().filter(|(v_fixed, _)| !*v_fixed) {
379 debug_assert!(u_fixed);
380 if let Some(normal) = self.corner_normal(u_first, v_first) {
381 return normal;
382 }
383 }
384 }
385 regular
386 }
387
388 fn corner_normal(&self, u_first: bool, v_first: bool) -> Option<GeopResult<Vector3>> {
403 let ((u0, u1), (v0, v1)) = (self.domain_u(), self.domain_v());
404 let (u, v) = (if u_first { u0 } else { u1 }, if v_first { v0 } else { v1 });
405 let (su, sv) = self.derivatives(u, v).ok()?;
406 if !su.could_be_parallel(&sv) {
407 return None;
408 }
409 let corner = || -> GeopResult<Vector3> {
410 let mu = su.prod_dot(&sv).div(su.prod_dot(&su))?;
411 let [suu, suv, svv] = self.second_derivatives(u, v)?;
412 let into = |first: bool| {
413 if first {
414 Scalar::ONE
415 } else {
416 Scalar::ONE.neg()
417 }
418 };
419 let along_u = su
420 .prod_cross(&suv.sub(&suu.prod_scalar(mu)))
421 .prod_scalar(into(u_first));
422 let along_v = su
423 .prod_cross(&svv.sub(&suv.prod_scalar(mu)))
424 .prod_scalar(into(v_first));
425 let normal = along_u.add(&along_v).normalize()?;
426 for w in [along_u, along_v] {
427 if !w.could_be_parallel(&normal)
428 || w.prod_dot(&normal).definitely_less(Scalar::ZERO)
429 {
430 return Err(GeopError::new(format!(
431 "the surface's normals next to the corner turn from {along_u:?} to {along_v:?}: a corner with no single normal"
432 )));
433 }
434 }
435 Ok(normal)
436 };
437 Some(corner().with_context(&|e: GeopError| {
438 e.with_context(format!(
439 "NurbSurface::corner_normal(corner at {:?}, S_u = {su:?}, S_v = {sv:?})",
440 (u, v)
441 ))
442 }))
443 }
444
445 pub fn normal_at(
449 &self,
450 point: &Vector3,
451 max_nodes: usize,
452 min_subdivision_size: Scalar,
453 ) -> GeopResult<Vector3> {
454 let (u, v) = self
455 .locate(point, max_nodes, min_subdivision_size)?
456 .ok_or_else(|| GeopError::new(format!("{point:?} is not on the surface")))?;
457 self.normal(u, v)
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use crate::nurb_surface::NurbSurface;
464 use crate::test_fixtures::{hom, revolved};
465 use geop_core_math::scalars::Scalar;
466
467 #[test]
472 fn partials_are_tight_over_a_box() {
473 let surface = revolved(1., 0.).refined([4, 4]).unwrap();
474 let (u, v) = (Scalar::from_f64(0.3), Scalar::from_f64(0.6));
475 let at = surface.partials(u, v, 3).unwrap();
476 let (su, sv) = surface.derivatives(u, v).unwrap();
477 let [suu, suv, svv] = surface.second_derivatives(u, v).unwrap();
478 for (got, expected) in [
479 (at[1][0], su),
480 (at[0][1], sv),
481 (at[2][0], suu),
482 (at[1][1], suv),
483 (at[0][2], svv),
484 ] {
485 assert!(got.could_be_equal(&expected), "{got:?} vs {expected:?}");
486 }
487 assert!(at[0][0].could_be_equal(&surface.evaluate(u, v).unwrap()));
488 let h = 1e-3;
491 let over = surface
492 .partials(
493 u.add(Scalar::new(-h / 2., h / 2.)),
494 v.add(Scalar::new(-h / 2., h / 2.)),
495 3,
496 )
497 .unwrap();
498 let mut table = String::new();
499 let mut worst: f64 = 0.;
500 for k in 0..=3 {
501 for l in 0..=3 - k {
502 let width = (0..3)
503 .map(|c| over[k][l][c].width().to_f64())
504 .fold(0., f64::max);
505 let size = (0..3)
506 .map(|c| at[k][l][c].abs().to_f64())
507 .fold(0., f64::max);
508 table += &format!("S[{k}][{l}]: {size:.3e} wide {width:.3e}\n");
509 worst = worst.max(width / h);
510 }
511 }
512 assert!(worst < 50., "{table}");
516 }
517
518 #[test]
524 fn partials_over_a_knot_enclose_both_spans() {
525 let surface = revolved(1., 0.).refined([4, 1]).unwrap();
526 let mut surface = surface.elevate_degree(true).unwrap();
528 let nv = surface.num_v;
529 let mid = surface.degree_u;
530 for j in 0..nv {
531 let cp = &mut surface.control_points[mid * nv + j];
532 cp[2] = cp[2].add(cp[3].mul(Scalar::from_f64(0.3)));
533 }
534 let knot = surface.knot_vector_u[surface.degree_u + 1];
535 let box_u = knot
536 .sub(Scalar::from_f64(0.05))
537 .union(knot.add(Scalar::from_f64(0.05)));
538 let v = Scalar::from_f64(0.4);
539 let over = surface.partials(box_u, v, 2).unwrap();
540 for i in 0..=10 {
541 let u = Scalar::interpolate(
542 box_u.lower(),
543 box_u.upper(),
544 Scalar::from_ratio(i, 10).unwrap(),
545 );
546 let at = surface.partials(u, v, 2).unwrap();
547 for (k, l) in [(0, 0), (1, 0), (0, 1), (2, 0), (1, 1), (0, 2)] {
548 assert!(
549 at[k][l].could_be_equal(&over[k][l]),
550 "S[{k}][{l}] at {u:?}: {:?}, over the box {:?}",
551 at[k][l],
552 over[k][l]
553 );
554 }
555 }
556 }
557
558 fn flat_xy() -> NurbSurface<4> {
560 let f = Scalar::from_f64;
561 NurbSurface::try_new(
562 1,
563 1,
564 vec![
565 hom(0., 0., 0., 1.),
566 hom(0., 1., 0., 1.),
567 hom(1., 0., 0., 1.),
568 hom(1., 1., 0., 1.),
569 ],
570 vec![f(0.), f(0.), f(1.), f(1.)],
571 vec![f(0.), f(0.), f(1.), f(1.)],
572 )
573 .unwrap()
574 }
575
576 #[test]
577 fn flat_surface_normal_is_constant_z() {
578 let s = flat_xy();
579 for &(u, v) in &[(0.0, 0.0), (0.5, 0.5), (1.0, 0.0), (0.2, 0.8)] {
580 let n = s.normal(Scalar::from_f64(u), Scalar::from_f64(v)).unwrap();
581 assert!(n[0].could_be_equal(Scalar::ZERO));
582 assert!(n[1].could_be_equal(Scalar::ZERO));
583 assert!(n[2].could_be_equal(Scalar::ONE));
584 }
585 }
586
587 fn bent_surface() -> NurbSurface<4> {
591 let f = Scalar::from_f64;
592 NurbSurface::try_new(
593 1,
594 1,
595 vec![
596 hom(0., 0., 0., 1.),
597 hom(0., 2., 1., 1.),
598 hom(2., 0., 1., 1.),
599 hom(2., 2., 0., 1.),
600 ],
601 vec![f(0.), f(0.), f(1.), f(1.)],
602 vec![f(0.), f(0.), f(1.), f(1.)],
603 )
604 .unwrap()
605 }
606
607 #[test]
608 fn bent_surface_center_normal_is_z() {
609 let s = bent_surface();
610 let n = s
611 .normal(Scalar::from_f64(0.5), Scalar::from_f64(0.5))
612 .unwrap();
613 assert!(n[0].could_be_equal(Scalar::ZERO));
614 assert!(n[1].could_be_equal(Scalar::ZERO));
615 assert!(n[2].definitely_greater(Scalar::ZERO));
616 }
617
618 #[test]
628 fn disc_normal_a_hair_off_the_pole() {
629 let f = Scalar::from_f64;
630 let disc = revolved(2., 2.);
631 for (u, v) in [(0., 5.4e-14), (1., 5.4e-14), (0.3, 1e-10)] {
632 let n = disc.normal(f(u), f(v)).unwrap();
633 let (su, sv) = disc.derivatives(f(u), f(v)).unwrap();
634 assert!(
635 n[2].abs().could_be_equal(Scalar::ONE) && n[0].width().to_f64() < 0.1,
636 "at {:?}: {n:?}, S_u = {su:?}, S_v = {sv:?}",
637 (u, v)
638 );
639 }
640 }
641
642 #[test]
645 fn disc_centre_normal() {
646 let f = Scalar::from_f64;
647 let disc = revolved(0., 0.);
648 let every_u = f(0.).union(f(1.));
649 let n = disc.normal(every_u, f(0.)).unwrap();
650 let off = disc.normal(f(0.5), f(0.5)).unwrap();
651 assert!(n.could_be_equal(&off), "{n:?} vs {off:?}");
652 assert!(
653 n[2].abs().could_be_equal(Scalar::ONE) && n[0].could_be_equal(Scalar::ZERO),
654 "{n:?}"
655 );
656 }
657
658 #[test]
660 fn cone_apex_has_no_normal() {
661 let f = Scalar::from_f64;
662 let cone = revolved(1., 0.);
663 assert!(cone.normal(f(0.).union(f(1.)), f(0.)).is_err());
664 assert!(cone.normal(f(0.5), f(0.5)).is_ok());
665 }
666
667 #[test]
671 fn sphere_pole_normal() {
672 let f = Scalar::from_f64;
673 let w = std::f64::consts::FRAC_1_SQRT_2;
674 let sphere = NurbSurface::try_new(
675 2,
676 2,
677 vec![
678 hom(1., 0., 0., 1.),
679 hom(w, 0., w, w),
680 hom(0., 0., 1., 1.),
681 hom(w, w, 0., w),
682 hom(0.5, 0.5, 0.5, 0.5),
683 hom(0., 0., w, w),
684 hom(0., 1., 0., 1.),
685 hom(0., w, w, w),
686 hom(0., 0., 1., 1.),
687 ],
688 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
689 vec![f(0.), f(0.), f(0.), f(1.), f(1.), f(1.)],
690 )
691 .unwrap();
692 let n = sphere.normal(f(0.).union(f(1.)), f(1.)).unwrap();
693 assert!(
694 n[0].could_be_equal(Scalar::ZERO) && n[1].could_be_equal(Scalar::ZERO),
695 "{n:?}"
696 );
697 let below = sphere.normal(f(0.5), f(0.9)).unwrap();
699 assert!(
700 n[2].mul(below[2]).definitely_greater(Scalar::ZERO),
701 "{n:?} vs {below:?}"
702 );
703 }
704}