1use geop_core_math::{
49 geop_error::{GeopError, GeopResult},
50 scalars::Scalar,
51 solvers::linear::solve_symmetric2,
52 vector::{Vector3, Vector4},
53};
54
55use super::{
56 NurbSurface3D,
57 patch::{Neighbour, SIDES, TILT},
58};
59use crate::{
60 nurb_curve::{NurbCurve, NurbCurve3D, bezier_knots},
61 shape::Plane,
62};
63
64const SPOKE_DEGREE: usize = 5;
67
68const MOST_SPLITS: usize = 16;
75
76#[derive(Clone, Debug)]
78pub struct Fill {
79 pub patches: Vec<NurbSurface3D>,
84 pub spokes: Vec<NurbCurve3D>,
87 pub inner: Vec<Vector3>,
89 pub plane: Plane,
92}
93
94impl NurbSurface3D {
95 pub fn fill(sides: &[NurbCurve3D], neighbours: &[Option<Neighbour<'_>>]) -> GeopResult<Fill> {
107 let n = sides.len();
108 if n < 3 || neighbours.len() != n {
109 return Err(GeopError::new(format!(
110 "NurbSurface::fill: {n} sides and {} neighbours: a loop of three sides at least, a neighbour or none along each",
111 neighbours.len()
112 )));
113 }
114 let curves = (sides.iter())
115 .map(NurbCurve3D::with_unit_domain)
116 .collect::<GeopResult<Vec<_>>>()?;
117 let corners = (curves.iter())
118 .map(NurbCurve3D::start)
119 .collect::<GeopResult<Vec<_>>>()?;
120 for k in 0..n {
121 let end = curves[k].end()?;
122 if !end.could_be_equal(&corners[(k + 1) % n]) {
123 return Err(GeopError::new(format!(
124 "NurbSurface::fill: side {k} ends at {end:?}, but side {} starts at {:?}",
125 (k + 1) % n,
126 corners[(k + 1) % n]
127 )));
128 }
129 }
130 let (plane, inner) = inner_polygon(&corners)?;
131 let frames = (0..n)
132 .map(|k| Frame::at(k, &curves, neighbours, &plane, &inner))
133 .collect::<GeopResult<Vec<_>>>()?;
134 let curved = |k: usize| neighbours[k].as_ref().is_some_and(Neighbour::tilted);
135 let mut added: Vec<Vec<Scalar>> = vec![Vec::new(); n];
137 let mut strays = String::new();
138 for _ in 0..=MOST_SPLITS {
139 let (patches, spokes) = build(&curves, neighbours, &frames, &inner, &plane, &added)?;
140 let mut again = false;
141 for k in (0..n).filter(|&k| curved(k)) {
142 for stray in patches[k].misses(&outer(neighbours, k))? {
143 let knots = &patches[k].knot_vector_u;
145 let after = knots.iter().position(|t| t.definitely_greater(stray.at));
146 let Some(after) = after else { continue };
147 let half = knots[after - 1]
148 .add(knots[after])
149 .div(Scalar::TWO)?
150 .sharpen();
151 if !added[k].iter().any(|t| t.could_be_equal(half)) {
152 added[k].push(half);
153 again = true;
154 }
155 strays = format!("the patch along side {k} {}", stray.what);
156 }
157 }
158 if !again {
159 return Ok(Fill {
160 patches,
161 spokes,
162 inner,
163 plane,
164 });
165 }
166 }
167 Err(GeopError::new(format!(
168 "NurbSurface::fill: split its patches {MOST_SPLITS} times where they strayed from their curved neighbours, the fill still does not leave them at {TILT:e} radians: {strays}"
169 )))
170 }
171}
172
173fn outer<'a>(neighbours: &[Option<Neighbour<'a>>], k: usize) -> [Option<Neighbour<'a>>; SIDES] {
177 [neighbours[k].clone(), None, None, None]
178}
179
180fn inner_polygon(corners: &[Vector3]) -> GeopResult<(Plane, Vec<Vector3>)> {
184 let n = corners.len();
185 let center = corners
186 .iter()
187 .fold(Vector3::zero(), |sum, p| sum.add(p))
188 .prod_scalar(Scalar::ONE.div(Scalar::from_i64(n as i64))?)
189 .sharpen();
190 let mut normal = [0.0f64; 3];
191 for k in 0..n {
192 let (a, b) = (
193 corners[k].to_f64_array(),
194 corners[(k + 1) % n].to_f64_array(),
195 );
196 normal[0] += (a[1] - b[1]) * (a[2] + b[2]);
197 normal[1] += (a[2] - b[2]) * (a[0] + b[0]);
198 normal[2] += (a[0] - b[0]) * (a[1] + b[1]);
199 }
200 let length = (normal[0].powi(2) + normal[1].powi(2) + normal[2].powi(2)).sqrt();
201 if length.is_nan() || length <= 0.0 {
202 return Err(GeopError::new(format!(
203 "NurbSurface::fill: the loop's corners {corners:?} enclose no area"
204 )));
205 }
206 let normal = Vector3::from_array(normal.map(|x| Scalar::from_f64(x / length)));
207 let plane = Plane {
208 point: center,
209 normal,
210 };
211 let half = Scalar::ONE.div(Scalar::TWO)?;
212 let inner: Vec<Vector3> = corners
213 .iter()
214 .map(|p| {
215 plane
216 .project(¢er.add(&p.sub(¢er).prod_scalar(half)))
217 .sharpen()
218 })
219 .collect();
220 let mut area = Scalar::ZERO;
221 for k in 0..n {
222 area = area.add(
223 inner[k]
224 .sub(¢er)
225 .prod_cross(&inner[(k + 1) % n].sub(¢er))
226 .prod_dot(&normal),
227 );
228 }
229 if !area.definitely_greater(Scalar::ZERO) {
230 return Err(GeopError::new(format!(
231 "NurbSurface::fill: the inner polygon {inner:?} does not run round its normal {normal:?}"
232 )));
233 }
234 Ok((plane, inner))
235}
236
237struct Frame {
241 normal: Vector3,
244 into: Vector3,
247 out: Vector3,
249}
250
251impl Frame {
252 fn at(
253 k: usize,
254 curves: &[NurbCurve3D],
255 neighbours: &[Option<Neighbour<'_>>],
256 plane: &Plane,
257 inner: &[Vector3],
258 ) -> GeopResult<Self> {
259 let n = curves.len();
260 let before = (k + n - 1) % n;
261 let e = curves[k].tangent(Scalar::ZERO)?.normalize()?;
262 let f = curves[before].tangent(Scalar::ONE)?.neg().normalize()?;
263 let oriented = |v: Vector3, what: &str| -> GeopResult<Vector3> {
265 let along = v.prod_dot(&plane.normal);
266 if along.definitely_greater(Scalar::ZERO) {
267 Ok(v)
268 } else if along.definitely_less(Scalar::ZERO) {
269 Ok(v.neg())
270 } else {
271 Err(GeopError::new(format!(
272 "NurbSurface::fill: at corner {k} {what} {v:?} stands square to the loop's normal {:?}",
273 plane.normal
274 )))
275 }
276 };
277 let mut given: Vec<(usize, Vector3)> = Vec::new();
278 let corner = curves[k].start()?;
279 for (side, t) in [(k, Scalar::ZERO), (before, Scalar::ONE)] {
280 if let Some(s) = &neighbours[side] {
281 let (normal, _) = s.at(&corner, &curves[side].tangent(t)?)?;
282 given.push((side, oriented(normal, "the normal of the face along side")?));
283 }
284 }
285 let normal = match given.as_slice() {
286 [(a, na), (b, nb)] => {
287 if !na.prod_cross(nb).norm_sq().could_be_equal(Scalar::ZERO) {
288 return Err(GeopError::new(format!(
289 "NurbSurface::fill: the faces along sides {b} and {a} meet at an angle at corner {k}, normals {nb:?} and {na:?}: the fill cannot be tangent to both"
290 )));
291 }
292 *na
293 }
294 [(_, normal)] => *normal,
295 _ => {
296 let across = e.prod_cross(&f);
297 if across.norm_sq().definitely_greater(Scalar::ZERO) {
298 oriented(across.normalize()?, "the plane of the sides")?
299 } else {
300 let nu = plane.normal;
301 nu.sub(&e.prod_scalar(nu.prod_dot(&e))).normalize()?
302 }
303 }
304 };
305 for (side, tangent) in [(k, e), (before, f)] {
306 if !tangent.prod_dot(&normal).could_be_equal(Scalar::ZERO) {
307 return Err(GeopError::new(format!(
308 "NurbSurface::fill: side {side} leaves the tangent plane at corner {k}, normal {normal:?}, along {tangent:?}: the patches cannot be tangent there"
309 )));
310 }
311 }
312 let normal = normal.sharpen();
313 let turn = e.sub(&f);
314 if turn.norm_sq().could_be_equal(Scalar::ZERO) {
315 return Err(GeopError::new(format!(
316 "NurbSurface::fill: the loop turns back on itself at corner {k}"
317 )));
318 }
319 let into = normal.prod_cross(&turn).normalize()?.sharpen();
320 let outward = |a: usize, b: usize| -> GeopResult<Vector3> {
321 inner[b]
322 .sub(&inner[a])
323 .prod_cross(&plane.normal)
324 .normalize()
325 };
326 let out = outward(before, k)?
327 .add(&outward(k, (k + 1) % n)?)
328 .normalize()?
329 .sharpen();
330 Ok(Self { normal, into, out })
331 }
332}
333
334fn spoke_points(from: Vector3, to: Vector3, frame: &Frame, lift: Scalar) -> Vec<Vector3> {
340 let step = Scalar::from_f64(to.sub(&from).norm().to_f64() / SPOKE_DEGREE as f64);
341 let along = |p: &Vector3, d: &Vector3, times: i64| {
342 p.add(&d.prod_scalar(step.mul(Scalar::from_i64(times))))
343 .sharpen()
344 };
345 vec![
346 from,
347 along(&from, &frame.into, 1),
348 along(&from, &frame.into, 2)
349 .add(&frame.normal.prod_scalar(lift))
350 .sharpen(),
351 along(&to, &frame.out, 2),
352 along(&to, &frame.out, 1),
353 to,
354 ]
355}
356
357fn spoke_curve(points: &[Vector3]) -> GeopResult<NurbCurve3D> {
358 NurbCurve::try_new(
359 SPOKE_DEGREE,
360 points.iter().map(|p| p.homogeneous(Scalar::ONE)).collect(),
361 bezier_knots(SPOKE_DEGREE),
362 )
363}
364
365fn times_linear(a: [Scalar; 2], b: &[Vector3]) -> GeopResult<Vec<Vector3>> {
369 let m = b.len() - 1;
370 let q = Scalar::from_i64((m + 1) as i64);
371 (0..=m + 1)
372 .map(|j| {
373 let mut out = Vector3::zero();
374 if j <= m {
375 let f = Scalar::from_i64((m + 1 - j) as i64).div(q)?;
376 out = out.add(&b[j].prod_scalar(a[0].mul(f)));
377 }
378 if j >= 1 {
379 let f = Scalar::from_i64(j as i64).div(q)?;
380 out = out.add(&b[j - 1].prod_scalar(a[1].mul(f)));
381 }
382 Ok(out)
383 })
384 .collect()
385}
386
387fn field(frame: &Frame, plane: &Plane, lean: Scalar) -> Vec<Vector3> {
392 let start = frame.normal.prod_cross(&frame.into).sharpen();
393 let end = frame.out.prod_cross(&plane.normal).sharpen();
394 vec![
395 start,
396 start.add(&frame.normal.prod_scalar(lean)).sharpen(),
397 start,
398 end,
399 end,
400 end,
401 ]
402}
403
404#[derive(Clone, Copy)]
407struct Across {
408 alpha: [Scalar; 2],
409 beta: [Scalar; 2],
410}
411
412impl Across {
413 fn of(ends: [Vector3; 2], v: [Vector3; 2], t: [Vector3; 2]) -> GeopResult<Self> {
417 let mut alpha = [Scalar::ZERO; 2];
418 let mut beta = [Scalar::ZERO; 2];
419 for e in 0..2 {
420 let (a, b) = solve_symmetric2(
421 (
422 v[e].prod_dot(&v[e]),
423 v[e].prod_dot(&t[e]),
424 t[e].prod_dot(&t[e]),
425 ),
426 (v[e].prod_dot(&ends[e]), t[e].prod_dot(&ends[e])),
427 )?;
428 alpha[e] = a.sharpen();
429 beta[e] = b.sharpen();
430 }
431 Ok(Self { alpha, beta })
432 }
433
434 fn points(&self, spoke: &[Vector3], v: &[Vector3]) -> GeopResult<Vec<Vector3>> {
438 let p = Scalar::from_i64(SPOKE_DEGREE as i64);
439 let t: Vec<Vector3> = spoke
440 .windows(2)
441 .map(|w| w[1].sub(&w[0]).prod_scalar(p))
442 .collect();
443 let tangent = times_linear([Scalar::ONE; 2], ×_linear(self.beta, &t)?)?;
444 let across = times_linear(self.alpha, v)?;
445 Ok(across.iter().zip(&tangent).map(|(a, b)| a.add(b)).collect())
446 }
447}
448
449fn row_next_to(
455 spoke: &[Vector3],
456 across: &[Vector3],
457 weights: [Scalar; 2],
458 into: bool,
459) -> GeopResult<Vec<Vector4>> {
460 let weighted = times_linear(weights, spoke)?;
461 let q = Scalar::from_i64((SPOKE_DEGREE + 1) as i64);
462 (0..=SPOKE_DEGREE + 1)
463 .map(|j| {
464 let w = Scalar::interpolate(weights[0], weights[1], Scalar::from_i64(j as i64).div(q)?);
465 let h = if into {
466 weighted[j].add(&across[j])
467 } else {
468 weighted[j].sub(&across[j])
469 };
470 Ok(Vector4::from_array([h[0], h[1], h[2], w]))
471 })
472 .collect()
473}
474
475fn split(cp: &Vector4) -> (Vector3, Scalar) {
477 (Vector3::from_array([cp[0], cp[1], cp[2]]), cp[3])
478}
479
480fn coons_quad(
490 side: &NurbCurve3D,
491 spokes: [&[Vector3]; 2],
492 inner: [Vector3; 2],
493 rows: usize,
494 added: &[Scalar],
495) -> GeopResult<NurbSurface3D> {
496 let start = spoke_curve(spokes[0])?;
497 let end = spoke_curve(spokes[1])?;
498 let back = NurbCurve3D::line(inner[1], inner[0])?;
499 let mut patch = NurbSurface3D::coons([side, &end, &back, &start.reverse()])?;
500 patch = patch.elevate_degree(false)?;
501 while patch.degree_u < 3 {
502 patch = patch.elevate_degree(true)?;
503 }
504 while patch.num_u < rows {
505 patch = patch.refined([2, 1])?;
506 }
507 Ok(patch.with_knots_u(added))
508}
509
510fn build(
513 curves: &[NurbCurve3D],
514 neighbours: &[Option<Neighbour<'_>>],
515 frames: &[Frame],
516 inner: &[Vector3],
517 plane: &Plane,
518 added: &[Vec<Scalar>],
519) -> GeopResult<(Vec<NurbSurface3D>, Vec<NurbCurve3D>)> {
520 let n = curves.len();
521 let corner = |k: usize| curves[k % n].start();
522 let rows = |k: usize| {
523 if neighbours[k].as_ref().is_some_and(|s| !s.flat()) {
524 5
525 } else {
526 4
527 }
528 };
529 let quads = |spokes: &[Vec<Vector3>]| -> GeopResult<Vec<NurbSurface3D>> {
530 (0..n)
531 .map(|k| {
532 coons_quad(
533 &curves[k],
534 [&spokes[k], &spokes[(k + 1) % n]],
535 [inner[k], inner[(k + 1) % n]],
536 rows(k),
537 &added[k],
538 )
539 })
540 .collect()
541 };
542 let flat: Vec<Vec<Vector3>> = (0..n)
546 .map(|k| Ok(spoke_points(corner(k)?, inner[k], &frames[k], Scalar::ZERO)))
547 .collect::<GeopResult<_>>()?;
548 let first = quads(&flat)?;
549 let mut acrosses = Vec::with_capacity(n);
553 for k in 0..n {
554 let (after, before) = (&first[k], &first[(k + n - 1) % n]);
555 let spoke = &flat[k];
556 let v = field(&frames[k], plane, Scalar::ZERO);
557 let p = SPOKE_DEGREE;
558 let t = [
559 spoke[1]
560 .sub(&spoke[0])
561 .prod_scalar(Scalar::from_i64(p as i64)),
562 spoke[p]
563 .sub(&spoke[p - 1])
564 .prod_scalar(Scalar::from_i64(p as i64)),
565 ];
566 let ends = |patch: &NurbSurface3D, i: usize, into: bool| -> GeopResult<[Vector3; 2]> {
567 let nv = patch.num_v;
568 let mut out = [Vector3::zero(); 2];
569 for (e, (j, on)) in [(0, spoke[0]), (nv - 1, spoke[p])].into_iter().enumerate() {
570 let (h, w) = split(&patch.control_points[i * nv + j]);
571 let d = h.sub(&on.prod_scalar(w));
572 out[e] = if into { d } else { d.neg() };
573 }
574 Ok(out)
575 };
576 let x = Across::of(ends(after, 1, true)?, [v[0], v[p]], t)?;
577 let y = Across::of(ends(before, before.num_u - 2, false)?, [v[0], v[p]], t)?;
578 for e in 0..2 {
579 if !x.alpha[e].mul(y.alpha[e]).definitely_greater(Scalar::ZERO) {
580 return Err(GeopError::new(format!(
581 "NurbSurface::fill: the patches either side of spoke {k} fold over each other at its {} end: they leave it {:?} and {:?} across",
582 if e == 0 { "outer" } else { "inner" },
583 x.alpha[e],
584 y.alpha[e]
585 )));
586 }
587 }
588 acrosses.push((x, y));
589 }
590 let mut spokes = Vec::with_capacity(n);
592 let mut fields = Vec::with_capacity(n);
593 for k in 0..n {
594 let (lift, lean) = twist(k, neighbours, &frames[k], &first, &flat, &acrosses)
595 .map_err(|e| e.with_context(format!("NurbSurface::fill: the twist at corner {k}")))?;
596 spokes.push(spoke_points(corner(k)?, inner[k], &frames[k], lift));
597 fields.push(field(&frames[k], plane, lean));
598 }
599 let mut patches = quads(&spokes)?;
600 for (k, patch) in patches.iter_mut().enumerate() {
601 set_rows(patch, k, &spokes, &fields, &acrosses, inner, plane)?;
602 if neighbours[k].is_some() {
603 let nu = patch.num_u;
604 *patch = patch
605 .tangent_rows(&outer(neighbours, k), &|i, j| {
606 j == 1 && (i == 1 || i == nu - 2)
607 })
608 .map_err(|e| {
609 e.with_context(format!("NurbSurface::fill: the patch along side {k}"))
610 })?;
611 }
612 }
613 let spokes = spokes
614 .iter()
615 .map(|s| spoke_curve(s))
616 .collect::<GeopResult<Vec<_>>>()?;
617 Ok((patches, spokes))
618}
619
620fn set_rows(
624 patch: &mut NurbSurface3D,
625 k: usize,
626 spokes: &[Vec<Vector3>],
627 fields: &[Vec<Vector3>],
628 acrosses: &[(Across, Across)],
629 inner: &[Vector3],
630 plane: &Plane,
631) -> GeopResult<()> {
632 let n = spokes.len();
633 let next = (k + 1) % n;
634 let (nu, nv) = (patch.num_u, patch.num_v);
635 let weights = |i: usize| {
636 [
637 patch.control_points[i * nv][3],
638 patch.control_points[i * nv + nv - 1][3],
639 ]
640 };
641 let start = row_next_to(
642 &spokes[k],
643 &acrosses[k].0.points(&spokes[k], &fields[k])?,
644 weights(1),
645 true,
646 )?;
647 let end = row_next_to(
648 &spokes[next],
649 &acrosses[next].1.points(&spokes[next], &fields[next])?,
650 weights(nu - 2),
651 false,
652 )?;
653 for j in 1..nv - 1 {
654 patch.control_points[nv + j] = start[j].sharpen();
655 patch.control_points[(nu - 2) * nv + j] = end[j].sharpen();
656 }
657 for i in 2..nu - 2 {
658 let cp = patch.control_points[i * nv + nv - 2];
659 let (point, w) = (cp.dehomogenized()?, cp[3]);
660 patch.control_points[i * nv + nv - 2] = plane.project(&point).homogeneous(w);
661 }
662 let out = inner[next].sub(&inner[k]).prod_cross(&plane.normal);
663 for i in 0..nu {
664 let row = patch.control_points[i * nv + nv - 2].dehomogenized()?;
665 let on = patch.control_points[i * nv + nv - 1].dehomogenized()?;
666 if !row.sub(&on).prod_dot(&out).definitely_greater(Scalar::ZERO) {
667 return Err(GeopError::new(format!(
668 "NurbSurface::fill: the patch along side {k} folds back over the inner polygon at control point ({i}, {}), {row:?}",
669 nv - 2
670 )));
671 }
672 }
673 patch.recompute_aabb();
674 Ok(())
675}
676
677fn twist(
696 k: usize,
697 neighbours: &[Option<Neighbour<'_>>],
698 frame: &Frame,
699 first: &[NurbSurface3D],
700 flat: &[Vec<Vector3>],
701 acrosses: &[(Across, Across)],
702) -> GeopResult<(Scalar, Scalar)> {
703 let n = neighbours.len();
704 let before = (k + n - 1) % n;
705 let curved = |s: usize| neighbours[s].as_ref().is_some_and(|s| !s.flat());
706 if !curved(k) && !curved(before) {
707 return Ok((Scalar::ZERO, Scalar::ZERO));
708 }
709 let spoke = &flat[k];
710 let p = Scalar::from_i64(SPOKE_DEGREE as i64);
711 let leaving = spoke[1].sub(&spoke[0]).prod_scalar(p);
713 let tilt = Scalar::from_f64(TILT).mul(leaving.norm());
714 let mut a = Vec::with_capacity(2);
715 let mut rhs = Vec::with_capacity(2);
716 for (side, into) in [(k, true), (before, false)] {
717 let Some(neighbour) = &neighbours[side] else {
718 continue;
719 };
720 let patch = &first[side];
721 let u = if into { Scalar::ZERO } else { Scalar::ONE };
722 let (along, _) = patch.derivatives(u, Scalar::ZERO)?;
723 let (normal, turn) = neighbour.turning(&spoke[0], &along).map_err(|e| {
724 e.with_context(format!(
725 "NurbSurface::fill: how the face along side {side} turns at corner {k}, {:?}, along {along:?}",
726 spoke[0]
727 ))
728 })?;
729 let knots = &patch.knot_vector_u;
732 let d = patch.degree_u;
733 let (span, abscissa) = if into {
734 (
735 knots[d + 1].sub(knots[0]),
736 knots[2..=d + 1].iter().fold(Scalar::ZERO, |s, &x| s.add(x)),
737 )
738 } else {
739 let m = knots.len() - 1;
740 (
741 knots[m].sub(knots[m - d - 1]),
742 knots[m - d - 1..m - 1]
743 .iter()
744 .fold(Scalar::ZERO, |s, &x| s.add(knots[m].sub(x))),
745 )
746 };
747 let scale = Scalar::from_i64(d as i64)
750 .div(span)?
751 .mul(normal.prod_dot(&frame.normal));
752 let across = if into { acrosses[k].0 } else { acrosses[k].1 };
753 let slope = if !neighbour.tilted() {
756 Scalar::ZERO
757 } else {
758 tilt.mul(Scalar::from_i64(d as i64)).div(abscissa)?
759 };
760 let wanted = if into { slope } else { slope.neg() };
763 let by_lift = scale.mul(Scalar::from_i64(20)).mul(across.beta[0]);
764 let by_lean = scale.mul(Scalar::from_i64(5)).mul(across.alpha[0]);
765 a.push((by_lift, by_lean));
766 rhs.push(wanted.sub(leaving.prod_dot(&turn)));
767 }
768 Ok(match (a.as_slice(), rhs.as_slice()) {
769 ([(l0, t0), (l1, t1)], [r0, r1]) => {
770 let det = l0.mul(*t1).sub(t0.mul(*l1));
771 (
772 r0.mul(*t1).sub(t0.mul(*r1)).div(det)?.sharpen(),
773 l0.mul(*r1).sub(r0.mul(*l1)).div(det)?.sharpen(),
774 )
775 }
776 ([(_, t0)], [r0]) => (Scalar::ZERO, r0.div(*t0)?.sharpen()),
777 _ => (Scalar::ZERO, Scalar::ZERO),
778 })
779}
780
781#[cfg(test)]
782mod tests {
783 use geop_core_math::{scalars::Scalar, vector::Vector3};
784
785 use crate::nurb_curve::{NurbCurve3D, sqrt2_over_2};
786 use crate::nurb_surface::NurbSurface3D;
787
788 fn v(x: f64, y: f64, z: f64) -> Vector3 {
789 Vector3::from_array([x, y, z].map(Scalar::from_f64))
790 }
791
792 fn quarter(a: Vector3, b: Vector3) -> NurbCurve3D {
795 NurbCurve3D::arc(a, a.add(&b), b, sqrt2_over_2()).unwrap()
796 }
797
798 fn parallel(a: &Vector3, b: &Vector3) -> bool {
801 a.prod_cross(b).norm().to_f64() < 1e-10
802 }
803
804 #[test]
809 fn an_octant_is_filled_smoothly() {
810 let (x, y, z) = (v(1., 0., 0.), v(0., 1., 0.), v(0., 0., 1.));
811 let sides = [quarter(x, y), quarter(y, z), quarter(z, x)];
812 let fill = NurbSurface3D::fill(&sides, &[None, None, None]).unwrap();
813 assert_eq!(fill.patches.len(), 3);
814 let s = |i: i64| Scalar::from_ratio(i, 16).unwrap();
815 for (k, patch) in fill.patches.iter().enumerate() {
816 let before = &fill.patches[(k + 2) % 3];
817 for i in 0..=16 {
818 let on = patch.evaluate(s(i), Scalar::ZERO).unwrap();
820 let side = sides[k]
821 .evaluate(sides[k].closest_point(&on).unwrap().0)
822 .unwrap();
823 assert!(on.sub(&side).norm().to_f64() < 1e-12, "side {k} at {i}");
824 if i < 16 {
827 let here = patch.normal(Scalar::ZERO, s(i)).unwrap();
828 let there = before.normal(Scalar::ONE, s(i)).unwrap();
829 assert!(
830 parallel(&here, &there),
831 "spoke {k} at {i}: {here:?} {there:?}"
832 );
833 }
834 if 0 < i && i < 16 {
836 let n = patch.normal(s(i), Scalar::ONE).unwrap();
837 assert!(
838 parallel(&n, &fill.plane.normal),
839 "inner side {k} at {i}: {n:?}"
840 );
841 }
842 }
843 }
844 }
845}