Skip to main content

geop_ops_sheetmetal/
fold.rs

1//! The solid sheet of a [`Layout`]: [`Sheet::folded`], every face in its
2//! place — a flat where it lies, a bend wrapped round its axis — and
3//! [`Sheet::unfolded`], every face where the layout has it.
4//!
5//! A flat's edges are its own curves placed in space, exactly. A bend's
6//! edges along its axis are lines, its edges across it arcs, exact too; a
7//! cut's curve that runs across a bend any other way is wrapped round it,
8//! which no NURBS curve is exactly, and is enclosed: a spline through
9//! points of it, widened to take in the rest (see
10//! [`NurbCurve3D::enclosing_pad`]).
11
12use std::collections::BTreeMap;
13
14use geop_core_geometry::{
15    nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D, true_point_fractions},
16    nurb_surface::NurbSurface3D,
17};
18use geop_core_math::{
19    geop_error::{GeopError, GeopResult, WithContext},
20    scalars::Scalar,
21    vector::{Vector2, Vector3, Vector4},
22    with_context,
23};
24use geop_core_topology::Sense;
25
26use crate::{
27    layout::{Home, Layout, LayoutEdge, Seg},
28    sheet::{BendFrame, Placement, Sheet},
29    thicken::{SheetCoedge, SheetEdge, SheetFace, SheetSurface, SheetVertex},
30};
31
32/// How many intervals a wrapped curve is sampled in: bounds how wide its
33/// enclosure comes out, not whether it encloses (see
34/// [`NurbCurve3D::enclosing_pad`]).
35const SAMPLES: usize = 32;
36
37/// A vertex's two points, on the A side and the B side.
38type Pair = (Vector3, Vector3);
39
40impl Sheet {
41    /// The A side of the sheet as it is bent, with its B side.
42    pub fn folded(&self) -> GeopResult<SheetSurface> {
43        self.surface(&self.layout()?, true)
44    }
45
46    /// The A side of the sheet laid flat, with its B side: every face in
47    /// the first flat's plane where the layout has it (see
48    /// [`crate::layout`]), every bend a flat strip as wide as its
49    /// developed length.
50    pub fn unfolded(&self) -> GeopResult<SheetSurface> {
51        self.surface(&self.layout()?, false)
52    }
53
54    /// The faces of `layout` folded into place, or laid flat.
55    fn surface(&self, layout: &Layout, folded: bool) -> GeopResult<SheetSurface> {
56        let t = Scalar::from_f64(self.rules.thickness);
57        let shifts = self.flat_shifts()?;
58        let used = layout.used_edges();
59        let mut vertex_ids: Vec<usize> = used
60            .iter()
61            .flat_map(|&e| [layout.edges[e].start, layout.edges[e].end])
62            .collect();
63        vertex_ids.sort_unstable();
64        vertex_ids.dedup();
65        let vindex: BTreeMap<usize, usize> = vertex_ids
66            .iter()
67            .enumerate()
68            .map(|(i, &v)| (v, i))
69            .collect();
70        let eindex: BTreeMap<usize, usize> =
71            used.iter().enumerate().map(|(i, &e)| (e, i)).collect();
72        let root = &self.flats[0].place;
73        let wraps: Vec<Wrap> = if folded {
74            (0..self.bends.len())
75                .map(|b| Wrap::new(self, b, layout))
76                .collect::<GeopResult<_>>()?
77        } else {
78            Vec::new()
79        };
80
81        // A vertex is folded with a flat it lies on, if any; else with its
82        // bend.
83        let mut home: BTreeMap<usize, Home> = BTreeMap::new();
84        for &e in &used {
85            let edge = &layout.edges[e];
86            for v in [edge.start, edge.end] {
87                if !matches!(home.get(&v), Some(Home::Flat(_))) {
88                    home.insert(v, edge.home);
89                }
90            }
91        }
92        let mut points: BTreeMap<usize, Pair> = BTreeMap::new();
93        let mut sheet = SheetSurface::default();
94        for &v in &vertex_ids {
95            let vertex = &layout.vertices[v];
96            let pair = if !folded {
97                (root.point(&vertex.at), root.offset(t).point(&vertex.at))
98            } else if let Some((f, own)) = &vertex.own {
99                let place = &self.flats[*f].place;
100                (place.point(own), place.offset(t).point(own))
101            } else {
102                match home[&v] {
103                    Home::Flat(f) => {
104                        let own = vertex.at.sub(&shifts[f]);
105                        let place = &self.flats[f].place;
106                        (place.point(&own), place.offset(t).point(&own))
107                    }
108                    Home::Bend(b) => wraps[b].pair(&vertex.at)?,
109                }
110            };
111            points.insert(v, pair);
112            sheet.vertices.push(SheetVertex {
113                a: pair.0,
114                b: pair.1,
115                name: vertex.name.clone().ok_or_else(|| {
116                    GeopError::new(format!(
117                        "the layout's vertex at {:?} has no name",
118                        vertex.at
119                    ))
120                })?,
121            });
122        }
123
124        // An edge in its flat's own coordinates.
125        let own = |edge: &LayoutEdge, f: usize| -> GeopResult<NurbCurve2D> {
126            match &edge.own {
127                Some(curve) => Ok(curve.clone()),
128                None => Ok(edge.curve.translate(shifts[f].neg())),
129            }
130        };
131        for &e in &used {
132            let edge = &layout.edges[e];
133            let ctx = with_context!("edge {}", edge.name.root());
134            let (a, b) = if !folded {
135                (root.curve(&edge.curve)?, root.offset(t).curve(&edge.curve)?)
136            } else {
137                match edge.home {
138                    Home::Flat(f) => {
139                        let curve = own(edge, f).with_context(ctx)?;
140                        let place = &self.flats[f].place;
141                        (place.curve(&curve)?, place.offset(t).curve(&curve)?)
142                    }
143                    Home::Bend(b) => wraps[b]
144                        .edge(edge, &points[&edge.start], &points[&edge.end])
145                        .with_context(ctx)?,
146                }
147            };
148            sheet.edges.push(SheetEdge {
149                a,
150                b,
151                start: vindex[&edge.start],
152                end: vindex[&edge.end],
153                name: edge.name.clone(),
154            });
155        }
156
157        for face in &layout.faces {
158            let ctx = with_context!("face {}", face.name.root());
159            let surfaces = match (folded, face.home) {
160                (true, Home::Bend(b)) => {
161                    let wrap = &wraps[b];
162                    let uv = wrap.vertex_uv(face, layout).with_context(ctx)?;
163                    let coedges = |lp: &Vec<(usize, Sense)>| -> GeopResult<Vec<SheetCoedge>> {
164                        lp.iter()
165                            .map(|&(e, sense)| {
166                                Ok(SheetCoedge {
167                                    edge: eindex[&e],
168                                    sense,
169                                    pcurve: wrap.pcurve(&layout.edges[e], sense, &uv)?,
170                                })
171                            })
172                            .collect()
173                    };
174                    let outer = coedges(&face.outer).with_context(ctx)?;
175                    let holes = face
176                        .holes
177                        .iter()
178                        .map(coedges)
179                        .collect::<GeopResult<_>>()
180                        .with_context(ctx)?;
181                    sheet.faces.push(SheetFace {
182                        a: wrap.strips.0.clone(),
183                        b: wrap.strips.1.clone(),
184                        outer,
185                        holes,
186                        name: face.name.clone(),
187                    });
188                    continue;
189                }
190                (true, Home::Flat(f)) => Some(f),
191                (false, _) => None,
192            };
193            // A plane, parametrized by the coordinates its curves are in.
194            let place: &Placement = match surfaces {
195                Some(f) => &self.flats[f].place,
196                None => root,
197            };
198            let curve = |e: usize| -> GeopResult<NurbCurve2D> {
199                match surfaces {
200                    Some(f) => own(&layout.edges[e], f),
201                    None => Ok(layout.edges[e].curve.clone()),
202                }
203            };
204            let coedges = |lp: &Vec<(usize, Sense)>| -> GeopResult<Vec<SheetCoedge>> {
205                lp.iter()
206                    .map(|&(e, sense)| {
207                        let c = curve(e)?;
208                        Ok(SheetCoedge {
209                            edge: eindex[&e],
210                            sense,
211                            pcurve: match sense {
212                                Sense::Forward => c,
213                                Sense::Reversed => c.reverse(),
214                            },
215                        })
216                    })
217                    .collect()
218            };
219            let outer = coedges(&face.outer).with_context(ctx)?;
220            let holes: Vec<Vec<SheetCoedge>> = face
221                .holes
222                .iter()
223                .map(coedges)
224                .collect::<GeopResult<_>>()
225                .with_context(ctx)?;
226            let refs: Vec<&NurbCurve2D> = std::iter::once(&outer)
227                .chain(&holes)
228                .flatten()
229                .map(|c| &c.pcurve)
230                .collect();
231            sheet.faces.push(SheetFace {
232                a: place.plane(&refs).with_context(ctx)?,
233                b: place.offset(t).plane(&refs).with_context(ctx)?,
234                outer,
235                holes,
236                name: face.name.clone(),
237            });
238        }
239        Ok(sheet)
240    }
241}
242
243/// A bend's place in the layout and in space: how a point of its strip,
244/// laid flat, is wrapped round its axis.
245///
246/// A point of the strip lies `along` its parent edge from where that
247/// starts and `across` it, out of the parent, on the neutral surface: so at
248/// the angle `θ = across / (radius + k_factor * thickness)` round the axis.
249/// On the bend's faces — rational quadratic arcs from the parent across to
250/// the child, swept along the axis — that is `v = along / length` and the
251/// `u` where the arc's parameter reaches `θ`: for an arc turning `α`,
252/// `tan((θ - α/2) / 2) = (2u - 1) tan(α/4)`.
253struct Wrap {
254    frame: BendFrame,
255    /// Where its parent edge starts and runs, and out of the parent, in the
256    /// layout.
257    origin: Vector2,
258    along: Vector2,
259    out: Vector2,
260    length: Scalar,
261    neutral: Scalar,
262    /// How many rational quadratic spans its arcs are built of (see
263    /// [`spans`]), and the angle each turns.
264    spans: usize,
265    step: Scalar,
266    /// `tan(step / 4)`.
267    quarter_tan: Scalar,
268    /// Its faces, A side and B side.
269    strips: (NurbSurface3D, NurbSurface3D),
270    /// Its edges across, where its parent edge starts and ends, on both
271    /// sides — as built, before any cut.
272    sides: [(NurbCurve3D, NurbCurve3D); 2],
273    /// The keys of its parent edge, its child edge and its sides.
274    keys: [String; 4],
275}
276
277impl Wrap {
278    fn new(sheet: &Sheet, b: usize, layout: &Layout) -> GeopResult<Self> {
279        let bend = &sheet.bends[b];
280        let ctx = with_context!("bend {}", bend.name.root());
281        let t = Scalar::from_f64(sheet.rules.thickness);
282        let frame = sheet.bend_frame(bend).with_context(ctx)?;
283        let [pa, pb, ca, cb] = layout.corners[b];
284        let corner = |v: usize| -> GeopResult<Pair> {
285            let (f, own) = layout.vertices[v].own.as_ref().ok_or_else(|| {
286                GeopError::new(format!(
287                    "bend {}'s corner is no vertex of the sheet as built",
288                    bend.name.root()
289                ))
290            })?;
291            let place = &sheet.flats[*f].place;
292            Ok((place.point(own), place.offset(t).point(own)))
293        };
294        let (s0, s1) = {
295            let turn = |p: Pair, c: Pair| -> GeopResult<(NurbCurve3D, NurbCurve3D)> {
296                Ok((
297                    turn(&frame, p.0, c.0, Scalar::ZERO, bend.angle, frame.r_a)?,
298                    turn(&frame, p.1, c.1, Scalar::ZERO, bend.angle, frame.r_b)?,
299                ))
300            };
301            (
302                turn(corner(pa)?, corner(ca)?).with_context(ctx)?,
303                turn(corner(pb)?, corner(cb)?).with_context(ctx)?,
304            )
305        };
306        // `u` from the parent across to the child, `v` along the parent's
307        // edge: `∂u × ∂v = m × tau = n`.
308        let strips = (
309            NurbSurface3D::ruled_compatible(&s0.0, &s1.0)?,
310            NurbSurface3D::ruled_compatible(&s0.1, &s1.1)?,
311        );
312        let origin = layout.vertices[pa].at;
313        let run = layout.vertices[pb].at.sub(&origin);
314        let along = run.normalize().with_context(ctx)?;
315        let neutral = bend
316            .radius
317            .add(Scalar::from_f64(sheet.rules.k_factor).mul(t));
318        let spans = spans(bend.angle);
319        let step = match spans {
320            1 => bend.angle,
321            n => bend.angle.div(Scalar::from_i64(n as i64))?,
322        };
323        let quarter = step.div(Scalar::from_f64(4.0))?;
324        Ok(Self {
325            origin,
326            out: Vector2::from_array([along[1], along[0].neg()]),
327            along,
328            length: run.norm(),
329            neutral,
330            spans,
331            step,
332            quarter_tan: quarter.sin().div(quarter.cos())?,
333            strips,
334            sides: [s0, s1],
335            keys: [
336                bend.parent_edge.clone(),
337                bend.child_edge.clone(),
338                bend.name.scoped("s0").root(),
339                bend.name.scoped("s1").root(),
340            ],
341            frame,
342        })
343    }
344
345    /// The layout point `x` as how far along the parent edge it is, and
346    /// at what angle round the axis.
347    fn coords(&self, x: &Vector2) -> GeopResult<(Scalar, Scalar)> {
348        let d = x.sub(&self.origin);
349        Ok((
350            d.prod_dot(&self.along),
351            d.prod_dot(&self.out).div(self.neutral)?,
352        ))
353    }
354
355    /// The point `along` the axis, at angle `theta` round it, `radius`
356    /// from it.
357    fn point(&self, along: Scalar, theta: Scalar, radius: Scalar) -> Vector3 {
358        let f = &self.frame;
359        let radial =
360            f.m.prod_scalar(theta.sin())
361                .sub(&f.n.prod_scalar(f.sigma.mul(theta.cos())));
362        f.center
363            .add(&f.tau.prod_scalar(along))
364            .add(&radial.prod_scalar(radius))
365    }
366
367    /// The layout point `x` wrapped onto the A side and the B side.
368    fn pair(&self, x: &Vector2) -> GeopResult<Pair> {
369        let (along, theta) = self.coords(x)?;
370        Ok((
371            self.point(along, theta, self.frame.r_a),
372            self.point(along, theta, self.frame.r_b),
373        ))
374    }
375
376    /// The parameters on the bend's faces of a point `along` the axis at
377    /// angle `theta`: in the span the angle falls in — at a joint between
378    /// two, either gives the same point, so which is a free choice.
379    fn uv(&self, along: Scalar, theta: Scalar) -> GeopResult<Vector2> {
380        let span =
381            ((theta.to_f64() / self.step.to_f64()).floor().max(0.0) as usize).min(self.spans - 1);
382        let local = match span {
383            0 => theta,
384            j => theta.sub(self.step.mul(Scalar::from_i64(j as i64))),
385        };
386        let half = local.sub(self.step.div(Scalar::TWO)?).div(Scalar::TWO)?;
387        let tan = half.sin().div(half.cos())?;
388        let t = Scalar::ONE
389            .add(tan.div(self.quarter_tan)?)
390            .div(Scalar::TWO)?;
391        let u = match self.spans {
392            1 => t,
393            n => t
394                .add(Scalar::from_i64(span as i64))
395                .div(Scalar::from_i64(n as i64))?,
396        };
397        Ok(Vector2::from_array([u, along.div(self.length)?]))
398    }
399
400    /// The parameters on the bend's faces of every vertex of `face`: `u`
401    /// exactly 0 on its parent edge and 1 on its child edge, `v` 0 and 1 on
402    /// its sides, the rest where the point wraps to.
403    fn vertex_uv(
404        &self,
405        face: &crate::layout::LayoutFace,
406        layout: &Layout,
407    ) -> GeopResult<BTreeMap<usize, Vector2>> {
408        let mut fixed: BTreeMap<usize, [Option<Scalar>; 2]> = BTreeMap::new();
409        for &(e, _) in face.loops().flatten() {
410            let edge = &layout.edges[e];
411            let set = match edge.origin.as_deref() {
412                Some(k) if k == self.keys[0] => Some((0, Scalar::ZERO)),
413                Some(k) if k == self.keys[1] => Some((0, Scalar::ONE)),
414                Some(k) if k == self.keys[2] => Some((1, Scalar::ZERO)),
415                Some(k) if k == self.keys[3] => Some((1, Scalar::ONE)),
416                _ => None,
417            };
418            for v in [edge.start, edge.end] {
419                let slot = fixed.entry(v).or_default();
420                if let Some((k, value)) = set {
421                    slot[k] = Some(value);
422                }
423            }
424        }
425        fixed
426            .into_iter()
427            .map(|(v, [u, w])| {
428                let (along, theta) = self.coords(&layout.vertices[v].at)?;
429                let free = self.uv(along, theta)?;
430                Ok((
431                    v,
432                    Vector2::from_array([u.unwrap_or(free[0]), w.unwrap_or(free[1])]),
433                ))
434            })
435            .collect()
436    }
437
438    /// How a curve of the strip runs: along the axis, across it, or
439    /// neither.
440    fn kind(&self, edge: &LayoutEdge) -> GeopResult<Run> {
441        if let Some(origin) = &edge.origin {
442            return Ok(if *origin == self.keys[2] || *origin == self.keys[3] {
443                Run::Across
444            } else {
445                Run::Along
446            });
447        }
448        Ok(match Seg::of(&edge.curve)? {
449            Seg::Line { a, b } => {
450                let d = b.sub(&a);
451                if d.prod_cross(&self.along).could_be_equal(Scalar::ZERO) {
452                    Run::Along
453                } else if d.prod_dot(&self.along).could_be_equal(Scalar::ZERO) {
454                    Run::Across
455                } else {
456                    Run::Wrapped
457                }
458            }
459            _ => Run::Wrapped,
460        })
461    }
462
463    /// The edge `edge` of the strip, from `start` to `end`, on both sides.
464    fn edge(
465        &self,
466        edge: &LayoutEdge,
467        start: &Pair,
468        end: &Pair,
469    ) -> GeopResult<(NurbCurve3D, NurbCurve3D)> {
470        // A side as built, uncut.
471        for (k, side) in self.sides.iter().enumerate() {
472            let key = &self.keys[2 + k];
473            if edge.origin.as_ref() == Some(key) && edge.name.root() == *key {
474                return Ok(side.clone());
475            }
476        }
477        match self.kind(edge)? {
478            Run::Along => Ok((
479                NurbCurve3D::line(start.0, end.0)?,
480                NurbCurve3D::line(start.1, end.1)?,
481            )),
482            Run::Across => {
483                let (_, from) = self.coords(&edge.curve.start()?)?;
484                let (_, to) = self.coords(&edge.curve.end()?)?;
485                Ok((
486                    turn(&self.frame, start.0, end.0, from, to, self.frame.r_a)?,
487                    turn(&self.frame, start.1, end.1, from, to, self.frame.r_b)?,
488                ))
489            }
490            Run::Wrapped => self.wrapped(&edge.curve, start, end),
491        }
492    }
493
494    /// The layout curve `curve` wrapped onto both sides, from `start` to
495    /// `end`: splines through points of it at the same parameters — alike,
496    /// so that the wall between them is ruled — each widened to enclose it.
497    fn wrapped(
498        &self,
499        curve: &NurbCurve2D,
500        start: &Pair,
501        end: &Pair,
502    ) -> GeopResult<(NurbCurve3D, NurbCurve3D)> {
503        let (params, samples, between) = sample(curve)?;
504        let mut sides = Vec::new();
505        for (k, radius) in [self.frame.r_a, self.frame.r_b].into_iter().enumerate() {
506            let at = |x: &Vector2| -> GeopResult<Vector3> {
507                let (along, theta) = self.coords(x)?;
508                Ok(self.point(along, theta, radius))
509            };
510            let mut points = samples.iter().map(at).collect::<GeopResult<Vec<_>>>()?;
511            let last = points.len() - 1;
512            points[0] = if k == 0 { start.0 } else { start.1 };
513            points[last] = if k == 0 { end.0 } else { end.1 };
514            // Through the points' centres — which points it passes through
515            // is a free choice — and widened to enclose them whole.
516            let values: Vec<Vector4> = points
517                .iter()
518                .map(|p| {
519                    let c = p.sharpen();
520                    Vector4::from_array([c[0], c[1], c[2], Scalar::ONE])
521                })
522                .collect();
523            let mut fit = NurbCurve::interpolate_homogeneous(&values, &params, 3)?;
524            let mut checks: Vec<(Scalar, Vector3)> =
525                params.iter().copied().zip(points.iter().copied()).collect();
526            for (param, x) in between.iter().flatten() {
527                checks.push((*param, at(x)?));
528            }
529            let pad = fit.enclosing_pad(&checks)?;
530            fit.widen(&pad);
531            sides.push(fit);
532        }
533        let b = sides.pop().expect("two sides");
534        let a = sides.pop().expect("two sides");
535        Ok((a, b))
536    }
537
538    /// The pcurve on the bend's faces of `edge`, run in `sense`, its ends
539    /// at the parameters `uv` gives its vertices.
540    fn pcurve(
541        &self,
542        edge: &LayoutEdge,
543        sense: Sense,
544        uv: &BTreeMap<usize, Vector2>,
545    ) -> GeopResult<NurbCurve2D> {
546        let (from, to) = match sense {
547            Sense::Forward => (edge.start, edge.end),
548            Sense::Reversed => (edge.end, edge.start),
549        };
550        if !matches!(self.kind(edge)?, Run::Wrapped) {
551            return NurbCurve2D::line(uv[&from], uv[&to]);
552        }
553        let curve = match sense {
554            Sense::Forward => edge.curve.clone(),
555            Sense::Reversed => edge.curve.reverse(),
556        };
557        let (_, samples, between) = sample(&curve)?;
558        let at = |x: &Vector2| -> GeopResult<Vector2> {
559            let (along, theta) = self.coords(x)?;
560            self.uv(along, theta)
561        };
562        let mut points = samples.iter().map(at).collect::<GeopResult<Vec<_>>>()?;
563        let last = points.len() - 1;
564        points[0] = uv[&from];
565        points[last] = uv[&to];
566        let inside = between
567            .iter()
568            .map(|chunk| chunk.iter().map(|(_, x)| at(x)).collect())
569            .collect::<GeopResult<Vec<Vec<_>>>>()?;
570        NurbCurve2D::interpolate_enclosing(&points, &inside, 3)
571    }
572}
573
574/// How many rational quadratic spans an arc turning `angle` is built of:
575/// one up to 120°, its middle weight `cos(angle / 2)` then at least a half,
576/// two beyond — up to the half turn of a hem, which one span cannot take.
577fn spans(angle: Scalar) -> usize {
578    if angle.abs().to_f64() <= 2.0 * std::f64::consts::PI / 3.0 {
579        1
580    } else {
581        2
582    }
583}
584
585/// The arc round the axis of the bend `frame` from `p`, at angle `from`,
586/// to `q`, at angle `to`, `radius` from the axis: of [`spans`] rational
587/// quadratic spans, each through the point where its end tangents meet.
588fn turn(
589    frame: &BendFrame,
590    p: Vector3,
591    q: Vector3,
592    from: Scalar,
593    to: Scalar,
594    radius: Scalar,
595) -> GeopResult<NurbCurve3D> {
596    let starts_flat = from.is_sharp() && from.could_be_equal(Scalar::ZERO);
597    let delta = if starts_flat { to } else { to.sub(from) };
598    let n = spans(delta);
599    let step = match n {
600        1 => delta,
601        n => delta.div(Scalar::from_i64(n as i64))?,
602    };
603    let half = step.div(Scalar::TWO)?;
604    let (tan, weight) = (half.sin().div(half.cos())?, half.cos());
605    let direction = |theta: Scalar, flat: bool| {
606        // Along the arc, and out from the axis, at `theta`.
607        if flat {
608            (frame.m, frame.n.prod_scalar(frame.sigma.neg()))
609        } else {
610            let (c, s) = (theta.cos(), theta.sin());
611            (
612                frame
613                    .m
614                    .prod_scalar(c)
615                    .add(&frame.n.prod_scalar(frame.sigma.mul(s))),
616                frame
617                    .m
618                    .prod_scalar(s)
619                    .sub(&frame.n.prod_scalar(frame.sigma.mul(c))),
620            )
621        }
622    };
623    if n == 1 {
624        let (tangent, _) = direction(from, starts_flat);
625        return NurbCurve3D::arc(p, p.add(&tangent.prod_scalar(radius.mul(tan))), q, weight);
626    }
627    let mut control_points = vec![p.homogeneous(Scalar::ONE)];
628    let mut knots = vec![Scalar::ZERO; 3];
629    let mut at = p;
630    for j in 0..n {
631        let theta = if j == 0 {
632            from
633        } else {
634            from.add(step.mul(Scalar::from_i64(j as i64)))
635        };
636        let (tangent, radial) = direction(theta, j == 0 && starts_flat);
637        let apex = at.add(&tangent.prod_scalar(radius.mul(tan)));
638        let next = if j + 1 == n {
639            q
640        } else {
641            let (_, out) = direction(from.add(step.mul(Scalar::from_i64(j as i64 + 1))), false);
642            at.add(&out.sub(&radial).prod_scalar(radius))
643        };
644        control_points.push(apex.homogeneous(weight));
645        control_points.push(next.homogeneous(Scalar::ONE));
646        if j + 1 < n {
647            let knot = Scalar::from_f64((j + 1) as f64 / n as f64);
648            knots.extend([knot, knot]);
649        }
650        at = next;
651    }
652    knots.extend([Scalar::ONE; 3]);
653    NurbCurve::try_new(2, control_points, knots)
654}
655
656/// How a curve of a bend's strip runs.
657enum Run {
658    /// Along the axis: a line wrapped.
659    Along,
660    /// Across it: an arc wrapped.
661    Across,
662    /// Any other way: no NURBS curve wrapped.
663    Wrapped,
664}
665
666/// Points of `curve` to fit a wrapped copy through: its parameters spread
667/// evenly from 0 to 1, the points there, and per interval between them
668/// points inside it, at their own parameters (see
669/// [`true_point_fractions`]).
670#[allow(clippy::type_complexity)]
671fn sample(
672    curve: &NurbCurve2D,
673) -> GeopResult<(Vec<Scalar>, Vec<Vector2>, Vec<Vec<(Scalar, Vector2)>>)> {
674    let (t0, t1) = curve.domain();
675    // Where to sample is a free choice, so sharp.
676    let at = |f: Scalar| -> GeopResult<Vector2> { curve.evaluate(Scalar::fraction(t0, t1, f)) };
677    let mut params = Vec::new();
678    let mut samples = Vec::new();
679    let mut between = Vec::new();
680    for i in 0..=SAMPLES {
681        // Sharp: the parameters a fit passes through are a free choice.
682        let f = Scalar::from_f64(i as f64 / SAMPLES as f64);
683        params.push(f);
684        samples.push(at(f)?);
685        if i < SAMPLES {
686            let mut inside = Vec::new();
687            for &(num, den) in true_point_fractions(i, SAMPLES) {
688                let g =
689                    Scalar::from_f64((i as i64 * den + num) as f64 / (SAMPLES as i64 * den) as f64);
690                inside.push((g, at(g)?));
691            }
692            between.push(inside);
693        }
694    }
695    Ok((params, samples, between))
696}