use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point2, Point3, Scalar, Vec3};
use axiolid_mesh::TriMesh;
use crate::profile::Rings;
#[derive(Debug, Clone)]
pub struct Station {
pub loops: Vec<Vec<Point3>>,
}
pub fn place(rings: &Rings, mut f: impl FnMut(Point2) -> Point3) -> Station {
let mut loops = Vec::with_capacity(1 + rings.holes.len());
loops.push(rings.outer.iter().map(|p| f(*p)).collect());
for hole in &rings.holes {
loops.push(hole.iter().map(|p| f(*p)).collect());
}
Station { loops }
}
pub fn loft(rings: &Rings, stations: &[Station], closed: bool) -> GeomResult<TriMesh> {
if stations.len() < 2 {
return Err(GeomError::InvalidInput(format!(
"a loft needs at least two stations, got {}",
stations.len()
)));
}
let shape: Vec<usize> = stations[0].loops.iter().map(|r| r.len()).collect();
for s in stations {
let this: Vec<usize> = s.loops.iter().map(|r| r.len()).collect();
if this != shape {
return Err(GeomError::InvalidInput(
"loft stations must share the profile's ring structure".to_owned(),
));
}
}
let per_station: usize = shape.iter().sum();
let mut positions: Vec<Point3> = Vec::with_capacity(stations.len() * per_station);
for s in stations {
for ring in &s.loops {
positions.extend(ring.iter().copied());
}
}
let spans = if closed {
stations.len()
} else {
stations.len() - 1
};
let mut indices: Vec<u32> = Vec::new();
for s in 0..spans {
let a0 = s * per_station;
let b0 = ((s + 1) % stations.len()) * per_station;
let mut base = 0usize;
for m in shape.iter() {
for k in 0..*m {
let kn = (k + 1) % *m;
let (a, b) = ((a0 + base + k) as u32, (a0 + base + kn) as u32);
let (c, d) = ((b0 + base + k) as u32, (b0 + base + kn) as u32);
indices.extend([a, b, d, a, d, c]);
}
base += *m;
}
}
if !closed {
let (points, tris) = crate::profile::triangulate(rings)?;
if points.len() != per_station {
return Err(GeomError::Degenerate(format!(
"cap triangulation produced {} points for {per_station} ring points",
points.len()
)));
}
let last = ((stations.len() - 1) * per_station) as u32;
for t in &tris {
indices.extend([t[0], t[2], t[1]]);
indices.extend([last + t[0], last + t[1], last + t[2]]);
}
}
Ok(TriMesh::new(positions, indices))
}
pub fn quad_deviation(a: Point3, b: Point3, c: Point3, d: Point3) -> Scalar {
let twist = (a - b - c + d).length() / 4.0;
let mut best = twist
.min((b - a).length())
.min((c - a).length())
.min(2.0 * (d - c).length())
.min(2.0 * (d - b).length());
if let Some(n) = unit((b - a).cross(c - a)) {
let g = (d - a).dot(n);
if convex_projection([a, b, d - n * g, c], n) {
best = best.min(g.abs());
}
}
if let Some(n) = unit((d - a).cross(c - b)) {
let e = 0.5 * (a - b).dot(n);
let (a2, d2) = (a - n * e, d - n * e);
let (b2, c2) = (b + n * e, c + n * e);
if convex_projection([a2, b2, d2, c2], n) {
best = best.min(2.0 * e.abs());
}
}
best
}
fn unit(v: Vec3) -> Option<Vec3> {
let length = v.length();
(length > 0.0 && length.is_finite()).then(|| v / length)
}
fn convex_projection(corners: [Point3; 4], n: Vec3) -> bool {
let scale = (0..4)
.map(|k| (corners[(k + 1) % 4] - corners[k]).length())
.fold(0.0, Scalar::max);
let slack = 1e-12 * scale * scale;
let turns: Vec<Scalar> = (0..4)
.map(|k| {
let (p, q, r) = (corners[k], corners[(k + 1) % 4], corners[(k + 2) % 4]);
(q - p).cross(r - q).dot(n)
})
.collect();
turns.iter().all(|t| *t >= -slack) || turns.iter().all(|t| *t <= slack)
}
pub(crate) fn span_deviation(from: &Station, to: &Station) -> Scalar {
let mut worst: Scalar = 0.0;
for (ring, next) in from.loops.iter().zip(&to.loops) {
let m = ring.len().min(next.len());
for k in 0..m {
let kn = (k + 1) % m;
worst = worst.max(quad_deviation(ring[k], ring[kn], next[k], next[kn]));
}
}
worst
}
#[derive(Debug, Clone)]
pub struct Frame {
pub origin: Point3,
pub x: Vec3,
pub y: Vec3,
}
impl Frame {
pub fn from_reference(origin: Point3, tangent: Vec3, reference: Vec3) -> GeomResult<Self> {
let t = tangent.normalize_or_zero();
if t == Vec3::ZERO {
return Err(GeomError::InvalidInput(
"sweep tangent must be a non-zero direction".to_owned(),
));
}
let x = (reference - t * t.dot(reference)).normalize_or_zero();
if x == Vec3::ZERO {
return Err(GeomError::InvalidInput(
"sweep reference direction must not be parallel to the directrix".to_owned(),
));
}
Ok(Self {
origin,
x,
y: t.cross(x),
})
}
}
pub fn at(frame: &Frame, p: Point2) -> Point3 {
frame.origin + frame.x * p.x + frame.y * p.y
}
pub fn blend(a: Point2, b: Point2, t: Scalar) -> Point2 {
Point2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
}
pub fn loft_tapered(start: &Rings, end: &Rings, stations: &[Station]) -> GeomResult<TriMesh> {
let mut mesh = loft(start, stations, false)?;
let per_station: usize = stations[0].loops.iter().map(|r| r.len()).sum();
let spans = stations.len() - 1;
let ring_edges: usize = stations[0].loops.iter().map(|r| r.len()).sum();
let wall_indices = spans * ring_edges * 6;
mesh.indices.truncate(wall_indices);
let (near_pts, near_tris) = crate::profile::triangulate(start)?;
let (far_pts, far_tris) = crate::profile::triangulate(end)?;
if near_pts.len() != per_station || far_pts.len() != per_station {
return Err(GeomError::Degenerate(
"tapered cap triangulation disagrees with the station rings".to_owned(),
));
}
let last = (spans * per_station) as u32;
for t in &near_tris {
mesh.indices.extend([t[0], t[2], t[1]]);
}
for t in &far_tris {
mesh.indices.extend([last + t[0], last + t[1], last + t[2]]);
}
Ok(mesh)
}
#[cfg(test)]
mod tests {
use super::*;
use axiolid_measure::proximity::closest_point_on_triangle;
fn measured(a: Point3, b: Point3, c: Point3, d: Point3) -> Scalar {
let n = 64;
let mut worst: Scalar = 0.0;
for i in 0..=n {
for j in 0..=n {
let (x, y) = (i as Scalar / n as Scalar, j as Scalar / n as Scalar);
let p = (a * (1.0 - x) + b * x) * (1.0 - y) + (c * (1.0 - x) + d * x) * y;
let near = [[a, b, d], [a, d, c]]
.iter()
.filter_map(|t| closest_point_on_triangle(p, *t).ok())
.map(|q| p.distance(q))
.fold(Scalar::INFINITY, Scalar::min);
worst = worst.max(near);
}
}
worst
}
fn quads() -> Vec<[Point3; 4]> {
let mut seed: u64 = 0x2310_5eed;
let mut next = move || {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((seed >> 11) as Scalar / (1u64 << 53) as Scalar) * 2.0 - 1.0
};
let mut out = vec![
[
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.2, 1.0, 0.0),
],
[
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(0.2, 0.2, 0.0),
],
[
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 1.0),
],
[
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.3),
],
];
for _ in 0..200 {
let mut q = [Point3::ZERO; 4];
for p in &mut q {
*p = Point3::new(next(), next(), 0.3 * next());
}
out.push(q);
}
out
}
#[test]
fn the_quad_bound_holds_for_every_quad() {
for [a, b, c, d] in quads() {
let bound = quad_deviation(a, b, c, d);
let got = measured(a, b, c, d);
assert!(
got <= bound * (1.0 + 1e-9) + 1e-12,
"quad {a:?} {b:?} {c:?} {d:?}: patch {got} from its triangles, bound {bound}"
);
}
}
#[test]
fn a_planar_convex_quad_costs_nothing() {
let [a, b, c, d] = quads()[0];
assert!(quad_deviation(a, b, c, d) <= 1e-15);
}
#[test]
fn a_twisted_quad_is_bounded_tightly() {
let [a, b, c, d] = quads()[2];
let bound = quad_deviation(a, b, c, d);
assert!((bound - 0.25).abs() <= 1e-12, "{bound}");
let got = measured(a, b, c, d);
assert!((got - 0.25 / 2f64.sqrt()).abs() <= 1e-3, "{got}");
}
}