1use geop_core_math::{
53 geop_error::{GeopError, GeopResult},
54 primitives::Motion,
55 scalars::Scalar,
56 vector::{Vector3, Vector4},
57};
58
59use crate::{bernstein::Bernstein, nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D};
60
61pub trait Deformation {
67 fn point(&self, p: &Vector3) -> GeopResult<Vector3>;
69 fn curve(
71 &self,
72 curve: &NurbCurve3D,
73 at: &dyn Fn() -> GeopResult<Vector3>,
74 ) -> GeopResult<NurbCurve3D>;
75 fn surface(
77 &self,
78 surface: &NurbSurface3D,
79 at: &dyn Fn() -> GeopResult<Vector3>,
80 ) -> GeopResult<NurbSurface3D>;
81 fn mirrors(&self) -> bool;
83 fn affine(&self) -> bool;
87}
88
89impl Deformation for Motion {
90 fn point(&self, p: &Vector3) -> GeopResult<Vector3> {
91 Ok(self.apply(p))
92 }
93
94 fn curve(
95 &self,
96 curve: &NurbCurve3D,
97 _: &dyn Fn() -> GeopResult<Vector3>,
98 ) -> GeopResult<NurbCurve3D> {
99 Ok(curve.transform(self))
100 }
101
102 fn surface(
103 &self,
104 surface: &NurbSurface3D,
105 _: &dyn Fn() -> GeopResult<Vector3>,
106 ) -> GeopResult<NurbSurface3D> {
107 Ok(surface.transform(self))
108 }
109
110 fn mirrors(&self) -> bool {
111 Motion::mirrors(self)
112 }
113
114 fn affine(&self) -> bool {
115 true
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq)]
123pub struct AxialFunction {
124 constant: Scalar,
125 slope: Scalar,
126 cos: Scalar,
127 sin: Scalar,
128 rate: Scalar,
129 start: Scalar,
130}
131
132impl AxialFunction {
133 const ZERO: Self = AxialFunction {
134 constant: Scalar::ZERO,
135 slope: Scalar::ZERO,
136 cos: Scalar::ZERO,
137 sin: Scalar::ZERO,
138 rate: Scalar::ZERO,
139 start: Scalar::ZERO,
140 };
141
142 fn linear(constant: Scalar, slope: Scalar, start: Scalar) -> Self {
144 AxialFunction {
145 constant,
146 slope,
147 start,
148 ..Self::ZERO
149 }
150 }
151
152 fn trig(constant: Scalar, cos: Scalar, sin: Scalar, rate: Scalar, start: Scalar) -> Self {
154 AxialFunction {
155 constant,
156 cos,
157 sin,
158 rate,
159 start,
160 ..Self::ZERO
161 }
162 }
163
164 fn is_zero(&self) -> bool {
165 [self.constant, self.slope, self.cos, self.sin]
166 .iter()
167 .all(|c| c.is_exactly(Scalar::ZERO))
168 }
169
170 fn is_trig(&self) -> bool {
172 !(self.cos.is_exactly(Scalar::ZERO) && self.sin.is_exactly(Scalar::ZERO))
173 }
174
175 fn evaluate(&self, t: Scalar) -> Scalar {
177 let s = t.sub(self.start);
178 let mut value = self.constant;
179 if !self.slope.is_exactly(Scalar::ZERO) {
180 value = value.add(self.slope.mul(s));
181 }
182 if self.is_trig() {
183 let phase = self.rate.mul(s);
184 value = value
185 .add(self.cos.mul(phase.cos()))
186 .add(self.sin.mul(phase.sin()));
187 }
188 value
189 }
190
191 fn derivative(&self) -> Self {
193 AxialFunction {
194 constant: self.slope,
195 slope: Scalar::ZERO,
196 cos: self.rate.mul(self.sin),
197 sin: self.rate.mul(self.cos).neg(),
198 rate: self.rate,
199 start: self.start,
200 }
201 }
202
203 fn interpolant(
216 &self,
217 middle: Scalar,
218 reach: Scalar,
219 degree: usize,
220 ) -> GeopResult<(Vec<Scalar>, Scalar)> {
221 let mut coefficients = vec![self.evaluate(middle)];
222 if reach.is_exactly(Scalar::ZERO) {
223 coefficients.resize(degree + 1, Scalar::ZERO);
224 return Ok((coefficients, Scalar::ZERO));
225 }
226 if !self.is_trig() {
227 coefficients.push(self.slope.mul(reach));
228 coefficients.resize(degree + 1, Scalar::ZERO);
229 return Ok((coefficients, Scalar::ZERO));
230 }
231 let nodes: Vec<Scalar> = (0..=degree)
232 .map(|j| {
233 let angle = std::f64::consts::PI * (2 * j + 1) as f64 / (2 * (degree + 1)) as f64;
234 Scalar::from_f64(angle.cos())
235 })
236 .collect();
237 let mut differences: Vec<Scalar> = (nodes.iter())
239 .map(|&n| self.evaluate(middle.add(reach.mul(n))))
240 .collect();
241 for level in 1..=degree {
242 for j in (level..=degree).rev() {
243 let step = nodes[j].sub(nodes[j - level]);
244 differences[j] = differences[j].sub(differences[j - 1]).div(step)?;
245 }
246 }
247 let mut coefficients = vec![differences[degree]];
248 for j in (0..degree).rev() {
249 let mut next = vec![Scalar::ZERO; coefficients.len() + 1];
251 for (n, &c) in coefficients.iter().enumerate() {
252 next[n + 1] = next[n + 1].add(c);
253 next[n] = next[n].sub(c.mul(nodes[j]));
254 }
255 next[0] = next[0].add(differences[j]);
256 coefficients = next;
257 }
258 const PIECES: i64 = 16;
262 let mut nodal = Scalar::ZERO;
263 for k in 0..PIECES {
264 let lo = Scalar::from_ratio(2 * k - PIECES, PIECES)?;
265 let hi = Scalar::from_ratio(2 * k + 2 - PIECES, PIECES)?;
266 let mut piece = Bernstein::constant(Scalar::ONE);
267 for &n in &nodes {
268 piece = piece.product(&Bernstein::new([1, 0], vec![lo.sub(n), hi.sub(n)]))?;
269 }
270 nodal = (piece.coefficients().iter()).fold(nodal, |m, c| m.max(c.abs()));
271 }
272 let step = self.rate.abs().mul(reach.abs());
273 let mut bound = self.cos.abs().add(self.sin.abs()).mul(nodal);
274 for n in 1..=degree + 1 {
275 bound = bound.mul(step).div(Scalar::from_i64(n as i64))?;
276 }
277 let bound = bound.upper().to_f64();
278 Ok((coefficients, Scalar::new(-bound, bound)))
279 }
280}
281
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285enum Piece {
286 Before,
287 Law,
288 After,
289}
290
291const MAX_PIECES: usize = 4096;
294
295const RELATIVE_TARGET: f64 = 1e-9;
300
301#[derive(Clone, Debug, PartialEq)]
308pub struct AxialMap {
309 origin: Vector3,
310 frame: [Vector3; 3],
311 start: Scalar,
312 end: Scalar,
313 columns: [[AxialFunction; 3]; 3],
314}
315
316impl AxialMap {
317 fn frame(
322 axis: &Vector3,
323 toward: Option<&Vector3>,
324 (start, end): (Scalar, Scalar),
325 ) -> GeopResult<([Vector3; 3], Scalar, Scalar)> {
326 if !end.definitely_greater(start) {
327 return Err(GeopError::new(format!(
328 "AxialMap: the zone from {start:?} to {end:?} along the axis does not run forward"
329 )));
330 }
331 let unit = |v: &Vector3| {
334 if v.norm_sq().is_exactly(Scalar::ONE) {
335 Ok(*v)
336 } else {
337 v.normalize()
338 }
339 };
340 let t = unit(axis).map_err(|e| e.with_context(format!("AxialMap: the axis {axis:?}")))?;
341 let x = match toward {
342 None => t.across()?[0],
343 Some(toward) => {
344 let square = toward.sub(&t.prod_scalar(toward.prod_dot(&t)));
345 unit(&square).map_err(|e| {
346 e.with_context(format!(
347 "AxialMap: {toward:?} runs along the axis {axis:?}, so it gives no direction to bend towards"
348 ))
349 })?
350 }
351 };
352 let y = t.prod_cross(&x);
353 Ok(([x, y, t], start, end))
354 }
355
356 pub fn taper(
362 origin: Vector3,
363 axis: &Vector3,
364 toward: Option<&Vector3>,
365 zone: (Scalar, Scalar),
366 factor: Scalar,
367 ) -> GeopResult<Self> {
368 if !factor.definitely_greater(Scalar::ZERO) {
369 return Err(GeopError::new(format!(
370 "AxialMap::taper: a factor of {factor:?} turns the body inside out or flattens it; it has to be positive"
371 )));
372 }
373 let (frame, start, end) = Self::frame(axis, toward, zone)?;
374 let slope = factor.sub(Scalar::ONE).div(end.sub(start))?;
375 let scale = AxialFunction::linear(Scalar::ONE, slope, start);
376 let zero = AxialFunction::ZERO;
377 let across = match toward {
378 None => scale,
379 Some(_) => AxialFunction::linear(Scalar::ONE, Scalar::ZERO, start),
380 };
381 let along = AxialFunction::linear(start, Scalar::ONE, start);
382 Ok(AxialMap {
383 origin,
384 frame,
385 start,
386 end,
387 columns: [
388 [scale, zero, zero],
389 [zero, across, zero],
390 [zero, zero, along],
391 ],
392 })
393 }
394
395 pub fn shear(
401 origin: Vector3,
402 axis: &Vector3,
403 toward: &Vector3,
404 zone: (Scalar, Scalar),
405 offset: Scalar,
406 ) -> GeopResult<Self> {
407 let (frame, start, end) = Self::frame(axis, Some(toward), zone)?;
408 let (zero, one, s0) = (Scalar::ZERO, Scalar::ONE, AxialFunction::ZERO);
409 let unit = AxialFunction::linear(one, zero, start);
410 let across = AxialFunction::linear(zero, offset.div(end.sub(start))?, start);
411 let along = AxialFunction::linear(start, one, start);
412 Ok(AxialMap {
413 origin,
414 frame,
415 start,
416 end,
417 columns: [[unit, s0, s0], [s0, unit, s0], [across, s0, along]],
418 })
419 }
420
421 pub fn as_motion(&self) -> GeopResult<Option<Motion>> {
426 let constant = |f: &AxialFunction| !f.is_trig() && f.slope.is_exactly(Scalar::ZERO);
427 let linear = |f: &AxialFunction| !f.is_trig();
428 let affine = self.columns[0].iter().all(constant)
429 && self.columns[1].iter().all(constant)
430 && self.columns[2].iter().all(linear);
431 if !affine {
432 return Ok(None);
433 }
434 self.tangent_motion(self.start).map(Some)
435 }
436
437 pub fn twist(
442 origin: Vector3,
443 axis: &Vector3,
444 zone: (Scalar, Scalar),
445 angle: Scalar,
446 ) -> GeopResult<Self> {
447 let (frame, start, end) = Self::frame(axis, None, zone)?;
448 let rate = angle.div(end.sub(start))?;
449 let one = Scalar::ONE;
450 let (zero, s0) = (Scalar::ZERO, AxialFunction::ZERO);
451 let cos = AxialFunction::trig(zero, one, zero, rate, start);
452 let sin = AxialFunction::trig(zero, zero, one, rate, start);
453 let minus_sin = AxialFunction::trig(zero, zero, one.neg(), rate, start);
454 let along = AxialFunction::linear(start, one, start);
455 Ok(AxialMap {
456 origin,
457 frame,
458 start,
459 end,
460 columns: [[cos, sin, s0], [minus_sin, cos, s0], [s0, s0, along]],
461 })
462 }
463
464 pub fn bend(
474 origin: Vector3,
475 axis: &Vector3,
476 toward: &Vector3,
477 zone: (Scalar, Scalar),
478 angle: Scalar,
479 ) -> GeopResult<Self> {
480 let (frame, start, end) = Self::frame(axis, Some(toward), zone)?;
481 let full = Scalar::PI.mul(Scalar::TWO);
482 if angle.abs().could_be_greater(full) || angle.abs().could_be_equal(full) {
483 return Err(GeopError::new(format!(
484 "AxialMap::bend: a bend of {angle:?} radians turns a whole circle or more, laying the zone over itself"
485 )));
486 }
487 let (zero, one, s0) = (Scalar::ZERO, Scalar::ONE, AxialFunction::ZERO);
488 let along = AxialFunction::linear(start, one, start);
489 let unit = AxialFunction::linear(one, zero, start);
490 let columns = if angle.is_exactly(zero) {
491 [[unit, s0, s0], [s0, unit, s0], [s0, s0, along]]
492 } else {
493 let rate = angle.div(end.sub(start))?;
494 let radius = end.sub(start).div(angle)?;
495 let cos = AxialFunction::trig(zero, one, zero, rate, start);
496 let minus_sin = AxialFunction::trig(zero, zero, one.neg(), rate, start);
497 let cx = AxialFunction::trig(radius, radius.neg(), zero, rate, start);
499 let ct = AxialFunction::trig(start, zero, radius, rate, start);
500 [[cos, s0, minus_sin], [s0, unit, s0], [cx, s0, ct]]
501 };
502 Ok(AxialMap {
503 origin,
504 frame,
505 start,
506 end,
507 columns,
508 })
509 }
510
511 pub fn zone(&self) -> (Scalar, Scalar) {
513 (self.start, self.end)
514 }
515
516 pub fn along(&self, p: &Vector3) -> Scalar {
518 self.local(p)[2]
519 }
520
521 pub fn plane_across(&self, t: Scalar, reach: Scalar) -> GeopResult<NurbSurface3D> {
525 let [x, y, axis] = &self.frame;
526 let center = self.origin.add(&axis.prod_scalar(t));
527 let corner = |sx: Scalar, sy: Scalar| {
528 center
529 .add(&x.prod_scalar(sx.mul(reach)))
530 .add(&y.prod_scalar(sy.mul(reach)))
531 };
532 let (minus, plus) = (Scalar::ONE.neg(), Scalar::ONE);
533 NurbSurface3D::bilinear(
534 corner(minus, minus),
535 corner(plus, minus),
536 corner(plus, plus),
537 corner(minus, plus),
538 )
539 }
540
541 fn local(&self, p: &Vector3) -> [Scalar; 3] {
543 let q = p.sub(&self.origin);
544 self.frame.map(|e| e.prod_dot(&q))
545 }
546
547 fn world(&self, local: [Scalar; 3]) -> Vector3 {
550 (self.frame.iter().zip(local))
551 .map(|(e, c)| e.prod_scalar(c))
552 .reduce(|a, b| a.add(&b))
553 .expect("three axes")
554 }
555
556 fn pieces(&self, t: Scalar) -> Vec<Piece> {
558 let mut pieces = Vec::new();
559 if t.could_be_less(self.start) {
560 pieces.push(Piece::Before);
561 }
562 if !(t.definitely_less(self.start) || t.definitely_greater(self.end)) {
563 pieces.push(Piece::Law);
564 }
565 if t.could_be_greater(self.end) {
566 pieces.push(Piece::After);
567 }
568 pieces
569 }
570
571 fn tangent_motion(&self, t: Scalar) -> GeopResult<Motion> {
575 let at = |column: &[AxialFunction; 3]| column.map(|f| f.evaluate(t));
576 let [a, b, c] = [
577 at(&self.columns[0]),
578 at(&self.columns[1]),
579 at(&self.columns[2]),
580 ];
581 let dc = self.columns[2].map(|f| f.derivative().evaluate(t));
582 let [ex, ey, et] = &self.frame;
583 let columns = std::array::from_fn(|k| {
586 self.world(std::array::from_fn(|i| {
587 a[i].mul(ex[k]).add(b[i].mul(ey[k])).add(dc[i].mul(et[k]))
588 }))
589 });
590 let linear = |p: &Vector3| -> Vector3 {
591 (columns.iter().zip(p.to_array()))
592 .map(|(col, pk): (&Vector3, Scalar)| col.prod_scalar(pk))
593 .reduce(|x, y| x.add(&y))
594 .expect("three columns")
595 };
596 let offset = self.world(std::array::from_fn(|i| c[i].sub(t.mul(dc[i]))));
597 let position = self.origin.add(&offset).sub(&linear(&self.origin));
598 Motion::affine(columns, position)
599 }
600
601 fn law_point(&self, [x, y, t]: [Scalar; 3]) -> Vector3 {
603 let local = std::array::from_fn(|i| {
604 let [a, b, c] = [0, 1, 2].map(|col| self.columns[col][i]);
605 let mut v = c.evaluate(t);
606 if !a.is_zero() {
607 v = v.add(x.mul(a.evaluate(t)));
608 }
609 if !b.is_zero() {
610 v = v.add(y.mul(b.evaluate(t)));
611 }
612 v
613 });
614 self.origin.add(&self.world(local))
615 }
616
617 fn interpolant_degree(&self) -> usize {
622 if self.columns.iter().flatten().any(AxialFunction::is_trig) {
623 3
624 } else {
625 1
626 }
627 }
628
629 fn piece_of(
634 &self,
635 points: &[Vector4],
636 at: &dyn Fn() -> GeopResult<Vector3>,
637 ) -> GeopResult<Piece> {
638 let t = (points.iter())
639 .map(|p| Ok(self.local(&p.dehomogenized()?)[2]))
640 .collect::<GeopResult<Vec<_>>>()?;
641 if let Some(piece) = self.piece_holding(&t) {
642 return Ok(piece);
643 }
644 let t = self.local(&at()?)[2];
645 Ok(self.piece_holding(&[t]).unwrap_or(Piece::Law))
646 }
647
648 fn piece_holding(&self, t: &[Scalar]) -> Option<Piece> {
655 let all = |f: &dyn Fn(Scalar) -> bool| t.iter().all(|&t| f(t));
656 if all(&|t| !t.definitely_less(self.start) && !t.definitely_greater(self.end)) {
657 Some(Piece::Law)
658 } else if all(&|t| !t.definitely_greater(self.start)) {
659 Some(Piece::Before)
660 } else if all(&|t| !t.definitely_less(self.end)) {
661 Some(Piece::After)
662 } else {
663 None
664 }
665 }
666
667 fn law_piece(&self, h: &[Bernstein; 4]) -> GeopResult<Option<[Bernstein; 4]>> {
674 let one = Bernstein::constant(Scalar::ONE);
678 let polynomial = (h[3].coefficients().iter()).all(|w| w.is_exactly(Scalar::ONE));
679 let w = if polynomial { &one } else { &h[3] };
680 let relative: Vec<Bernstein> = (0..3)
682 .map(|k| {
683 let o = self.origin[k];
684 if o.is_exactly(Scalar::ZERO) {
685 Ok(h[k].clone())
686 } else {
687 Ok(Bernstein::combination([(None, &h[k]), (Some(o.neg()), w)])?
688 .expect("two terms"))
689 }
690 })
691 .collect::<GeopResult<_>>()?;
692 let local: Vec<Bernstein> = (self.frame.iter())
693 .map(|e| {
694 let terms = (0..3).filter(|&k| !e[k].is_exactly(Scalar::ZERO)).map(|k| {
695 (
696 (!e[k].is_exactly(Scalar::ONE)).then_some(e[k]),
697 &relative[k],
698 )
699 });
700 Ok(Bernstein::combination(terms)?
701 .unwrap_or_else(|| Bernstein::constant(Scalar::ZERO)))
702 })
703 .collect::<GeopResult<_>>()?;
704 let reach = |b: &Bernstein| -> GeopResult<Scalar> {
707 (b.coefficients().iter().zip(h[3].coefficients()))
708 .map(|(c, w)| if polynomial { Ok(*c) } else { c.div(*w) })
709 .reduce(|a, b| Ok(a?.union(b?)))
710 .expect("at least one coefficient")
711 };
712 let [x_range, y_range, t_range] = [reach(&local[0])?, reach(&local[1])?, reach(&local[2])?];
713 self.check_axis(x_range)?;
714 let middle = t_range.sharpen();
715 let half = (t_range.upper().sub(middle))
716 .max(middle.sub(t_range.lower()))
717 .upper();
718 let factor = [x_range.abs().upper(), y_range.abs().upper(), Scalar::ONE];
719 let size = [x_range, y_range, t_range]
720 .iter()
721 .fold(Scalar::ONE, |m, r| m.max(r.abs().upper()));
722 let target = size.mul(Scalar::from_f64(RELATIVE_TARGET));
723 let functions = || {
724 (0..3)
725 .flat_map(move |col| (0..3).map(move |i| (col, i, self.columns[col][i])))
726 .filter(|(_, _, f)| !f.is_zero())
727 };
728 let mut spread = [Scalar::ZERO; 3];
732 for (col, i, f) in functions() {
733 spread[i] = spread[i].add(f.evaluate(t_range).width().mul(factor[col]));
734 }
735 let constant = spread.iter().all(|s| !s.upper().definitely_greater(target));
736 let degree = if constant {
737 0
738 } else {
739 self.interpolant_degree()
740 };
741 let mut error = [Scalar::ZERO; 3];
744 let mut polynomials = [[None, None, None], [None, None, None], [None, None, None]];
745 for (col, i, f) in functions() {
746 if constant {
747 polynomials[col][i] = Some(vec![f.evaluate(t_range)]);
748 continue;
749 }
750 let (coefficients, remainder) = f.interpolant(middle, half, degree)?;
751 error[i] = error[i].add(remainder.abs().mul(factor[col]));
752 polynomials[col][i] = Some(coefficients);
753 }
754 if error.iter().any(|e| e.upper().definitely_greater(target)) {
755 return Ok(None);
756 }
757 let scale = if constant {
760 Scalar::ONE
761 } else {
762 Scalar::ONE.div(half)?
763 };
764 let u =
765 Bernstein::combination([(Some(scale), &local[2]), (Some(middle.mul(scale).neg()), w)])?
766 .expect("two terms");
767 let basis: Vec<Bernstein> = (0..=degree)
768 .map(|n| u.power(n)?.product(&w.power(degree - n)?))
769 .collect::<GeopResult<_>>()?;
770 let composed = |coefficients: &Vec<Scalar>| -> GeopResult<Bernstein> {
771 let terms = (coefficients.iter().zip(&basis))
772 .filter(|(c, _)| !c.is_exactly(Scalar::ZERO))
773 .map(|(c, b)| ((!c.is_exactly(Scalar::ONE)).then_some(*c), b));
774 Ok(Bernstein::combination(terms)?.unwrap_or_else(|| Bernstein::constant(Scalar::ZERO)))
775 };
776 let factors = [&local[0], &local[1], w];
777 let numerators: Vec<Option<Bernstein>> = (0..3)
779 .map(|i| {
780 let products = (0..3)
781 .filter_map(|col| polynomials[col][i].as_ref().map(|c| (col, c)))
782 .map(|(col, c)| composed(c)?.product(factors[col]))
783 .collect::<GeopResult<Vec<_>>>()?;
784 Bernstein::combination(products.iter().map(|p| (None, p)))
785 })
786 .collect::<GeopResult<_>>()?;
787 let weight = basis[0].product(w)?;
788 let image: Vec<Bernstein> = (0..3)
790 .map(|k| {
791 let mut terms = Vec::new();
792 if !self.origin[k].is_exactly(Scalar::ZERO) {
793 terms.push((Some(self.origin[k]), &weight));
794 }
795 for (e, n) in self.frame.iter().zip(&numerators) {
796 if let Some(n) = n
797 && !e[k].is_exactly(Scalar::ZERO)
798 {
799 terms.push(((!e[k].is_exactly(Scalar::ONE)).then_some(e[k]), n));
800 }
801 }
802 Ok(Bernstein::combination(terms)?
803 .unwrap_or_else(|| Bernstein::constant(Scalar::ZERO)))
804 })
805 .collect::<GeopResult<_>>()?;
806 let degree = (image.iter().chain([&weight]).map(Bernstein::degree))
808 .fold([0, 0], |a, b| [a[0].max(b[0]), a[1].max(b[1])]);
809 let image: Vec<Bernstein> = (image.iter())
810 .map(|b| b.elevated(degree))
811 .collect::<GeopResult<_>>()?;
812 let weight = weight.elevated(degree)?;
813 let widen: [Scalar; 3] = std::array::from_fn(|k| {
817 let e = (self.frame.iter().zip(error))
818 .fold(Scalar::ZERO, |sum, (axis, e)| sum.add(axis[k].abs().mul(e)))
819 .upper()
820 .to_f64();
821 Scalar::new(-e, e)
822 });
823 let image: [Bernstein; 3] = std::array::from_fn(|k| {
824 if widen[k].is_exactly(Scalar::ZERO) {
825 return image[k].clone();
826 }
827 let coefficients = (image[k].coefficients().iter().zip(weight.coefficients()))
828 .map(|(c, w)| c.add(widen[k].mul(*w)))
829 .collect();
830 Bernstein::new(weight.degree(), coefficients)
831 });
832 let [x, y, z] = image;
833 Ok(Some([x, y, z, weight]))
834 }
835
836 fn check_axis(&self, x: Scalar) -> GeopResult<()> {
839 let cx = self.columns[2][0];
840 if !cx.is_trig() {
841 return Ok(());
842 }
843 let radius = cx.constant;
844 let reaches = if radius.definitely_greater(Scalar::ZERO) {
845 x.could_be_greater(radius) || x.could_be_equal(radius)
846 } else {
847 x.could_be_less(radius) || x.could_be_equal(radius)
848 };
849 if reaches {
850 return Err(GeopError::new(format!(
851 "AxialMap::bend: material reaches {x:?} towards the bend's centre, as far as its radius {radius:?} or past it, where it would be turned inside out"
852 )));
853 }
854 Ok(())
855 }
856
857 fn law_curve(&self, curve: &NurbCurve3D) -> GeopResult<NurbCurve3D> {
861 let mut todo: Vec<NurbCurve3D> = curve.bezier_pieces()?;
862 todo.reverse();
863 let mut done: Vec<(Scalar, [Bernstein; 4])> = Vec::new();
864 while let Some(piece) = todo.pop() {
865 let p = piece.degree;
866 let h: [Bernstein; 4] = std::array::from_fn(|k| {
867 Bernstein::new([p, 0], piece.control_points.iter().map(|c| c[k]).collect())
868 });
869 match self.law_piece(&h)? {
870 Some(image) => done.push((piece.domain().0, image)),
871 None => {
872 if done.len() + todo.len() + 2 > MAX_PIECES {
873 return Err(GeopError::new(format!(
874 "AxialMap: the curve needs more than {MAX_PIECES} pieces to be deformed within the target; its domain {:?}",
875 curve.domain()
876 )));
877 }
878 let (left, right) = piece.split_mid()?;
879 todo.push(right);
880 todo.push(left);
881 }
882 }
883 }
884 let degree = done[0].1[3].degree()[0];
885 let mut points: Vec<Vector4> = Vec::new();
886 let mut knots = vec![curve.domain().0; degree + 1];
887 for (i, (start, image)) in done.iter().enumerate() {
888 if i > 0 {
889 knots.extend(std::iter::repeat_n(*start, degree));
890 }
891 let piece: Vec<Vector4> = (0..=degree)
892 .map(|j| Vector4::from_array(std::array::from_fn(|k| image[k].coefficients()[j])))
893 .collect();
894 match points.last_mut() {
895 Some(last) => {
896 *last = last.union(&piece[0]);
897 points.extend_from_slice(&piece[1..]);
898 }
899 None => points.extend(piece),
900 }
901 }
902 knots.extend(std::iter::repeat_n(curve.domain().1, degree + 1));
903 NurbCurve3D::try_new(degree, points, knots)
904 }
905
906 fn law_surface(&self, surface: &NurbSurface3D) -> GeopResult<NurbSurface3D> {
911 let mut grid = [surface.breakpoints_u(), surface.breakpoints_v()];
912 loop {
913 let cells = (grid[0].len() - 1) * (grid[1].len() - 1);
914 if cells > MAX_PIECES {
915 return Err(GeopError::new(format!(
916 "AxialMap: the surface needs more than {MAX_PIECES} patches to be deformed within the target; its domain {:?} x {:?}",
917 surface.domain_u(),
918 surface.domain_v()
919 )));
920 }
921 let mut halve = [
922 vec![false; grid[0].len() - 1],
923 vec![false; grid[1].len() - 1],
924 ];
925 let mut images = Vec::with_capacity(cells);
926 for i in 0..grid[0].len() - 1 {
927 for j in 0..grid[1].len() - 1 {
928 let cell = surface
929 .sub_surface((grid[0][i], grid[0][i + 1]), (grid[1][j], grid[1][j + 1]))?;
930 let h: [Bernstein; 4] = std::array::from_fn(|k| {
931 Bernstein::new(
932 [cell.degree_u, cell.degree_v],
933 cell.control_points.iter().map(|c| c[k]).collect(),
934 )
935 });
936 match self.law_piece(&h)? {
937 Some(image) => images.push(image),
938 None => {
939 let [along_u, along_v] = self.runs_along_axis(&cell)?;
940 halve[0][i] |= along_u * 2.0 >= along_v;
943 halve[1][j] |= along_v * 2.0 >= along_u;
944 }
945 }
946 }
947 }
948 if images.len() == cells {
949 return assemble(&grid, images);
950 }
951 for (breaks, halve) in grid.iter_mut().zip(halve) {
952 let mut refined = vec![breaks[0]];
953 for (pair, halve) in breaks.windows(2).zip(halve) {
954 if halve {
955 refined.push(pair[0].add(pair[1]).div(Scalar::TWO)?.sharpen());
956 }
957 refined.push(pair[1]);
958 }
959 *breaks = refined;
960 }
961 }
962 }
963
964 fn runs_along_axis(&self, cell: &NurbSurface3D) -> GeopResult<[f64; 2]> {
968 let t: Vec<f64> = (cell.control_points.iter())
969 .map(|p| Ok(self.local(&p.dehomogenized()?)[2].to_f64()))
970 .collect::<GeopResult<_>>()?;
971 let (nu, nv) = (cell.num_u, cell.num_v);
972 let along_u = (0..nv)
973 .map(|j| {
974 (1..nu)
975 .map(|i| (t[i * nv + j] - t[(i - 1) * nv + j]).abs())
976 .sum::<f64>()
977 })
978 .fold(0.0, f64::max);
979 let along_v = (0..nu)
980 .map(|i| {
981 (1..nv)
982 .map(|j| (t[i * nv + j] - t[i * nv + j - 1]).abs())
983 .sum::<f64>()
984 })
985 .fold(0.0, f64::max);
986 Ok([along_u, along_v])
987 }
988}
989
990fn assemble(grid: &[Vec<Scalar>; 2], images: Vec<[Bernstein; 4]>) -> GeopResult<NurbSurface3D> {
995 let [du, dv] = images[0][3].degree();
996 let (cu, cv) = (grid[0].len() - 1, grid[1].len() - 1);
997 let (nu, nv) = (cu * du + 1, cv * dv + 1);
998 let mut points: Vec<Option<Vector4>> = vec![None; nu * nv];
999 for (index, image) in images.iter().enumerate() {
1000 let (ci, cj) = (index / cv, index % cv);
1001 for a in 0..=du {
1002 for b in 0..=dv {
1003 let p = Vector4::from_array(std::array::from_fn(|k| {
1004 image[k].coefficients()[a * (dv + 1) + b]
1005 }));
1006 let slot = &mut points[(ci * du + a) * nv + cj * dv + b];
1007 *slot = Some(match slot {
1008 None => p,
1009 Some(q) => q.union(&p),
1010 });
1011 }
1012 }
1013 }
1014 let knots = |breaks: &[Scalar], degree: usize| {
1015 let mut knots = vec![breaks[0]; degree + 1];
1016 for &k in &breaks[1..breaks.len() - 1] {
1017 knots.extend(std::iter::repeat_n(k, degree));
1018 }
1019 knots.extend(std::iter::repeat_n(breaks[breaks.len() - 1], degree + 1));
1020 knots
1021 };
1022 NurbSurface3D::try_new(
1023 du,
1024 dv,
1025 points
1026 .into_iter()
1027 .map(|p| p.expect("every control point set"))
1028 .collect(),
1029 knots(&grid[0], du),
1030 knots(&grid[1], dv),
1031 )
1032}
1033
1034impl Deformation for AxialMap {
1035 fn point(&self, p: &Vector3) -> GeopResult<Vector3> {
1039 let local = self.local(p);
1040 let mut image: Option<Vector3> = None;
1041 for piece in self.pieces(local[2]) {
1042 let q = match piece {
1043 Piece::Before => self.tangent_motion(self.start)?.apply(p),
1044 Piece::After => self.tangent_motion(self.end)?.apply(p),
1045 Piece::Law => {
1046 self.check_axis(local[0])?;
1047 self.law_point(local)
1048 }
1049 };
1050 image = Some(image.map_or(q, |i| i.union(&q)));
1051 }
1052 Ok(image.expect("every t falls in some piece"))
1053 }
1054
1055 fn curve(
1056 &self,
1057 curve: &NurbCurve3D,
1058 at: &dyn Fn() -> GeopResult<Vector3>,
1059 ) -> GeopResult<NurbCurve3D> {
1060 match self.piece_of(&curve.control_points, at)? {
1061 Piece::Before => Ok(curve.transform(&self.tangent_motion(self.start)?)),
1062 Piece::After => Ok(curve.transform(&self.tangent_motion(self.end)?)),
1063 Piece::Law => self.law_curve(curve),
1064 }
1065 }
1066
1067 fn surface(
1068 &self,
1069 surface: &NurbSurface3D,
1070 at: &dyn Fn() -> GeopResult<Vector3>,
1071 ) -> GeopResult<NurbSurface3D> {
1072 match self.piece_of(&surface.control_points, at)? {
1073 Piece::Before => Ok(surface.transform(&self.tangent_motion(self.start)?)),
1074 Piece::After => Ok(surface.transform(&self.tangent_motion(self.end)?)),
1075 Piece::Law => self.law_surface(surface),
1076 }
1077 }
1078
1079 fn mirrors(&self) -> bool {
1081 false
1082 }
1083
1084 fn affine(&self) -> bool {
1085 false
1086 }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use crate::test_fixtures::line;
1093
1094 fn v(x: f64, y: f64, z: f64) -> Vector3 {
1095 Vector3::from_array([x, y, z].map(Scalar::from_f64))
1096 }
1097
1098 fn zone(a: f64, b: f64) -> (Scalar, Scalar) {
1099 (Scalar::from_f64(a), Scalar::from_f64(b))
1100 }
1101
1102 fn slanted() -> NurbSurface3D {
1104 NurbSurface3D::bilinear(
1105 v(0.2, -0.3, 0.0),
1106 v(1.0, 0.1, 0.2),
1107 v(0.8, 0.5, 2.0),
1108 v(-0.1, 0.4, 1.7),
1109 )
1110 .unwrap()
1111 }
1112
1113 fn none() -> GeopResult<Vector3> {
1114 Err(GeopError::new("no point asked for"))
1115 }
1116
1117 fn assert_maps_surface(map: &AxialMap, surface: &NurbSurface3D) -> NurbSurface3D {
1120 let image = map.surface(surface, &none).unwrap();
1121 let (u0, u1) = surface.domain_u();
1122 let (v0, v1) = surface.domain_v();
1123 for i in 0..=6 {
1124 for j in 0..=6 {
1125 let u = Scalar::interpolate(u0, u1, Scalar::from_f64(i as f64 / 6.0)).sharpen();
1126 let w = Scalar::interpolate(v0, v1, Scalar::from_f64(j as f64 / 6.0)).sharpen();
1127 let expected = map.point(&surface.evaluate(u, w).unwrap()).unwrap();
1128 let got = image.evaluate(u, w).unwrap();
1129 assert!(
1130 got.could_be_equal(&expected),
1131 "at ({u:?}, {w:?}): {got:?} vs {expected:?}"
1132 );
1133 for k in 0..3 {
1134 assert!(got[k].width().to_f64() < 1e-7, "{got:?}");
1135 }
1136 }
1137 }
1138 image
1139 }
1140
1141 #[test]
1144 fn a_taper_is_exact_and_quadratic() {
1145 let map = AxialMap::taper(
1146 v(0.0, 0.0, 0.0),
1147 &v(0.0, 0.0, 1.0),
1148 None,
1149 zone(0.0, 2.0),
1150 Scalar::from_f64(0.5),
1151 )
1152 .unwrap();
1153 let surface = slanted();
1154 let image = assert_maps_surface(&map, &surface);
1155 assert_eq!((image.degree_u, image.degree_v), (2, 2));
1156 assert!(map.as_motion().unwrap().is_none());
1157 }
1158
1159 #[test]
1163 fn a_pinch_scales_one_way_across() {
1164 let map = AxialMap::taper(
1165 v(0.0, 0.0, 0.0),
1166 &v(0.0, 0.0, 1.0),
1167 Some(&v(1.0, 0.0, 0.0)),
1168 zone(0.0, 2.0),
1169 Scalar::from_f64(0.5),
1170 )
1171 .unwrap();
1172 let p = map.point(&v(1.0, 1.0, 1.0)).unwrap();
1173 assert!(p.could_be_equal(&v(0.75, 1.0, 1.0)), "{p:?}");
1174 assert_maps_surface(&map, &slanted());
1175 }
1176
1177 #[test]
1181 fn a_shear_is_a_motion() {
1182 let map = AxialMap::shear(
1183 v(0.0, 0.0, 0.0),
1184 &v(0.0, 0.0, 1.0),
1185 &v(0.0, 1.0, 0.0),
1186 zone(1.0, 3.0),
1187 Scalar::from_f64(0.5),
1188 )
1189 .unwrap();
1190 let motion = map.as_motion().unwrap().expect("a shear is affine");
1191 for (p, q) in [
1192 (v(1.0, 0.0, 0.0), v(1.0, -0.25, 0.0)),
1193 (v(0.0, 2.0, 2.0), v(0.0, 2.25, 2.0)),
1194 (v(0.5, 0.0, 5.0), v(0.5, 1.0, 5.0)),
1195 ] {
1196 assert!(motion.apply(&p).could_be_equal(&q), "{p:?}");
1197 assert!(map.point(&p).unwrap().could_be_equal(&q), "{p:?}");
1198 }
1199 assert!(!motion.mirrors());
1200 }
1201
1202 #[test]
1205 fn a_twist_and_a_bend_enclose_the_true_image() {
1206 let surface = slanted();
1207 let twist = AxialMap::twist(
1208 v(0.1, 0.0, 0.0),
1209 &v(0.0, 0.0, 1.0),
1210 zone(0.0, 2.0),
1211 Scalar::from_f64(1.5),
1212 )
1213 .unwrap();
1214 assert_maps_surface(&twist, &surface);
1215 let bend = AxialMap::bend(
1216 v(0.0, 0.0, 0.0),
1217 &v(0.0, 0.0, 1.0),
1218 &v(-1.0, 0.0, 0.0),
1219 zone(0.0, 2.0),
1220 Scalar::from_f64(3.0),
1221 )
1222 .unwrap();
1223 assert_maps_surface(&bend, &surface);
1224 }
1225
1226 #[test]
1229 fn a_line_along_the_axis_bends_into_an_arc() {
1230 let bend = AxialMap::bend(
1231 v(0.0, 0.0, 0.0),
1232 &v(0.0, 0.0, 1.0),
1233 &v(1.0, 0.0, 0.0),
1234 zone(0.0, 1.0),
1235 Scalar::PI.div(Scalar::TWO).unwrap(),
1236 )
1237 .unwrap();
1238 let curve = line([0.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
1239 let image = bend.curve(&curve, &none).unwrap();
1240 let radius = 2.0 / std::f64::consts::PI;
1241 let end = image.evaluate(image.domain().1).unwrap();
1242 assert!(
1243 end.could_be_equal(&v(radius, 0.0, radius)) || (end[0].to_f64() - radius).abs() < 1e-9,
1244 "{end:?}"
1245 );
1246 let mid = image.evaluate(image.mid_parameter().unwrap()).unwrap();
1247 let r = ((mid[0].to_f64() - radius).powi(2) + mid[2].to_f64().powi(2)).sqrt();
1248 assert!((r - radius).abs() < 1e-9, "{mid:?}");
1249 }
1250
1251 #[test]
1254 fn past_the_zone_a_twist_turns_rigidly() {
1255 let twist = AxialMap::twist(
1256 v(0.0, 0.0, 0.0),
1257 &v(0.0, 0.0, 1.0),
1258 zone(0.0, 1.0),
1259 Scalar::PI.div(Scalar::TWO).unwrap(),
1260 )
1261 .unwrap();
1262 let p = twist.point(&v(1.0, 0.0, 3.0)).unwrap();
1263 assert!(
1264 p.could_be_equal(&v(0.0, 1.0, 3.0)) || (p[1].to_f64() - 1.0).abs() < 1e-12,
1265 "{p:?}"
1266 );
1267 assert!(
1268 twist
1269 .point(&v(1.0, 0.0, -2.0))
1270 .unwrap()
1271 .could_be_equal(&v(1.0, 0.0, -2.0))
1272 );
1273 let q = twist.point(&v(0.0, 2.0, 1.0)).unwrap();
1275 assert!(q.could_be_equal(&v(-2.0, 0.0, 1.0)), "{q:?}");
1276 }
1277
1278 #[test]
1282 fn the_interpolant_of_a_cosine_is_within_its_bound() {
1283 let one = Scalar::ONE;
1284 let f = AxialFunction::trig(Scalar::ZERO, one, Scalar::ZERO, one, Scalar::ZERO);
1285 let (middle, reach) = (Scalar::from_f64(0.3), Scalar::from_f64(0.1));
1286 let (coefficients, bound) = f.interpolant(middle, reach, 3).unwrap();
1287 let bound = bound.upper().to_f64();
1288 assert!(bound < 4.0 * 1e-4 / 192.0, "{bound}");
1289 for k in 0..=20 {
1290 let s = -0.1 + 0.01 * k as f64;
1291 let p: f64 =
1292 (coefficients.iter().rev()).fold(0.0, |acc, c| acc * (s / 0.1) + c.to_f64());
1293 assert!(
1294 (p - (0.3 + s).cos()).abs() <= bound,
1295 "at {s}: {p} vs {}",
1296 (0.3 + s).cos()
1297 );
1298 }
1299 }
1300
1301 #[test]
1304 fn a_full_turn_and_material_past_the_centre_are_refused() {
1305 let full = AxialMap::bend(
1306 v(0.0, 0.0, 0.0),
1307 &v(0.0, 0.0, 1.0),
1308 &v(1.0, 0.0, 0.0),
1309 zone(0.0, 1.0),
1310 Scalar::from_f64(7.0),
1311 );
1312 assert!(full.is_err());
1313 let bend = AxialMap::bend(
1315 v(0.0, 0.0, 0.0),
1316 &v(0.0, 0.0, 1.0),
1317 &v(1.0, 0.0, 0.0),
1318 zone(0.0, 1.0),
1319 Scalar::from_f64(3.0),
1320 )
1321 .unwrap();
1322 let error = bend.point(&v(0.5, 0.0, 0.5)).unwrap_err();
1323 assert!(format!("{error}").contains("radius"), "{error}");
1324 }
1325}