Skip to main content

ogeom_offset/sweep/
evolving.rs

1//! Pipe shells whose section scales and twists along the spine.
2//!
3//! The section is the profile placed in the law's frame at each point of
4//! the spine, as for [`make_pipe_shell_with`](super::make_pipe_shell_with),
5//! and before that scaled about the spine and turned about it, each by its
6//! own law of the share of the spine's length run there: linear, a
7//! monotone cubic through given values, or a function the caller gives.
8//!
9//! Scaled without a twist down a straight spine, every profile point runs
10//! along a straight line, at a constant speed, to its scaled image at the
11//! end: the solid is the ruled loft between the profile and that image,
12//! exact (a pyramid's frustum over a polygon, a cone's over a circle, the
13//! pyramid or cone itself where the scale reaches zero). Everywhere else
14//! the walls are skinned through the placed sections and held to the
15//! tolerance between them against the law itself.
16
17use std::sync::Arc;
18
19use ogeom_algo::{Built, History, curve_length, find_plane};
20use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
21use ogeom_geom::Curve;
22use ogeom_math::{Axis, Direction, Point, Transform};
23use ogeom_topo::{Filter, Model, Shape, ShapeType, VertexData, explore};
24
25use super::corners::law_cornered;
26use super::placement::{PipeContact, profile_in_contact};
27use super::{
28    PipeCorners, PipeLaw, SpineStation, WalkCurve, closed_pipe_shell, closes_on_a_corner,
29    densified, evenly_by, has_sharp_corner, law_loft, make_loft, make_pipe_shell_with, moved_ring,
30    placed_path, profile_square_at_start, shell_stations, spine_closes, spine_curve_of,
31    straight_legs,
32};
33
34/// How a pipe's section scales and twists along its spine, each in
35/// proportion to the length run along the spine: none at its start, all
36/// of it at its end.
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct PipeEvolution {
39    /// The section's scale at the spine's end, about the spine; it is one
40    /// at the start. Zero closes the pipe on the spine's end point.
41    pub scale: f64,
42    /// The angle in radians the section has turned through about the
43    /// spine by its end, counter-clockwise about the direction of travel.
44    pub twist: f64,
45}
46
47/// How a quantity runs along a pipe's spine, against the share of the
48/// spine's length run: from its value at the start (one for a scale,
49/// naught for a twist) at a share of naught.
50#[derive(Debug, Clone, PartialEq)]
51pub enum SpineLaw {
52    /// Linearly to this value at the spine's end.
53    Linear(f64),
54    /// Through these `(share, value)` points, the shares rising strictly
55    /// and the last of them one, the spine's end. Between two neighbouring
56    /// points the law is the cubic that keeps between their values, and it
57    /// runs on through each point with one slope (the monotone cubic
58    /// Hermite interpolant): a value the points never leave, the law never
59    /// leaves either.
60    Through(Vec<(f64, f64)>),
61    /// The value the function gives at each share, from naught to one. It
62    /// gives the start value at naught exactly. The sweep reads it at its
63    /// stations and wherever it checks its skin between them, and holds
64    /// the skin to the tolerance against what it reads there; it is to be
65    /// continuous over the spine, and a scale law above naught all along.
66    Function(SpineFunction),
67}
68
69/// A law of the share of a pipe's spine run, given as a function from a
70/// share in `[0, 1]` to the law's value there.
71#[derive(Clone)]
72pub struct SpineFunction(Arc<dyn Fn(f64) -> f64 + Send + Sync>);
73
74impl SpineFunction {
75    /// The law `f`.
76    pub fn new(f: impl Fn(f64) -> f64 + Send + Sync + 'static) -> Self {
77        Self(Arc::new(f))
78    }
79
80    /// The law's value at share `lambda`.
81    pub fn at(&self, lambda: f64) -> f64 {
82        (self.0)(lambda)
83    }
84}
85
86impl core::fmt::Debug for SpineFunction {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        f.write_str("SpineFunction(..)")
89    }
90}
91
92/// Two laws are one where they are the same function.
93impl PartialEq for SpineFunction {
94    fn eq(&self, other: &Self) -> bool {
95        Arc::ptr_eq(&self.0, &other.0)
96    }
97}
98
99/// How a pipe's section scales and twists along its spine, each by its
100/// own law of the share of the spine's length run, and how its profile is
101/// brought onto the spine first.
102#[derive(Debug, Clone, PartialEq)]
103pub struct PipeLaws {
104    /// The section's scale about the spine; one at the start.
105    pub scale: SpineLaw,
106    /// The angle in radians the section has turned through about the
107    /// spine, counter-clockwise about the direction of travel; naught at
108    /// the start.
109    pub twist: SpineLaw,
110    /// How the profile is moved onto the spine's start before it is
111    /// swept; as placed by default.
112    pub contact: PipeContact,
113}
114
115/// The section neither scaled nor twisted, its profile swept as placed.
116impl Default for PipeLaws {
117    fn default() -> Self {
118        Self {
119            scale: SpineLaw::Linear(1.0),
120            twist: SpineLaw::Linear(0.0),
121            contact: PipeContact::AsPlaced,
122        }
123    }
124}
125
126impl From<PipeEvolution> for PipeLaws {
127    fn from(evolution: PipeEvolution) -> Self {
128        Self {
129            scale: SpineLaw::Linear(evolution.scale),
130            twist: SpineLaw::Linear(evolution.twist),
131            contact: PipeContact::AsPlaced,
132        }
133    }
134}
135
136/// A [`SpineLaw`] read for evaluation: its values at rising shares from
137/// naught to one, and the slope the law runs through each with.
138#[derive(Debug, Clone, PartialEq)]
139pub(super) struct Ramp {
140    shares: Vec<f64>,
141    values: Vec<f64>,
142    slopes: Vec<f64>,
143    /// Whether the law runs straight from its start value to its end one.
144    linear: bool,
145    /// A law given as a function, read in place of the cubics.
146    function: Option<FunctionRead>,
147}
148
149/// A [`SpineFunction`] read over a stretch of the spine: at a share `λ` of
150/// the stretch it gives `gain f(from + (to - from) λ) + offset + tilt λ`.
151#[derive(Debug, Clone, PartialEq)]
152struct FunctionRead {
153    law: SpineFunction,
154    from: f64,
155    to: f64,
156    gain: f64,
157    offset: f64,
158    tilt: f64,
159}
160
161impl FunctionRead {
162    fn at(&self, lambda: f64) -> f64 {
163        self.gain * self.law.at(self.from + (self.to - self.from) * lambda)
164            + self.offset
165            + self.tilt * lambda
166    }
167}
168
169/// How many even shares a law given as a function is read at to bound
170/// from below how far it runs, which seeds the stations a skin starts
171/// from (the skin then refines itself between them), and to refuse a
172/// scale it shows at or below naught.
173const FUNCTION_READS: u32 = 64;
174
175impl Ramp {
176    /// The law that holds `value` all along.
177    pub(super) fn level(value: f64) -> Self {
178        Self {
179            shares: vec![0.0, 1.0],
180            values: vec![value, value],
181            slopes: vec![0.0, 0.0],
182            linear: true,
183            function: None,
184        }
185    }
186
187    /// The law `f`, which gives `start` at the spine's start; `what` names
188    /// it in a refusal.
189    fn of_function(f: &SpineFunction, start: f64, what: &str) -> OgeomResult<Self> {
190        let (first, last) = (f.at(0.0), f.at(1.0));
191        if first != start {
192            ogeom_bail!(
193                Construction,
194                "a {what} law given as a function gives {start} at the spine's start, \
195                 not {first}"
196            );
197        }
198        if !last.is_finite() {
199            ogeom_bail!(Construction, "a {what} law takes finite values");
200        }
201        Ok(Self {
202            shares: vec![0.0, 1.0],
203            values: vec![first, last],
204            slopes: vec![0.0, 0.0],
205            linear: false,
206            function: Some(FunctionRead {
207                law: f.clone(),
208                from: 0.0,
209                to: 1.0,
210                gain: 1.0,
211                offset: 0.0,
212                tilt: 0.0,
213            }),
214        })
215    }
216
217    /// `law` read from `start` at the spine's start; `what` names it in a
218    /// refusal.
219    fn of(law: &SpineLaw, start: f64, what: &str) -> OgeomResult<Self> {
220        let mut shares = vec![0.0];
221        let mut values = vec![start];
222        match law {
223            SpineLaw::Linear(end) => {
224                shares.push(1.0);
225                values.push(*end);
226            }
227            SpineLaw::Through(points) => {
228                for &(share, value) in points {
229                    shares.push(share);
230                    values.push(value);
231                }
232            }
233            SpineLaw::Function(f) => return Self::of_function(f, start, what),
234        }
235        if values.iter().any(|v| !v.is_finite()) {
236            ogeom_bail!(Construction, "a {what} law takes finite values");
237        }
238        if shares
239            .windows(2)
240            .any(|w| w[1].partial_cmp(&w[0]) != Some(core::cmp::Ordering::Greater))
241            || shares[shares.len() - 1] != 1.0
242        {
243            ogeom_bail!(
244                Construction,
245                "a {what} law's shares of the spine rise strictly from naught to one, \
246                 the last of them one"
247            );
248        }
249        // Fritsch and Carlson's slopes: each span's secant, the mean of two
250        // where they agree in sign and none where they do not, each pair
251        // then drawn in onto the circle of radius three about the secant,
252        // inside which the span's cubic keeps between its ends.
253        let n = shares.len();
254        let secants: Vec<f64> = (0..n - 1)
255            .map(|k| (values[k + 1] - values[k]) / (shares[k + 1] - shares[k]))
256            .collect();
257        let mut slopes = vec![0.0; n];
258        slopes[0] = secants[0];
259        slopes[n - 1] = secants[n - 2];
260        for k in 1..n - 1 {
261            if secants[k - 1] * secants[k] > 0.0 {
262                slopes[k] = f64::midpoint(secants[k - 1], secants[k]);
263            }
264        }
265        for k in 0..n - 1 {
266            if secants[k] == 0.0 {
267                slopes[k] = 0.0;
268                slopes[k + 1] = 0.0;
269                continue;
270            }
271            let (a, b) = (slopes[k] / secants[k], slopes[k + 1] / secants[k]);
272            let reach = a.hypot(b);
273            if reach > 3.0 {
274                slopes[k] = 3.0 * a / reach * secants[k];
275                slopes[k + 1] = 3.0 * b / reach * secants[k];
276            }
277        }
278        Ok(Self {
279            linear: matches!(law, SpineLaw::Linear(_)),
280            shares,
281            values,
282            slopes,
283            function: None,
284        })
285    }
286
287    /// The span of the law `lambda` falls in, and how far along it.
288    fn span(&self, lambda: f64) -> (usize, f64) {
289        let k = self.shares[1..self.shares.len() - 1].partition_point(|s| *s < lambda);
290        let t = (lambda - self.shares[k]) / (self.shares[k + 1] - self.shares[k]);
291        (k, t)
292    }
293
294    /// The law's value a share `lambda` of the way along.
295    pub(super) fn at(&self, lambda: f64) -> f64 {
296        let lambda = lambda.clamp(0.0, 1.0);
297        if let Some(f) = &self.function {
298            return f.at(lambda);
299        }
300        if self.linear {
301            return self.values[0] + (self.values[1] - self.values[0]) * lambda;
302        }
303        let (k, t) = self.span(lambda);
304        if self.values[k] == self.values[k + 1]
305            && self.slopes[k] == 0.0
306            && self.slopes[k + 1] == 0.0
307        {
308            return self.values[k];
309        }
310        let h = self.shares[k + 1] - self.shares[k];
311        let (t2, t3) = (t * t, t * t * t);
312        (2.0 * t3 - 3.0 * t2 + 1.0) * self.values[k]
313            + (t3 - 2.0 * t2 + t) * h * self.slopes[k]
314            + (3.0 * t2 - 2.0 * t3) * self.values[k + 1]
315            + (t3 - t2) * h * self.slopes[k + 1]
316    }
317
318    /// The law's rate of change with the share a share `lambda` of the way
319    /// along.
320    fn slope_at(&self, lambda: f64) -> f64 {
321        let lambda = lambda.clamp(0.0, 1.0);
322        if self.linear {
323            return self.values[1] - self.values[0];
324        }
325        let (k, t) = self.span(lambda);
326        let h = self.shares[k + 1] - self.shares[k];
327        let t2 = t * t;
328        ((6.0 * t2 - 6.0 * t) * (self.values[k] - self.values[k + 1])) / h
329            + (3.0 * t2 - 4.0 * t + 1.0) * self.slopes[k]
330            + (3.0 * t2 - 2.0 * t) * self.slopes[k + 1]
331    }
332
333    /// The law over the stretch from share `from` to share `to`, read
334    /// against the share of that stretch: the same cubics, cut where the
335    /// stretch ends.
336    fn window(&self, from: f64, to: f64) -> Self {
337        let width = to - from;
338        if let Some(f) = &self.function {
339            let span = f.to - f.from;
340            let read = FunctionRead {
341                from: f.from + span * from,
342                to: f.from + span * to,
343                offset: f.offset + f.tilt * from,
344                tilt: f.tilt * width,
345                ..f.clone()
346            };
347            return Self {
348                shares: vec![0.0, 1.0],
349                values: vec![read.at(0.0), read.at(1.0)],
350                slopes: vec![0.0, 0.0],
351                linear: false,
352                function: Some(read),
353            };
354        }
355        if self.linear {
356            return Self {
357                shares: vec![0.0, 1.0],
358                values: vec![self.at(from), self.at(to)],
359                slopes: vec![0.0, 0.0],
360                linear: true,
361                function: None,
362            };
363        }
364        let mut shares = vec![0.0];
365        let mut values = vec![self.at(from)];
366        let mut slopes = vec![self.slope_at(from) * width];
367        for k in 0..self.shares.len() {
368            if self.shares[k] > from && self.shares[k] < to {
369                shares.push((self.shares[k] - from) / width);
370                values.push(self.values[k]);
371                slopes.push(self.slopes[k] * width);
372            }
373        }
374        shares.push(1.0);
375        values.push(self.at(to));
376        slopes.push(self.slope_at(to) * width);
377        Self {
378            shares,
379            values,
380            slopes,
381            linear: false,
382            function: None,
383        }
384    }
385
386    /// The law with every value taken through `f`, an affine map whose
387    /// rate is `rate`.
388    fn mapped(mut self, f: impl Fn(f64) -> f64, rate: f64) -> Self {
389        if let Some(read) = &mut self.function {
390            read.gain *= rate;
391            read.offset = f(read.offset);
392            read.tilt *= rate;
393        }
394        for v in &mut self.values {
395            *v = f(*v);
396        }
397        for m in &mut self.slopes {
398            *m *= rate;
399        }
400        self
401    }
402
403    /// The value at the spine's end.
404    pub(super) fn end(&self) -> f64 {
405        self.values[self.values.len() - 1]
406    }
407
408    /// How far from the start the law holds its start value, and from how
409    /// far it holds its end value to the end: the shares bounding its
410    /// level spans at either end, where its values and slopes stand still.
411    fn still_ends(&self) -> (f64, f64) {
412        let n = self.shares.len();
413        if self.function.is_some() {
414            // A function's level stretches are not known: it is read as
415            // moving all along.
416            return (0.0, 1.0);
417        }
418        if self.linear {
419            return if self.values[0] == self.values[1] {
420                (1.0, 0.0)
421            } else {
422                (0.0, 1.0)
423            };
424        }
425        let flat = |k: usize| {
426            self.values[k] == self.values[k + 1]
427                && self.slopes[k] == 0.0
428                && self.slopes[k + 1] == 0.0
429        };
430        let mut lead = 0;
431        while lead + 1 < n && flat(lead) {
432            lead += 1;
433        }
434        let mut trail = n - 1;
435        while trail > 0 && flat(trail - 1) {
436            trail -= 1;
437        }
438        (self.shares[lead], self.shares[trail])
439    }
440
441    /// Whether the law holds `value` all along.
442    pub(super) fn is_level(&self, value: f64) -> bool {
443        self.function.is_none() && self.values.iter().all(|v| *v == value)
444    }
445
446    /// Whether the law runs straight from its start value to its end one.
447    fn is_linear(&self) -> bool {
448        self.linear
449    }
450
451    /// The law's values at [`FUNCTION_READS`] even steps from start to
452    /// end, for a law given as a function.
453    fn function_reads(&self) -> Option<Vec<f64>> {
454        self.function.as_ref().map(|f| {
455            (0..=FUNCTION_READS)
456                .map(|k| f.at(f64::from(k) / f64::from(FUNCTION_READS)))
457                .collect()
458        })
459    }
460
461    /// The least value the law takes: each span keeps between its ends. A
462    /// function's is the least it shows where it is read.
463    fn least(&self) -> f64 {
464        let reads = self.function_reads().unwrap_or_else(|| self.values.clone());
465        reads.iter().copied().fold(f64::INFINITY, |a, b| {
466            if b.is_nan() {
467                f64::NEG_INFINITY
468            } else {
469                a.min(b)
470            }
471        })
472    }
473
474    /// How far the law runs, up and down, from start to end: each span
475    /// runs one way only. A function runs at least as far as it shows
476    /// where it is read.
477    fn variation(&self) -> f64 {
478        let reads = self.function_reads().unwrap_or_else(|| self.values.clone());
479        reads.windows(2).map(|w| (w[1] - w[0]).abs()).sum()
480    }
481
482    /// The same law ending at `end`.
483    fn ending_at(&self, end: f64) -> OgeomResult<Self> {
484        if let Some(f) = &self.function {
485            let read = FunctionRead {
486                tilt: f.tilt + end - f.at(1.0),
487                ..f.clone()
488            };
489            return Ok(Self {
490                values: vec![read.at(0.0), read.at(1.0)],
491                function: Some(read),
492                ..self.clone()
493            });
494        }
495        let points: Vec<(f64, f64)> = self.shares[1..]
496            .iter()
497            .zip(&self.values[1..])
498            .map(|(s, v)| (*s, *v))
499            .collect();
500        let mut points = points;
501        if let Some(last) = points.last_mut() {
502            last.1 = end;
503        }
504        let law = if self.linear {
505            SpineLaw::Linear(end)
506        } else {
507            SpineLaw::Through(points)
508        };
509        Self::of(&law, self.values[0], "twist")
510    }
511}
512
513/// A pipe's scale and twist laws, read for evaluation.
514#[derive(Debug, Clone)]
515pub(super) struct Evolution {
516    scale: Ramp,
517    twist: Ramp,
518}
519
520impl Evolution {
521    /// The scale and turn a share `lambda` of the way along, in the start
522    /// frame's coordinates: about its origin, the spine, and its `z`, the
523    /// spine's tangent.
524    pub(super) fn local(&self, lambda: f64, tol: Tolerances) -> OgeomResult<Transform> {
525        let (scale, twist) = (self.scale.at(lambda), self.twist.at(lambda));
526        // A law given as a function is checked wherever it is read.
527        if self.scale.function.is_some() && !(scale.is_finite() && scale > 0.0) {
528            ogeom_bail!(
529                Construction,
530                "a scale law given as a function stays above naught; it gives {scale} \
531                 where the sweep reads it"
532            );
533        }
534        if self.twist.function.is_some() && !twist.is_finite() {
535            ogeom_bail!(
536                Construction,
537                "a twist law takes finite values; it gives {twist} where the sweep reads it"
538            );
539        }
540        let turn = Transform::rotation(
541            Axis {
542                location: Point::ORIGIN,
543                direction: Direction::Z,
544            },
545            twist,
546        );
547        Ok(turn * Transform::scaling(Point::ORIGIN, scale, tol)?)
548    }
549
550    /// The shares of the way along bounding where the laws move the
551    /// section: before the first and after the second both stand still.
552    pub(super) fn still_ends(&self) -> (f64, f64) {
553        let (scale, twist) = (self.scale.still_ends(), self.twist.still_ends());
554        (scale.0.min(twist.0), scale.1.max(twist.1))
555    }
556
557    /// Whether the laws neither scale nor turn the section anywhere.
558    pub(super) fn is_still(&self) -> bool {
559        self.scale.is_level(1.0) && self.twist.is_level(0.0)
560    }
561
562    /// The laws over the stretch from share `from` to share `to`, against
563    /// the share of that stretch and from the section as it stands at
564    /// `from`: the scale over its value there, the twist less its value
565    /// there. About the spine a scale and a turn commute, so the section
566    /// at `from` carried on by these is the profile carried by the whole
567    /// laws.
568    pub(super) fn window(&self, from: f64, to: f64) -> Self {
569        let (s0, t0) = (self.scale.at(from), self.twist.at(from));
570        Self {
571            scale: self.scale.window(from, to).mapped(|v| v / s0, 1.0 / s0),
572            twist: self.twist.window(from, to).mapped(|v| v - t0, 1.0),
573        }
574    }
575}
576
577/// The length of a spine's edges and of the run up to each.
578pub(super) struct SpineLengths {
579    before: Vec<f64>,
580    total: f64,
581}
582
583impl SpineLengths {
584    /// The lengths along `curves`, a spine's edges in order.
585    pub(super) fn of(curves: &[WalkCurve], tol: Tolerances) -> OgeomResult<Self> {
586        let mut before = Vec::with_capacity(curves.len());
587        let mut total = 0.0;
588        for (curve, range, _) in curves {
589            before.push(total);
590            total += curve_length(curve, *range, tol)?;
591        }
592        if total <= tol.confusion() {
593            ogeom_bail!(Construction, "the spine has no length");
594        }
595        Ok(Self { before, total })
596    }
597
598    /// The share of the spine's length run to parameter `t` on edge `edge`.
599    pub(super) fn fraction(
600        &self,
601        curves: &[WalkCurve],
602        edge: usize,
603        t: f64,
604        tol: Tolerances,
605    ) -> OgeomResult<f64> {
606        let (curve, range, reversed) = &curves[edge];
607        let run = if *reversed {
608            curve_length(curve, (t, range.1), tol)?
609        } else {
610            curve_length(curve, (range.0, t), tol)?
611        };
612        Ok(((self.before[edge] + run) / self.total).clamp(0.0, 1.0))
613    }
614}
615
616/// Sweep a planar profile along a spine under a frame law, the section
617/// scaling and twisting as it goes.
618///
619/// The profile (a closed wire, or a face whose holes ride along) stands
620/// square to the spine at its start, as for a pipe shell. At each point of
621/// the spine the section is the profile scaled about the spine by
622/// `1 + (scale - 1) λ` and turned about it by `twist λ`, `λ` the share of
623/// the spine's length run there, then carried by `law`. With no scale and
624/// no twist this is [`make_pipe_shell_with`] with mitred corners; it is
625/// [`make_pipe_shell_laws`] with linear laws.
626///
627/// Scaled without a twist down one straight edge under the
628/// rotation-minimizing, binormal or fixed law (none of which turns the
629/// section along a line), the solid is the ruled loft from the profile to
630/// its scaled image, exact; a scale of zero closes it on the spine's end,
631/// the exact pyramid or cone. Otherwise each loop of the profile is
632/// skinned through its placed sections and held to `tolerance` between
633/// them against the law, along an open spine with no sharp corner. Round
634/// a smooth closed spine the section comes home to itself only unscaled,
635/// and twisted whole turns or a part of one that turns it onto itself (a
636/// square a quarter turn, a regular hexagon a sixth, a circle about its
637/// centre any angle): the skin is closed round the spine, each hole a
638/// void tunnel of its own shell. Each loop of the profile must come home
639/// onto itself, a faceted one edge onto edge.
640///
641/// Along an open spine each hole sweeps its own solid under the same law,
642/// cut from the outer one.
643///
644/// # Errors
645///
646/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
647/// `scale` is negative or `twist` not finite; if the profile is not a
648/// closed planar wire or face standing square to the spine at its start;
649/// if a skinned sweep is asked round a sharp corner, of the fixed law, or
650/// to a scale of zero; if a section is scaled, or twisted by a part of a
651/// turn that does not bring each of its loops home onto itself, round a
652/// closed spine; and as
653/// [`make_pipe_shell_with`] or the law refuses the spine.
654/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if the skin
655/// cannot reach `tolerance`.
656pub fn make_pipe_shell_evolving(
657    model: &mut Model,
658    profile: &Shape,
659    spine: &Shape,
660    law: &PipeLaw<'_>,
661    evolution: PipeEvolution,
662    tolerance: f64,
663    tol: Tolerances,
664) -> OgeomResult<Built> {
665    make_pipe_shell_laws(
666        model,
667        profile,
668        spine,
669        law,
670        &evolution.into(),
671        PipeCorners::Mitre,
672        tolerance,
673        tol,
674    )
675}
676
677/// [`make_pipe_shell_evolving`] with a law of its own for the scale and
678/// for the twist: at each point of the spine the section is the profile
679/// scaled about the spine by `laws.scale` and turned about it by
680/// `laws.twist`, each read at the share of the spine's length run there,
681/// then carried by `law`.
682///
683/// Linear laws build as [`make_pipe_shell_evolving`] does. Any other law
684/// skins each loop through its placed sections, held to `tolerance`
685/// against the laws; round a smooth closed spine the twist may follow any
686/// law that ends where a linear one may, unscaled. A scale law through
687/// values is above naught all along: a cubic span keeps between its ends.
688/// A law given as a function is read wherever the sweep places a section
689/// or checks its skin, and is never taken as standing still: every run it
690/// moves along is skinned, and round a closed spine it twists only, as a
691/// scale it is refused there.
692///
693/// Where `laws.contact` asks, the profile is first moved rigidly onto the
694/// spine's start as [`PipeContact`] defines, and the moved profile is
695/// swept as above; the history records the given profile as generating
696/// the result.
697///
698/// Round sharp corners a face sweeps run by run as
699/// [`make_pipe_shell_with`] does, each corner turned as `corners` asks:
700/// each smooth run is swept under the laws over its share of the spine's
701/// length, from the section it starts with (a run the laws neither scale
702/// nor turn is the prism or pipe shell of that section), and the runs
703/// meet at each corner on the section the laws give there.
704///
705/// # Errors
706///
707/// As [`make_pipe_shell_evolving`] and, round sharp corners, as
708/// [`make_pipe_shell_with`]; and
709/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a
710/// law's shares do not rise strictly to one, a value is not finite, a
711/// scale law through values reaches naught or below, or a law given as a
712/// function does not give the start value at the spine's start or, as a
713/// scale, gives naught or below where it is read; and, where
714/// `laws.contact` moves the profile, if the profile is not planar, or its
715/// plane holds the spine's start tangent under a correction.
716#[allow(clippy::too_many_arguments)]
717pub fn make_pipe_shell_laws(
718    model: &mut Model,
719    profile: &Shape,
720    spine: &Shape,
721    law: &PipeLaw<'_>,
722    laws: &PipeLaws,
723    corners: PipeCorners,
724    tolerance: f64,
725    tol: Tolerances,
726) -> OgeomResult<Built> {
727    if let SpineLaw::Linear(scale) = laws.scale
728        && (!scale.is_finite() || scale < 0.0)
729    {
730        ogeom_bail!(
731            Construction,
732            "a section scales to {scale} at the spine's end; a scale is naught or more"
733        );
734    }
735    if let SpineLaw::Linear(twist) = laws.twist
736        && !twist.is_finite()
737    {
738        ogeom_bail!(Construction, "a twist of {twist} is not an angle");
739    }
740    let evolution = Evolution {
741        scale: Ramp::of(&laws.scale, 1.0, "scale")?,
742        twist: Ramp::of(&laws.twist, 0.0, "twist")?,
743    };
744    // The spine is swept where it stands.
745    let placed_spine = placed_path(model, spine, tol)?;
746    if placed_spine != *spine {
747        let mut built = make_pipe_shell_laws(
748            model,
749            profile,
750            &placed_spine,
751            law,
752            laws,
753            corners,
754            tolerance,
755            tol,
756        )?;
757        built.history.generate(spine, built.shape.clone());
758        return Ok(built);
759    }
760    let placed = profile_in_contact(model, profile, spine, laws.contact, tol)?;
761    if placed != *profile {
762        let mut built = make_pipe_shell_laws(
763            model,
764            &placed,
765            spine,
766            law,
767            &PipeLaws {
768                contact: PipeContact::AsPlaced,
769                ..laws.clone()
770            },
771            corners,
772            tolerance,
773            tol,
774        )?;
775        built.history.generate(profile, built.shape.clone());
776        return Ok(built);
777    }
778    if matches!(laws.scale, SpineLaw::Through(_) | SpineLaw::Function(_))
779        && evolution.scale.least() <= 0.0
780    {
781        ogeom_bail!(
782            Construction,
783            "a scale law through values or given as a function stays above naught; a \
784             section shrinks to a point only linearly"
785        );
786    }
787    if evolution.is_still() {
788        return make_pipe_shell_with(model, profile, spine, law, corners, tolerance, tol);
789    }
790    let rings: Vec<Shape> = match model.kind_of(profile)? {
791        ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
792        ShapeType::Wire => vec![profile.clone()],
793        other => ogeom_bail!(
794            Construction,
795            "a pipe shell sweeps a planar wire or face, not a {other:?}"
796        ),
797    };
798    let Some(plane) = find_plane(model, profile, tol)? else {
799        ogeom_bail!(Construction, "a pipe shell sweeps a planar profile");
800    };
801    let stations = shell_stations(model, spine, tol)?;
802    profile_square_at_start(model, &plane, spine, stations[0].at, tol)?;
803    if rings.len() > 1 && evolution.scale.end() == 0.0 {
804        ogeom_bail!(
805            Construction,
806            "a profile with holes cannot close on a point; its holes would meet"
807        );
808    }
809    if spine_closes(model, spine, tol)?
810        && !has_sharp_corner(model, spine, tol)?
811        && !closes_on_a_corner(model, spine, tol)?
812        && !matches!(law, PipeLaw::Fixed)
813    {
814        if !evolution.scale.is_level(1.0) {
815            ogeom_bail!(
816                Construction,
817                "a section scaling round a closed spine would not come home to itself; \
818                 it scales along an open one"
819            );
820        }
821        // Whole turns bring the section home to itself; a part of a turn
822        // only where its symmetry does, which the closed shell checks.
823        let end = evolution.twist.end();
824        let turns = end / core::f64::consts::TAU;
825        let twist = if (turns - turns.round()).abs() * core::f64::consts::TAU > tol.angular() {
826            evolution.twist
827        } else {
828            evolution
829                .twist
830                .ending_at(turns.round() * core::f64::consts::TAU)?
831        };
832        return closed_law_pipe(model, profile, spine, law, &twist, tolerance, tol);
833    }
834    if !matches!(law, PipeLaw::Fixed)
835        && (has_sharp_corner(model, spine, tol)? || closes_on_a_corner(model, spine, tol)?)
836    {
837        return law_cornered(
838            model,
839            profile,
840            spine,
841            law,
842            corners,
843            Some(&evolution),
844            tolerance,
845            tol,
846        );
847    }
848    let shape = evolved_run(
849        model, profile, spine, law, &evolution, false, tolerance, tol,
850    )?;
851    let mut history = History::new();
852    history.generate(spine, shape.clone());
853    history.generate(profile, shape.clone());
854    Ok(Built::new(shape, history))
855}
856
857/// A planar profile swept along a spine with no sharp corner under the
858/// evolving laws: each loop its own solid, the holes' cut from the outer.
859/// Where `exact_ends` asks it, a skinned loop of one edge with an exact
860/// rational form ends on its end sections exactly.
861#[allow(clippy::too_many_arguments)]
862pub(super) fn evolved_run(
863    model: &mut Model,
864    profile: &Shape,
865    spine: &Shape,
866    law: &PipeLaw<'_>,
867    evolution: &Evolution,
868    exact_ends: bool,
869    tolerance: f64,
870    tol: Tolerances,
871) -> OgeomResult<Shape> {
872    let rings: Vec<Shape> = match model.kind_of(profile)? {
873        ShapeType::Face => explore(model, profile, Filter::OfType(ShapeType::Wire))?,
874        _ => vec![profile.clone()],
875    };
876    let mut solids = Vec::with_capacity(rings.len());
877    for ring in &rings {
878        solids.push(evolving_ring(
879            model, ring, spine, law, evolution, exact_ends, tolerance, tol,
880        )?);
881    }
882    let mut shape = solids[0].clone();
883    for hole in &solids[1..] {
884        shape = ogeom_bool::cut(model, &shape, hole, tol)?.shape;
885    }
886    Ok(shape)
887}
888
889/// One closed ring of the profile swept under the evolving law, as a solid.
890#[allow(clippy::too_many_arguments)]
891fn evolving_ring(
892    model: &mut Model,
893    ring: &Shape,
894    spine: &Shape,
895    law: &PipeLaw<'_>,
896    evolution: &Evolution,
897    exact_ends: bool,
898    tolerance: f64,
899    tol: Tolerances,
900) -> OgeomResult<Shape> {
901    let level = matches!(
902        law,
903        PipeLaw::RotationMinimizing | PipeLaw::Binormal(_) | PipeLaw::Fixed
904    );
905    let scale = evolution.scale.end();
906    if evolution.twist.is_level(0.0)
907        && evolution.scale.is_linear()
908        && level
909        && let Some(legs) = straight_legs(model, spine, tol)?
910        && legs.len() == 1
911    {
912        let (start, run) = legs[0];
913        let end = if scale == 0.0 {
914            model.add_vertex(VertexData::new(start + run))
915        } else {
916            let motion = Transform::translation(run) * Transform::scaling(start, scale, tol)?;
917            moved_ring(model, ring, &motion, tol)?
918        };
919        // A polygon or a circle lofts to its image as planes or a cone;
920        // any other ring as the ruled walls between matched edges.
921        let shape = if scale == 0.0 || analytic_ring(model, ring, start, tol)? {
922            make_loft(model, ring, &end, tol)?.shape
923        } else {
924            crate::loft::make_loft_solid(model, &[ring.clone(), end], &[], true, tol)?.shape
925        };
926        return Ok(shape);
927    }
928    if matches!(law, PipeLaw::Fixed) {
929        ogeom_bail!(
930            Construction,
931            "a fixed section scales or twists only down one straight edge, unturned"
932        );
933    }
934    if scale == 0.0 {
935        ogeom_bail!(
936            Construction,
937            "a section shrinks to a point only down one straight edge, unturned"
938        );
939    }
940    let stations = shell_stations(model, spine, tol)?;
941    let ring_spine =
942        stations[0].at.distance(stations[stations.len() - 1].at) <= tol.confusion() * 10.0;
943    if ring_spine || has_sharp_corner(model, spine, tol)? {
944        ogeom_bail!(
945            Construction,
946            "a scaling or twisting section sweeps an open spine with no sharp corner"
947        );
948    }
949    // As many stations as the law's turning or the twist asks, each edge
950    // its share by length.
951    let turned = densified(model, spine, stations.clone(), law, tolerance, tol)?.len();
952    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
953    let twisted = (evolution.twist.variation() / super::MOST_TURN).ceil() as usize + 1;
954    let wanted = turned.max(twisted);
955    let lengths = edge_lengths(model, spine, tol)?;
956    let total: f64 = lengths.iter().sum();
957    #[allow(
958        clippy::cast_precision_loss,
959        clippy::cast_possible_truncation,
960        clippy::cast_sign_loss
961    )]
962    let share = |e: usize| -> u32 {
963        let length = lengths.get(e).copied().unwrap_or(0.0);
964        ((wanted as f64 * length / total).ceil() as u32).max(2)
965    };
966    let stations = evenly_by(model, spine, stations, &share, false, tol)?;
967    Ok(law_loft(
968        model,
969        ring,
970        spine,
971        &stations,
972        law,
973        Some(evolution),
974        exact_ends,
975        tolerance,
976        tol,
977    )?
978    .shape)
979}
980
981/// A planar profile swept round a smooth closed spine under `law`, turned
982/// about the spine by the `twist` law of the length run (ending on whole
983/// turns, or a part of one the section's symmetry closes): stations as
984/// many as the law's turning or the twist asks, each edge its share by
985/// length, and the skin closed on itself.
986pub(super) fn closed_law_pipe(
987    model: &mut Model,
988    profile: &Shape,
989    spine: &Shape,
990    law: &PipeLaw<'_>,
991    twist: &Ramp,
992    tolerance: f64,
993    tol: Tolerances,
994) -> OgeomResult<Built> {
995    let stations = shell_stations(model, spine, tol)?;
996    let turned = densified(model, spine, stations.clone(), law, tolerance, tol)?.len();
997    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
998    let twisted = (twist.variation() / super::MOST_TURN).ceil() as usize + 1;
999    let wanted = turned.max(twisted);
1000    let lengths = edge_lengths(model, spine, tol)?;
1001    let total: f64 = lengths.iter().sum();
1002    #[allow(
1003        clippy::cast_precision_loss,
1004        clippy::cast_possible_truncation,
1005        clippy::cast_sign_loss
1006    )]
1007    let share = |e: usize| -> u32 {
1008        let length = lengths.get(e).copied().unwrap_or(0.0);
1009        ((wanted as f64 * length / total).ceil() as u32).max(2)
1010    };
1011    let stations = evenly_by(model, spine, stations, &share, false, tol)?;
1012    // Where two edges meet the walk stands twice; the loop takes it once,
1013    // the frame between it and the next station read off the next edge.
1014    let mut kept: Vec<SpineStation> = Vec::with_capacity(stations.len());
1015    for station in stations {
1016        if kept
1017            .last()
1018            .is_some_and(|p| p.at.distance(station.at) <= tol.confusion())
1019        {
1020            continue;
1021        }
1022        kept.push(station);
1023    }
1024    closed_pipe_shell(model, profile, spine, kept, law, twist, tolerance, tol)
1025}
1026
1027/// Whether a ring is all straight edges, or one circle about `axis_at`:
1028/// what [`make_loft`] lofts to its image scaled about `axis_at` exactly.
1029fn analytic_ring(
1030    model: &Model,
1031    ring: &Shape,
1032    axis_at: Point,
1033    tol: Tolerances,
1034) -> OgeomResult<bool> {
1035    let edges = explore(model, ring, Filter::OfType(ShapeType::Edge))?;
1036    let mut kinds = Vec::with_capacity(edges.len());
1037    for edge in &edges {
1038        kinds.push(spine_curve_of(model, edge)?.0);
1039    }
1040    Ok(kinds.iter().all(|c| matches!(c, Curve::Line(_)))
1041        || (kinds.len() == 1
1042            && matches!(&kinds[0], Curve::Circle(c)
1043                if c.circle().centre().distance(axis_at) <= tol.confusion())))
1044}
1045
1046/// Each spine edge's length, in the spine's order.
1047fn edge_lengths(model: &Model, spine: &Shape, tol: Tolerances) -> OgeomResult<Vec<f64>> {
1048    let edges: Vec<Shape> = match model.kind_of(spine)? {
1049        ShapeType::Edge => vec![spine.clone()],
1050        _ => model.ordered_children_of(spine)?,
1051    };
1052    let mut out = Vec::with_capacity(edges.len());
1053    for edge in &edges {
1054        let (curve, range) = spine_curve_of(model, edge)?;
1055        out.push(curve_length(&curve, range, tol)?);
1056    }
1057    Ok(out)
1058}