use core::f64::consts::FRAC_PI_2;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point3, Scalar, Vec3};
use crate::loft::Station;
use crate::revolve::rotate;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct MitreBand {
pub centre: Point3,
pub axis: Vec3,
pub corner: Point3,
pub normal: Vec3,
pub back: Scalar,
pub ahead: Scalar,
}
pub(crate) const MARGIN: Scalar = 0.01;
const REACH_STEPS: usize = 1024;
impl MitreBand {
#[must_use]
pub fn bend_radius(&self) -> Scalar {
(self.corner - self.centre).length()
}
#[must_use]
pub fn height(&self, p: Point3) -> Scalar {
(p - self.corner).dot(self.normal)
}
#[must_use]
pub fn turned(&self, p: Point3, angle: Scalar) -> Point3 {
rotate(p, self.centre, self.axis, angle)
}
pub(crate) fn turn_to(
&self,
p: Point3,
level: Scalar,
lo: Scalar,
hi: Scalar,
) -> Option<Scalar> {
let f = |t: Scalar| self.height(self.turned(p, t)) - level;
let (mut a, mut b) = (lo, hi);
if !(f(a) < 0.0 && f(b) > 0.0) {
return None;
}
for _ in 0..200 {
let m = 0.5 * (a + b);
if m <= a || m >= b {
break;
}
if f(m) < 0.0 {
a = m;
} else {
b = m;
}
}
Some(0.5 * (a + b))
}
pub(crate) fn stations_toward(
&self,
from: &Station,
target: &Station,
offset: Option<Scalar>,
reach_radius: Scalar,
tol: Scalar,
) -> GeomResult<Vec<Station>> {
let mut turns: Vec<Vec<Scalar>> = Vec::with_capacity(from.loops.len());
let mut widest: Scalar = 0.0;
for ring in &from.loops {
let mut row = Vec::with_capacity(ring.len());
for &p in ring {
let below = -self.height(p);
let level = offset.map_or(0.0, |cap| -cap.min(0.5 * below));
let t = self
.turn_to(p, level, 0.0, self.back + self.ahead)
.ok_or_else(|| {
GeomError::Degenerate(
"swept disk mitre band: a section vertex does not cross the mitre \
plane within the band"
.to_owned(),
)
})?;
widest = widest.max(t);
row.push(t);
}
turns.push(row);
}
let steps = crate::revolve::steps(reach_radius, widest, tol).map_err(|_| {
GeomError::BudgetExceeded {
resource: "swept disk mitre band steps",
}
})?;
let last = if offset.is_some() { steps } else { steps - 1 };
let mut out = Vec::with_capacity(last + 1);
for j in 1..=last {
let s = j as Scalar / steps as Scalar;
out.push(Station {
loops: from
.loops
.iter()
.zip(&turns)
.map(|(ring, row)| {
ring.iter()
.zip(row)
.map(|(&p, &t)| self.turned(p, s * t))
.collect()
})
.collect(),
});
}
out.push(target.clone());
Ok(out)
}
pub(crate) fn cut(&self, square: &Station) -> GeomResult<Station> {
let loops = square
.loops
.iter()
.map(|ring| {
ring.iter()
.map(|&p| {
self.turn_to(p, 0.0, -self.back, self.ahead)
.map(|t| self.turned(p, t))
.ok_or_else(|| {
GeomError::Degenerate(
"swept disk mitre band: a section vertex does not cross \
the mitre plane within the band"
.to_owned(),
)
})
})
.collect::<GeomResult<Vec<_>>>()
})
.collect::<GeomResult<Vec<_>>>()?;
Ok(Station { loops })
}
}
pub(crate) fn reach(
centre: Point3,
axis: Vec3,
corner: Point3,
normal: Vec3,
radius: Scalar,
) -> Option<(Scalar, Scalar)> {
let big = (corner - centre).length();
let e = (corner - centre) / big;
let t = axis.cross(e);
let (en, tn, an) = (e.dot(normal), t.dot(normal), axis.dot(normal));
let margin = MARGIN * radius;
let turns = || (1..=REACH_STEPS).map(|k| FRAC_PI_2 * k as Scalar / REACH_STEPS as Scalar);
let back = turns().find(|&psi| {
let (s, c) = psi.sin_cos();
big * ((c - 1.0) * en - s * tn) + radius * ((c * en - s * tn).powi(2) + an * an).sqrt()
<= -margin
})?;
let ahead = turns().find(|&psi| {
let (s, c) = psi.sin_cos();
big * ((c - 1.0) * en + s * tn) - radius * ((c * en + s * tn).powi(2) + an * an).sqrt()
>= margin
})?;
Some((back, ahead))
}
#[cfg(test)]
mod tests {
use super::*;
fn band(turn: Scalar, tilt: Vec3, big: Scalar, r: Scalar) -> MitreBand {
let centre = Point3::ZERO;
let axis = Vec3::Z;
let corner = Point3::new(big, 0.0, 0.0);
let u = Vec3::Y;
let v = (u * turn.cos() + tilt.normalize() * turn.sin()).normalize();
let normal = (u + v).normalize();
let (back, ahead) = reach(centre, axis, corner, normal, r).expect("a band");
MitreBand {
centre,
axis,
corner,
normal,
back,
ahead,
}
}
#[test]
fn every_circle_of_the_band_crosses_the_plane_once_inside_it() {
let r = 0.05;
for (turn, tilt) in [
(0.3, Vec3::X),
(1.2, -Vec3::X),
(FRAC_PI_2, Vec3::X),
(FRAC_PI_2, -Vec3::X),
(1.0, Vec3::Z),
(0.8, Vec3::new(1.0, 0.0, 1.0)),
] {
let b = band(turn, tilt, 0.2, r);
let e = Vec3::X;
for k in 0..360 {
let beta = core::f64::consts::TAU * k as Scalar / 360.0;
for rho in [r, 0.5 * r, 0.0] {
let p = b.corner + (e * beta.cos() + Vec3::Z * beta.sin()) * rho;
let lo = b.height(b.turned(p, -b.back));
let hi = b.height(b.turned(p, b.ahead));
assert!(lo <= -MARGIN * r * (1.0 - 1e-9), "{turn} {tilt:?}: {lo}");
assert!(hi >= MARGIN * r * (1.0 - 1e-9), "{turn} {tilt:?}: {hi}");
let n = 400;
let changes = (0..n)
.filter(|&j| {
let at = |j: usize| {
b.height(b.turned(
p,
-b.back + (b.back + b.ahead) * j as Scalar / n as Scalar,
))
};
(at(j) < 0.0) != (at(j + 1) < 0.0)
})
.count();
assert_eq!(changes, 1, "{turn} {tilt:?} {beta} {rho}");
}
}
}
}
#[test]
fn a_plane_that_wraps_round_the_bend_has_no_band() {
let r = 0.05;
let corner = Point3::new(1.1 * r, 0.0, 0.0);
let u = Vec3::Y;
let v = (u * 3.0f64.cos() + Vec3::X * 3.0f64.sin()).normalize();
let normal = (u + v).normalize();
assert!(reach(Point3::ZERO, Vec3::Z, corner, normal, r).is_none());
}
}