Skip to main content

geop_core_geometry/
deform.rs

1//! Maps of space applied to geometry: a rigid motion, a mirror or a
2//! scaling ([`Motion`]), and the deformations of a body about an axis —
3//! tapering (or pinching, one way across the axis), shearing, twisting and
4//! bending ([`AxialMap`]).
5//!
6//! # One mechanism: composition on the control points
7//!
8//! In a frame whose third axis is the deformation's axis, an axial map
9//! takes the point `(x, y, t)` to `x a(t) + y b(t) + c(t)`: affine in `x`
10//! and `y`, with coefficients that are functions of `t` alone. A taper
11//! scales `x` and `y` by a factor linear in `t`; a twist turns them by an
12//! angle linear in `t`; a shear moves them across by an offset linear in
13//! `t`; a bend carries the plane `t = const` round a
14//! circle, `x` measured towards its centre. Each coefficient is
15//! `constant + slope s + cos·cos(rate s) + sin·sin(rate s)` with `s` the
16//! distance along the axis from the zone's start ([`AxialFunction`]).
17//!
18//! On a Bézier piece of a NURBS, `x`, `y` and `t` are polynomials over the
19//! weight `W`. A coefficient that is a polynomial of degree `d` in `t`,
20//! written about the piece's middle `tc` as `Σ f_n (t - tc)^n`, composes
21//! into `Σ f_n U^n W^(d-n) / W^d` with `U = T - tc W`: so the image of the
22//! piece is the rational Bézier piece of numerator `Σ_i e_i (Â_i x̂ + B̂_i
23//! ŷ + Ĉ_i W)` and weight `W^(d+1)`, of `d + 1` times the degree — computed
24//! from the control points by products of Bernstein polynomials
25//! ([`crate::bernstein`]), no evaluation, no fitting. The parametrization
26//! is kept, so every pcurve on a deformed face still traces it.
27//!
28//! A taper's coefficients are linear: the composition is exact. A twist's
29//! and a bend's are trigonometric: each is replaced by its cubic
30//! interpolant at Chebyshev points over the piece's reach along the axis,
31//! whose error, `(|cos| + |sin|) |rate|^4 / 4! · max |Π (τ - τ_j)|`, is a
32//! proven bound; times what `x` and `y` reach over the piece, it bounds how
33//! far the composed piece can be from the true image, and every control
34//! point is widened by it (a polynomial piece stays polynomial, its weights
35//! exactly one). A piece whose bound is above the target is halved (along
36//! the direction it runs most along the axis) until it is below, and a
37//! curve or surface that would need more than `MAX_PIECES` pieces is
38//! refused by name. The pieces are joined back at knots of full
39//! multiplicity, the shared control points the union of the two
40//! enclosures.
41//!
42//! # Zones
43//!
44//! The law holds over the zone `[start, end]` along the axis. Before and
45//! after it the map continues as the affine map it is tangent to at the
46//! zone's end ([`Motion::affine`]): rigidly for a twist or a bend, as a
47//! constant scaling across the axis for a taper. So a body is first divided
48//! where it crosses the zone's ends (the operation imprints those planes),
49//! and each piece of it is moved by the piece of the map it lies in. The
50//! two agree on the plane they share.
51
52use 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
61/// A map of space as it moves geometry: points, curves and surfaces, each
62/// keeping its parametrization, so a pcurve on a moved face still traces
63/// it. `at` gives a point of the curve or of the face the surface bounds,
64/// asked for only by a map made of pieces where the geometry's control
65/// points straddle two of them: the piece that holds `at` moves it.
66pub trait Deformation {
67    /// The point `p` moved.
68    fn point(&self, p: &Vector3) -> GeopResult<Vector3>;
69    /// The curve `curve` moved.
70    fn curve(
71        &self,
72        curve: &NurbCurve3D,
73        at: &dyn Fn() -> GeopResult<Vector3>,
74    ) -> GeopResult<NurbCurve3D>;
75    /// The surface `surface` moved.
76    fn surface(
77        &self,
78        surface: &NurbSurface3D,
79        at: &dyn Fn() -> GeopResult<Vector3>,
80    ) -> GeopResult<NurbSurface3D>;
81    /// Whether it turns the side a surface's normal points to: a mirror.
82    fn mirrors(&self) -> bool;
83    /// Whether it is affine, mapping control points to control points.
84    /// One that is not costs by the size of what it maps, so a face's
85    /// surface is first cut down to what the face uses of it.
86    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/// `constant + slope s + cos·cos(rate s) + sin·sin(rate s)`, with `s = t -
120/// start`: a coefficient of an [`AxialMap`] as a function of the
121/// coordinate `t` along its axis.
122#[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    /// `constant + slope (t - start)`.
143    fn linear(constant: Scalar, slope: Scalar, start: Scalar) -> Self {
144        AxialFunction {
145            constant,
146            slope,
147            start,
148            ..Self::ZERO
149        }
150    }
151
152    /// `constant + cos·cos(rate s) + sin·sin(rate s)`, `s = t - start`.
153    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    /// Whether it has a trigonometric part: a polynomial of no degree.
171    fn is_trig(&self) -> bool {
172        !(self.cos.is_exactly(Scalar::ZERO) && self.sin.is_exactly(Scalar::ZERO))
173    }
174
175    /// The value at `t`, an enclosure for an interval `t`.
176    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    /// The derivative along the axis.
192    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    /// A polynomial of degree `degree` (at least one) standing in for the
204    /// function within `reach` of `middle`, as its coefficients of powers
205    /// of `s = (t - middle) / reach` (of nothing but the constant where
206    /// `reach` is zero), and an enclosure of how far the function can be
207    /// from it there. The linear part is itself: exact. The trigonometric
208    /// one is interpolated at Chebyshev points `s_j`, whose error is
209    /// bounded by `max |d^(d+1) f / ds^(d+1)| / (d+1)! · max |Π (s -
210    /// s_j)|` (the remainder of interpolation, for any nodes): the
211    /// derivative by `(|cos| + |sin|) (|rate| reach)^(d+1)`, the node
212    /// polynomial by the largest of its Bernstein coefficients over pieces
213    /// of `[-1, 1]`. The nodes are a free choice, so they are sharp. In `s`
214    /// nothing is divided by the reach, which may be as small as rounding.
215    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        // Newton's divided differences, then its form multiplied out.
238        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            // coefficients · (s - s_j) + differences[j].
250            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        // The node polynomial's largest value over [-1, 1]: the largest
259        // Bernstein coefficient over each of a few pieces of it (over the
260        // whole, they stand far above its oscillation).
261        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/// Which piece of an [`AxialMap`] moves a point: the affine map before the
283/// zone, the law over it, the affine map after it.
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285enum Piece {
286    Before,
287    Law,
288    After,
289}
290
291/// The most pieces one curve or surface is cut into to bring its image
292/// within the target: past it, the map is refused for that geometry.
293const MAX_PIECES: usize = 4096;
294
295/// How close to the true image a composed piece has to be, relative to
296/// its size and its distance from the axis: a target for how finely to
297/// cut, not a tolerance any answer relies on — whatever the bound comes to
298/// widens the piece.
299const RELATIVE_TARGET: f64 = 1e-9;
300
301/// A taper, a twist or a bend about an axis (see the module's doc): in the
302/// frame `frame` at `origin` — `x`, `y` and the axis, orthonormal and
303/// right-handed — the point `(x, y, t)` goes to `x a(t) + y b(t) + c(t)`
304/// over the zone `start ≤ t ≤ end`, `columns` being `[a, b, c]` by their
305/// components in the frame. Before and after the zone, the affine map the
306/// law is tangent to at its end.
307#[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    /// The frame `x`, `y`, `axis` , `x` along `toward` (square
318    /// to the axis) or, without it, chosen by [`Vector3::across`]. Fails
319    /// for an axis of no length, a `toward` along it, or a zone that does
320    /// not run definitely forward.
321    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        // A unit axis is kept as it is: dividing by a norm of exactly one
332        // would only widen it.
333        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    /// Tapering about the axis through `origin` along `axis`: across the
357    /// axis, scaled by one at `start` and by `factor` at `end`, linearly in
358    /// between, and by `factor` past the end — or, pinching, only along
359    /// `toward` (square to the axis), the other way across left as it is.
360    /// Exact. Fails for a factor that is not definitely positive.
361    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    /// Shearing along the axis through `origin` along `axis`: each plane
396    /// across the axis moved towards `toward` (square to the axis) by
397    /// `offset` times its distance from `start` over the zone's length.
398    /// Affine, so the same map before, over and past the zone: the zone
399    /// only sets the rate ([`AxialMap::as_motion`]).
400    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    /// The affine map it is, where it is one everywhere — a shear: its
422    /// columns across the axis constant and its offset linear along it, so
423    /// the law is the map it is tangent to at either end. None for a taper,
424    /// a twist or a bend.
425    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    /// Twisting about the axis through `origin` along `axis`: turned by
438    /// nothing at `start` and by `angle` (radians, right-handed about the
439    /// axis) at `end`, uniformly in between, rigidly by `angle` past the
440    /// end. Either hand.
441    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    /// Bending about an axis square to the line through `origin` along
465    /// `axis`: the stretch of that line from `start` to `end` is wound onto
466    /// a circular arc of `angle` (radians), curving towards `toward`, its
467    /// length kept — the radius is the zone's length over the angle. The
468    /// plane square to the line at each point stays a plane, turned with
469    /// it; past the end everything is carried on rigidly. A negative angle
470    /// curves away from `toward`, an angle of nothing leaves everything
471    /// where it is. Fails for a turn of a whole circle or more, which would
472    /// lay the zone over itself.
473    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            // c = (R (1 - cos), 0, start + R sin).
498            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    /// Where the zone starts and ends along the axis.
512    pub fn zone(&self) -> (Scalar, Scalar) {
513        (self.start, self.end)
514    }
515
516    /// How far along the axis `p` lies: its coordinate `t`.
517    pub fn along(&self, p: &Vector3) -> Scalar {
518        self.local(p)[2]
519    }
520
521    /// The square of the plane across the axis at `t` along it, reaching
522    /// `reach` from the axis each way: what divides a body where it crosses
523    /// from one piece of the map into the next.
524    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    /// The coordinates of `p` in the frame: `(x, y, t)`.
542    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    /// The point with coordinates `local` in the frame, relative to the
548    /// origin: `Σ local_i e_i`.
549    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    /// The pieces of the map a coordinate `t` along the axis could fall in.
557    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    /// The affine map the law is tangent to at `t`, a zone's end:
572    /// `x a(t) + y b(t) + c(t) + (s - t) c'(t)` for the point at `s` along
573    /// the axis.
574    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        // Column k of the linear part: the image of the world's axis k,
584        // whose coordinates in the frame are (ex[k], ey[k], et[k]).
585        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    /// The law at the point with coordinates `(x, y, t)`, `t` in the zone.
602    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    /// The degree of the polynomials the law's coefficients are replaced
618    /// by: one where all are linear (then exactly); three where one is
619    /// trigonometric, so a plane's image is of degree four, whose moments
620    /// are still integrated exactly (`Model::mass_properties`).
621    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    /// Which piece moves geometry whose control points are `points`: the
630    /// one their coordinates along the axis fall in, or, where they
631    /// straddle a zone's end, the one `at` falls in — the law where that is
632    /// on the end itself, where the law and the affine map agree.
633    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    /// The piece that holds every one of `t`, coordinates along the axis,
649    /// up to its ends: the law where each could be within the zone, its
650    /// ends included, where the law and the affine maps agree; an affine
651    /// map where each could be on that one's side. None where they reach
652    /// definitely into two. Each coordinate on its own, so a point on an
653    /// end that rounding has widened past it still counts as on it.
654    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    /// The law applied to one Bézier piece, given by the Bernstein
668    /// polynomials of its homogeneous coordinates `(X, Y, Z, W)`: the
669    /// image's, its control points widened by the bound on how far it can
670    /// be from the true image. `None` where that bound is above the target:
671    /// the piece has to be cut smaller. Fails where the piece reaches the
672    /// axis a bend winds round.
673    fn law_piece(&self, h: &[Bernstein; 4]) -> GeopResult<Option<[Bernstein; 4]>> {
674        // A polynomial piece, its weights all exactly one, has a polynomial
675        // image, its weights exactly one too, rather than products of ones
676        // rounded outward into a rational piece.
677        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        // Coordinates relative to the origin, then in the frame.
681        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        // What x, y and t reach over the piece (the convex hull property:
705        // the weights are positive).
706        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        // Over a piece short enough along the axis (a face across it), each
729        // coefficient is enclosed by its value over the whole piece: a
730        // constant, the image of the degree of the piece, nothing to bound.
731        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        // Each coefficient's polynomial in `s = (t - middle) / half`, and
742        // the bound on how far the composed point can be off, in the frame.
743        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        // s^n W^(d-n) for n = 0..=d, as `S = (T - middle W) / half`; a
758        // piece with no reach along the axis is a constant one.
759        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        // N_i = Â_i x + B̂_i y + Ĉ_i W, in the frame.
778        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        // Back in the world: origin W' + Σ_i e_i N_i.
789        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        // All of one degree: the numerators', or the weight's if higher.
807        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        // Each control point widened by what the error reaches along each
814        // of the world's axes: a convex combination of points each within
815        // it of where it should be is within it too.
816        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    /// Fails where `x`, the coordinate towards a bend's centre, could reach
837    /// the axis the bend winds round: the radius and past it.
838    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    /// The law applied to `curve`: each Bézier piece, halved while its
858    /// image is not within the target, joined back at knots of full
859    /// multiplicity at the parameters they were cut at.
860    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    /// The law applied to `surface`: its Bézier patches, on a grid refined
907    /// (each cell halved along the direction it runs further along the
908    /// axis) until every cell's image is within the target, joined back at
909    /// knots of full multiplicity where the grid's lines were.
910    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                            // Both where neither runs twice as far as the
941                            // other along the axis.
942                            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    /// How far a patch's control net runs along the axis along `u` and
965    /// along `v`: the largest sum of the steps along a row of it. Only
966    /// chooses which way to cut a patch, a free choice.
967    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
990/// The patches `images` of the cells of `grid` over `surface`'s domain
991/// (`u`-index major), joined into one surface: knots of full multiplicity
992/// at the grid's lines, the control points two cells share the union of
993/// theirs. The parametrization is the surface's.
994fn 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    /// The image of every piece `p` could lie in, united: where `p` could
1036    /// be on a zone's end, the law and the affine map there both enclose
1037    /// it.
1038    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    /// A positive taper, a twist and a bend all keep a frame right-handed.
1080    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    /// A patch slanted across the axis `z`, from `z = 0` to `z = 2`.
1103    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    /// The image of every point of a surface is where the map puts it:
1118    /// sampled on a grid, each composed point encloses the map's own.
1119    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    /// A taper of a plane through the axis's zone is exact: of degree two,
1142    /// every control point a few roundings wide.
1143    #[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    /// A pinch scales only the way across it is given: a point at `(1, 1,
1160    /// 1)`, halfway along a zone pinching `x` to a half, goes to `(0.75, 1,
1161    /// 1)`. Exact, as a taper is.
1162    #[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    /// A shear is affine: the motion it is, the same before, over and past
1178    /// the zone, moving a plane across the axis by the offset times its
1179    /// distance from the zone's start over the zone's length.
1180    #[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    /// A twist of a quarter turn and a bend of a half turn of a slanted
1203    /// patch enclose the true image to well within its width.
1204    #[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    /// A line along the axis, bent: an arc of the bend's radius, its ends
1227    /// where the bend's law puts them.
1228    #[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    /// Before and after the zone the map is affine and continues the law:
1252    /// a twist carries everything past its end turned by its angle.
1253    #[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        // On the end itself both pieces enclose the point.
1274        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    /// The cubic standing in for `cos` over a reach of 0.1 is within its
1279    /// bound of it, and the bound within a few times the true error,
1280    /// `0.1^4 / (8 · 4!)` at most.
1281    #[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    /// A bend of a whole turn, and one whose axis the body reaches, are
1302    /// refused.
1303    #[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        // Radius 1 / 3: a point at x = 0.5 lies past the centre.
1314        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}