Skip to main content

axiolid_construct/pipe/
band.rs

1//! The band of a bend's tube next to a corner mitred beside an arc (#248).
2//!
3//! A corner between two straight segments shares one mitre ring that lies
4//! on both exact cylinders (#245). Beside an arc it cannot: the mitre plane
5//! cuts the bend's torus in a different curve than the tube on the other
6//! side, so the exact solid, each tube cut by the plane, has a planar face
7//! between the two section curves where one tube ends and the other does
8//! not begin. The pipe therefore meshes the end of the bend as a band of
9//! stations whose vertices walk along the torus itself:
10//!
11//! - The band starts at a square section `back` short of the corner, and
12//!   each profile vertex of it turns about the bend's axis, along its own
13//!   circle of the torus, toward the mitre plane. The torus continued past
14//!   the arc's end is the exact tube there (the format's mitre at half
15//!   angle extends the tube to the plane), so every band vertex lies on the
16//!   exact surface.
17//! - `back` is the least turn at which the section short of the corner
18//!   lies wholly behind the plane, and `ahead` the least past it at which
19//!   the section lies wholly beyond, each by at least `MARGIN r`: each
20//!   vertex's circle then crosses the plane exactly once between them (its
21//!   height above the plane is `A0 + A1 cos psi + A2 sin psi`, with
22//!   opposite signs at the band's two ends less than a half turn apart),
23//!   and [`reach`] finds both from the closed form of the height's extremes
24//!   over the section circle. A corner whose plane does not clear the tube
25//!   within a quarter turn either way is refused.
26//! - Where the other side's ring is not this band's own cut, the band stops
27//!   a little short of the plane (`offset`) and its last span runs, nearly
28//!   in the plane, to the shared ring: that strip is the mesh of the planar
29//!   face between the two section curves.
30//!
31//! No proof by construction covers the band and the planar face; the
32//! compiler certifies them against the mesh by branch and bound
33//! (`axiolid_mesh_compile`'s deviation report), which is why the band's
34//! geometry is public.
35
36use core::f64::consts::FRAC_PI_2;
37
38use axiolid_contracts::{GeomError, GeomResult};
39use axiolid_core::{Point3, Scalar, Vec3};
40
41use crate::loft::Station;
42use crate::revolve::rotate;
43
44/// The tube of a bend next to a corner mitred beside it (#248): the torus
45/// about the circle through `corner` round `centre` and `axis`, from the
46/// square section `back` short of the corner to the mitre plane.
47#[derive(Debug, Clone, Copy, PartialEq)]
48#[non_exhaustive]
49pub struct MitreBand {
50    /// Centre of the bend's circle.
51    pub centre: Point3,
52    /// Unit axis of the bend, oriented so that a positive turn about it
53    /// carries the arc toward the corner and on past it.
54    pub axis: Vec3,
55    /// The arc's end at the corner. The mitre plane passes through it.
56    pub corner: Point3,
57    /// Unit normal of the mitre plane, pointing away from the band: the
58    /// band's part of the exact solid has non-positive height along it.
59    pub normal: Vec3,
60    /// How far the band reaches back from the corner, in radians: the
61    /// section turned by `-back` about `axis` from the corner's lies wholly
62    /// behind the mitre plane.
63    pub back: Scalar,
64    /// How far past the corner the tube is continued, in radians: the
65    /// section turned by `ahead` lies wholly beyond the plane, and the
66    /// plane cuts every circle of the torus strictly between the two.
67    pub ahead: Scalar,
68}
69
70/// How far, as a fraction of the disk radius, the band's two end sections
71/// must clear the mitre plane.
72pub(crate) const MARGIN: Scalar = 0.01;
73
74/// Angles tried for [`reach`], up to a quarter turn.
75const REACH_STEPS: usize = 1024;
76
77impl MitreBand {
78    /// The radius of the bend's circle.
79    #[must_use]
80    pub fn bend_radius(&self) -> Scalar {
81        (self.corner - self.centre).length()
82    }
83
84    /// The signed height of `p` above the mitre plane, along `normal`.
85    #[must_use]
86    pub fn height(&self, p: Point3) -> Scalar {
87        (p - self.corner).dot(self.normal)
88    }
89
90    /// `p` turned by `angle` about the bend's axis.
91    #[must_use]
92    pub fn turned(&self, p: Point3, angle: Scalar) -> Point3 {
93        rotate(p, self.centre, self.axis, angle)
94    }
95
96    /// The turn in `[lo, hi]` that carries `p` along its circle to height
97    /// `level`, found by bisection; `None` unless the height is below
98    /// `level` at `lo` and above it at `hi`.
99    pub(crate) fn turn_to(
100        &self,
101        p: Point3,
102        level: Scalar,
103        lo: Scalar,
104        hi: Scalar,
105    ) -> Option<Scalar> {
106        let f = |t: Scalar| self.height(self.turned(p, t)) - level;
107        let (mut a, mut b) = (lo, hi);
108        if !(f(a) < 0.0 && f(b) > 0.0) {
109            return None;
110        }
111        for _ in 0..200 {
112            let m = 0.5 * (a + b);
113            if m <= a || m >= b {
114                break;
115            }
116            if f(m) < 0.0 {
117                a = m;
118            } else {
119                b = m;
120            }
121        }
122        Some(0.5 * (a + b))
123    }
124
125    /// The band's stations from `from` (excluded), the square section at
126    /// its far end, toward the corner, ending with `target`, the ring the
127    /// pipe shares across the mitre.
128    ///
129    /// Each vertex of `from` turns along its circle by a fraction of the
130    /// turn that takes it to the plane (`offset` = `None`) or to `offset`
131    /// short of it (at most half its own height below the plane); the turns
132    /// are split into equal steps whose sagitta at `reach_radius` is within
133    /// `tol`. With `offset` the last band station stands that close to the
134    /// plane and `target` follows it; without, `target` takes the place of
135    /// the last one, which is the cut itself.
136    pub(crate) fn stations_toward(
137        &self,
138        from: &Station,
139        target: &Station,
140        offset: Option<Scalar>,
141        reach_radius: Scalar,
142        tol: Scalar,
143    ) -> GeomResult<Vec<Station>> {
144        let mut turns: Vec<Vec<Scalar>> = Vec::with_capacity(from.loops.len());
145        let mut widest: Scalar = 0.0;
146        for ring in &from.loops {
147            let mut row = Vec::with_capacity(ring.len());
148            for &p in ring {
149                let below = -self.height(p);
150                let level = offset.map_or(0.0, |cap| -cap.min(0.5 * below));
151                let t = self
152                    .turn_to(p, level, 0.0, self.back + self.ahead)
153                    .ok_or_else(|| {
154                        GeomError::Degenerate(
155                            "swept disk mitre band: a section vertex does not cross the mitre \
156                             plane within the band"
157                                .to_owned(),
158                        )
159                    })?;
160                widest = widest.max(t);
161                row.push(t);
162            }
163            turns.push(row);
164        }
165        let steps = crate::revolve::steps(reach_radius, widest, tol).map_err(|_| {
166            GeomError::BudgetExceeded {
167                resource: "swept disk mitre band steps",
168            }
169        })?;
170        let last = if offset.is_some() { steps } else { steps - 1 };
171        let mut out = Vec::with_capacity(last + 1);
172        for j in 1..=last {
173            let s = j as Scalar / steps as Scalar;
174            out.push(Station {
175                loops: from
176                    .loops
177                    .iter()
178                    .zip(&turns)
179                    .map(|(ring, row)| {
180                        ring.iter()
181                            .zip(row)
182                            .map(|(&p, &t)| self.turned(p, s * t))
183                            .collect()
184                    })
185                    .collect(),
186            });
187        }
188        out.push(target.clone());
189        Ok(out)
190    }
191
192    /// The section of the band's tube by the mitre plane, at the vertices
193    /// of `square`, a section standing square to the arc at the corner:
194    /// each vertex turned along its circle onto the plane.
195    pub(crate) fn cut(&self, square: &Station) -> GeomResult<Station> {
196        let loops = square
197            .loops
198            .iter()
199            .map(|ring| {
200                ring.iter()
201                    .map(|&p| {
202                        self.turn_to(p, 0.0, -self.back, self.ahead)
203                            .map(|t| self.turned(p, t))
204                            .ok_or_else(|| {
205                                GeomError::Degenerate(
206                                    "swept disk mitre band: a section vertex does not cross \
207                                     the mitre plane within the band"
208                                        .to_owned(),
209                                )
210                            })
211                    })
212                    .collect::<GeomResult<Vec<_>>>()
213            })
214            .collect::<GeomResult<Vec<_>>>()?;
215        Ok(Station { loops })
216    }
217}
218
219/// The least turns `(back, ahead)` (multiples of a quarter turn over
220/// [`REACH_STEPS`]) at which the section `back` short of the corner lies
221/// wholly at least `MARGIN radius` behind the plane through `corner` with
222/// unit normal `normal`, and the section `ahead` past it wholly that far
223/// beyond; `None` when either needs more than a quarter turn.
224///
225/// With `e` the unit vector from `centre` to `corner` (bend radius `R`),
226/// `t = axis x e` and a point `corner + r (cos b e + sin b axis)` of the
227/// corner's section turned by `psi`, the height above the plane is
228/// `R ((cos psi - 1) e.n + sin psi t.n) + r (cos b (cos psi e.n + sin psi
229/// t.n) + sin b axis.n)`; its extremes over `b` are the first term plus or
230/// minus `r` times the length of the second's coefficients. Any smaller
231/// radius (a bore, the centre) lies between them.
232pub(crate) fn reach(
233    centre: Point3,
234    axis: Vec3,
235    corner: Point3,
236    normal: Vec3,
237    radius: Scalar,
238) -> Option<(Scalar, Scalar)> {
239    let big = (corner - centre).length();
240    let e = (corner - centre) / big;
241    let t = axis.cross(e);
242    let (en, tn, an) = (e.dot(normal), t.dot(normal), axis.dot(normal));
243    let margin = MARGIN * radius;
244    let turns = || (1..=REACH_STEPS).map(|k| FRAC_PI_2 * k as Scalar / REACH_STEPS as Scalar);
245    let back = turns().find(|&psi| {
246        let (s, c) = psi.sin_cos();
247        big * ((c - 1.0) * en - s * tn) + radius * ((c * en - s * tn).powi(2) + an * an).sqrt()
248            <= -margin
249    })?;
250    let ahead = turns().find(|&psi| {
251        let (s, c) = psi.sin_cos();
252        big * ((c - 1.0) * en + s * tn) - radius * ((c * en + s * tn).powi(2) + an * an).sqrt()
253            >= margin
254    })?;
255    Some((back, ahead))
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn band(turn: Scalar, tilt: Vec3, big: Scalar, r: Scalar) -> MitreBand {
263        // A bend in the xy plane about +z ending at (big, 0, 0) heading +y;
264        // the mitre normal bisects +y and the outgoing direction.
265        let centre = Point3::ZERO;
266        let axis = Vec3::Z;
267        let corner = Point3::new(big, 0.0, 0.0);
268        let u = Vec3::Y;
269        let v = (u * turn.cos() + tilt.normalize() * turn.sin()).normalize();
270        let normal = (u + v).normalize();
271        let (back, ahead) = reach(centre, axis, corner, normal, r).expect("a band");
272        MitreBand {
273            centre,
274            axis,
275            corner,
276            normal,
277            back,
278            ahead,
279        }
280    }
281
282    #[test]
283    fn every_circle_of_the_band_crosses_the_plane_once_inside_it() {
284        let r = 0.05;
285        for (turn, tilt) in [
286            (0.3, Vec3::X),
287            (1.2, -Vec3::X),
288            (FRAC_PI_2, Vec3::X),
289            (FRAC_PI_2, -Vec3::X),
290            (1.0, Vec3::Z),
291            (0.8, Vec3::new(1.0, 0.0, 1.0)),
292        ] {
293            let b = band(turn, tilt, 0.2, r);
294            let e = Vec3::X;
295            for k in 0..360 {
296                let beta = core::f64::consts::TAU * k as Scalar / 360.0;
297                for rho in [r, 0.5 * r, 0.0] {
298                    let p = b.corner + (e * beta.cos() + Vec3::Z * beta.sin()) * rho;
299                    let lo = b.height(b.turned(p, -b.back));
300                    let hi = b.height(b.turned(p, b.ahead));
301                    assert!(lo <= -MARGIN * r * (1.0 - 1e-9), "{turn} {tilt:?}: {lo}");
302                    assert!(hi >= MARGIN * r * (1.0 - 1e-9), "{turn} {tilt:?}: {hi}");
303                    // Dense: exactly one sign change.
304                    let n = 400;
305                    let changes = (0..n)
306                        .filter(|&j| {
307                            let at = |j: usize| {
308                                b.height(b.turned(
309                                    p,
310                                    -b.back + (b.back + b.ahead) * j as Scalar / n as Scalar,
311                                ))
312                            };
313                            (at(j) < 0.0) != (at(j + 1) < 0.0)
314                        })
315                        .count();
316                    assert_eq!(changes, 1, "{turn} {tilt:?} {beta} {rho}");
317                }
318            }
319        }
320    }
321
322    #[test]
323    fn a_plane_that_wraps_round_the_bend_has_no_band() {
324        // A bend of radius 1.1 r turning a corner of nearly half a turn.
325        let r = 0.05;
326        let corner = Point3::new(1.1 * r, 0.0, 0.0);
327        let u = Vec3::Y;
328        let v = (u * 3.0f64.cos() + Vec3::X * 3.0f64.sin()).normalize();
329        let normal = (u + v).normalize();
330        assert!(reach(Point3::ZERO, Vec3::Z, corner, normal, r).is_none());
331    }
332}