axiolid_construct/
loft.rs1use axiolid_contracts::{GeomError, GeomResult};
13use axiolid_core::{Point2, Point3, Scalar, Vec3};
14use axiolid_mesh::TriMesh;
15
16use crate::profile::Rings;
17
18pub struct Station {
20 pub loops: Vec<Vec<Point3>>,
22}
23
24pub fn place(rings: &Rings, mut f: impl FnMut(Point2) -> Point3) -> Station {
30 let mut loops = Vec::with_capacity(1 + rings.holes.len());
31 loops.push(rings.outer.iter().map(|p| f(*p)).collect());
32 for hole in &rings.holes {
33 loops.push(hole.iter().map(|p| f(*p)).collect());
34 }
35 Station { loops }
36}
37
38pub fn loft(rings: &Rings, stations: &[Station], closed: bool) -> GeomResult<TriMesh> {
48 if stations.len() < 2 {
49 return Err(GeomError::InvalidInput(format!(
50 "a loft needs at least two stations, got {}",
51 stations.len()
52 )));
53 }
54 let shape: Vec<usize> = stations[0].loops.iter().map(|r| r.len()).collect();
55 for s in stations {
56 let this: Vec<usize> = s.loops.iter().map(|r| r.len()).collect();
57 if this != shape {
58 return Err(GeomError::InvalidInput(
59 "loft stations must share the profile's ring structure".to_owned(),
60 ));
61 }
62 }
63 let per_station: usize = shape.iter().sum();
64 let mut positions: Vec<Point3> = Vec::with_capacity(stations.len() * per_station);
65 for s in stations {
66 for ring in &s.loops {
67 positions.extend(ring.iter().copied());
68 }
69 }
70 let spans = if closed {
72 stations.len()
73 } else {
74 stations.len() - 1
75 };
76 let mut indices: Vec<u32> = Vec::new();
77 for s in 0..spans {
78 let a0 = s * per_station;
79 let b0 = ((s + 1) % stations.len()) * per_station;
80 let mut base = 0usize;
81 for m in shape.iter() {
82 for k in 0..*m {
83 let kn = (k + 1) % *m;
84 let (a, b) = ((a0 + base + k) as u32, (a0 + base + kn) as u32);
85 let (c, d) = ((b0 + base + k) as u32, (b0 + base + kn) as u32);
86 indices.extend([a, b, d, a, d, c]);
91 }
92 base += *m;
93 }
94 }
95 if !closed {
98 let (points, tris) = crate::profile::triangulate(rings)?;
99 if points.len() != per_station {
100 return Err(GeomError::Degenerate(format!(
101 "cap triangulation produced {} points for {per_station} ring points",
102 points.len()
103 )));
104 }
105 let last = ((stations.len() - 1) * per_station) as u32;
106 for t in &tris {
107 indices.extend([t[0], t[2], t[1]]);
108 indices.extend([last + t[0], last + t[1], last + t[2]]);
109 }
110 }
111 Ok(TriMesh::new(positions, indices))
112}
113
114pub struct Frame {
120 pub origin: Point3,
122 pub x: Vec3,
124 pub y: Vec3,
126}
127
128impl Frame {
129 pub fn from_reference(origin: Point3, tangent: Vec3, reference: Vec3) -> GeomResult<Self> {
137 let t = tangent.normalize_or_zero();
138 if t == Vec3::ZERO {
139 return Err(GeomError::InvalidInput(
140 "sweep tangent must be a non-zero direction".to_owned(),
141 ));
142 }
143 let x = (reference - t * t.dot(reference)).normalize_or_zero();
144 if x == Vec3::ZERO {
145 return Err(GeomError::InvalidInput(
146 "sweep reference direction must not be parallel to the directrix".to_owned(),
147 ));
148 }
149 Ok(Self {
150 origin,
151 x,
152 y: t.cross(x),
153 })
154 }
155}
156
157pub fn at(frame: &Frame, p: Point2) -> Point3 {
159 frame.origin + frame.x * p.x + frame.y * p.y
160}
161
162pub fn blend(a: Point2, b: Point2, t: Scalar) -> Point2 {
167 Point2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
168}
169
170pub fn loft_tapered(start: &Rings, end: &Rings, stations: &[Station]) -> GeomResult<TriMesh> {
176 let mut mesh = loft(start, stations, false)?;
177 let per_station: usize = stations[0].loops.iter().map(|r| r.len()).sum();
178 let spans = stations.len() - 1;
182 let ring_edges: usize = stations[0].loops.iter().map(|r| r.len()).sum();
183 let wall_indices = spans * ring_edges * 6;
184 mesh.indices.truncate(wall_indices);
185 let (near_pts, near_tris) = crate::profile::triangulate(start)?;
186 let (far_pts, far_tris) = crate::profile::triangulate(end)?;
187 if near_pts.len() != per_station || far_pts.len() != per_station {
188 return Err(GeomError::Degenerate(
189 "tapered cap triangulation disagrees with the station rings".to_owned(),
190 ));
191 }
192 let last = (spans * per_station) as u32;
193 for t in &near_tris {
194 mesh.indices.extend([t[0], t[2], t[1]]);
195 }
196 for t in &far_tris {
197 mesh.indices.extend([last + t[0], last + t[1], last + t[2]]);
198 }
199 Ok(mesh)
200}