use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point2, Point3, Scalar, Vec3};
use axiolid_mesh::TriMesh;
use crate::profile::Rings;
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 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)
}