1use axiolid_contracts::{GeomError, GeomResult};
9use axiolid_core::{Point3, Scalar, Tolerance};
10use axiolid_mesh::TriMesh;
11use axiolid_primitive::Primitive;
12
13fn segments(radius: Scalar, tolerance: Scalar) -> usize {
18 if !(radius.is_finite() && radius > 0.0 && tolerance.is_finite() && tolerance > 0.0) {
19 return 3;
20 }
21 let ratio = 1.0 - (tolerance / radius).min(1.0);
22 let n = (core::f64::consts::PI / ratio.acos().max(1e-9)).ceil();
23 (n as usize).clamp(3, 4096)
24}
25
26pub fn tessellate_primitive(primitive: &Primitive, tolerance: Tolerance) -> GeomResult<TriMesh> {
32 let tol = tolerance.linear();
33 match primitive {
34 Primitive::Block { x, y, z } => block(*x, *y, *z),
35 Primitive::Sphere { radius } => sphere(*radius, tol),
36 Primitive::Cylinder { radius, height } => cylinder(*radius, *height, tol),
37 Primitive::Cone { radius, height } => cone(*radius, *height, tol),
38 Primitive::Pyramid { x, y, height } => pyramid(*x, *y, *height),
39 Primitive::Torus {
40 major_radius,
41 minor_radius,
42 } => torus(*major_radius, *minor_radius, tol),
43 Primitive::Wedge {
44 x,
45 y,
46 height,
47 top_x_min,
48 top_x_max,
49 top_y_min,
50 top_y_max,
51 } => wedge(
52 [*x, *y, *height],
53 [*top_x_min, *top_x_max],
54 [*top_y_min, *top_y_max],
55 ),
56 _ => Err(GeomError::Unsupported {
59 backend: crate::ScalarBoolean::ID,
60 operation: axiolid_contracts::Operation::Tessellation,
61 }),
62 }
63}
64
65fn positive(value: Scalar, what: &str) -> GeomResult<Scalar> {
67 if !value.is_finite() || value <= 0.0 {
68 return Err(GeomError::InvalidInput(format!(
69 "{what} must be positive and finite, got {value}"
70 )));
71 }
72 Ok(value)
73}
74
75fn block(x: Scalar, y: Scalar, z: Scalar) -> GeomResult<TriMesh> {
77 let (hx, hy, hz) = (
78 positive(x, "block x")? / 2.0,
79 positive(y, "block y")? / 2.0,
80 positive(z, "block z")? / 2.0,
81 );
82 let p = vec![
83 Point3::new(-hx, -hy, -hz),
84 Point3::new(hx, -hy, -hz),
85 Point3::new(hx, hy, -hz),
86 Point3::new(-hx, hy, -hz),
87 Point3::new(-hx, -hy, hz),
88 Point3::new(hx, -hy, hz),
89 Point3::new(hx, hy, hz),
90 Point3::new(-hx, hy, hz),
91 ];
92 let i = vec![
95 0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7, 6,
96 3, 0, 4, 3, 4, 7,
97 ];
98 Ok(TriMesh::new(p, i))
99}
100
101fn pyramid(x: Scalar, y: Scalar, height: Scalar) -> GeomResult<TriMesh> {
103 let (hx, hy) = (
104 positive(x, "pyramid x")? / 2.0,
105 positive(y, "pyramid y")? / 2.0,
106 );
107 let h = positive(height, "pyramid height")?;
108 let p = vec![
109 Point3::new(-hx, -hy, 0.0),
110 Point3::new(hx, -hy, 0.0),
111 Point3::new(hx, hy, 0.0),
112 Point3::new(-hx, hy, 0.0),
113 Point3::new(0.0, 0.0, h),
114 ];
115 let i = vec![0, 2, 1, 0, 3, 2, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4];
116 Ok(TriMesh::new(p, i))
117}
118
119fn wedge(
127 [x, y, height]: [Scalar; 3],
128 [x0, x1]: [Scalar; 2],
129 [y0, y1]: [Scalar; 2],
130) -> GeomResult<TriMesh> {
131 let (x, y, h) = (
132 positive(x, "wedge x")?,
133 positive(y, "wedge y")?,
134 positive(height, "wedge height")?,
135 );
136 for (value, what) in [
137 (x0, "wedge top x min"),
138 (x1, "wedge top x max"),
139 (y0, "wedge top y min"),
140 (y1, "wedge top y max"),
141 ] {
142 if !value.is_finite() {
143 return Err(GeomError::InvalidInput(format!(
144 "{what} must be finite, got {value}"
145 )));
146 }
147 }
148 if x0 > x1 || y0 > y1 {
149 return Err(GeomError::InvalidInput(format!(
150 "wedge top must have min <= max, got x {x0}..{x1}, y {y0}..{y1}"
151 )));
152 }
153 let corners = [
154 Point3::new(0.0, 0.0, 0.0),
155 Point3::new(x, 0.0, 0.0),
156 Point3::new(x, y, 0.0),
157 Point3::new(0.0, y, 0.0),
158 Point3::new(x0, y0, h),
159 Point3::new(x1, y0, h),
160 Point3::new(x1, y1, h),
161 Point3::new(x0, y1, h),
162 ];
163 const FACES: [[usize; 4]; 6] = [
166 [0, 3, 2, 1],
167 [4, 5, 6, 7],
168 [0, 1, 5, 4],
169 [1, 2, 6, 5],
170 [2, 3, 7, 6],
171 [3, 0, 4, 7],
172 ];
173 let mut p: Vec<Point3> = Vec::with_capacity(8);
174 let index: Vec<u32> = corners
175 .iter()
176 .map(|&c| match p.iter().position(|&q| q == c) {
177 Some(k) => k as u32,
178 None => {
179 p.push(c);
180 (p.len() - 1) as u32
181 }
182 })
183 .collect();
184 let mut i = Vec::with_capacity(36);
185 for face in FACES {
186 let mut ring: Vec<u32> = Vec::with_capacity(4);
187 for corner in face {
188 let v = index[corner];
189 if ring.last() != Some(&v) {
190 ring.push(v);
191 }
192 }
193 if ring.len() > 1 && ring.first() == ring.last() {
194 ring.pop();
195 }
196 if ring.len() < 3 {
197 continue;
198 }
199 for k in 1..ring.len() - 1 {
201 i.extend([ring[0], ring[k], ring[k + 1]]);
202 }
203 }
204 Ok(TriMesh::new(p, i))
205}
206
207fn torus(major: Scalar, minor: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
215 let big = positive(major, "torus major radius")?;
216 let r = positive(minor, "torus minor radius")?;
217 if r >= big {
218 let kind = if r == big {
219 "a horn torus (minor radius equal to major)"
220 } else {
221 "a spindle torus (minor radius above major)"
222 };
223 return Err(GeomError::InvalidInput(format!(
224 "torus minor radius {r} must be below major radius {big}: {kind} \
225 does not bound a two-manifold solid"
226 )));
227 }
228 let n = segments(big + r, tol);
229 let m = segments(r, tol);
230 let mut p = Vec::with_capacity(n * m);
231 for i in 0..n {
232 let theta = core::f64::consts::TAU * (i as Scalar) / (n as Scalar);
233 for j in 0..m {
234 let phi = core::f64::consts::TAU * (j as Scalar) / (m as Scalar);
235 let rho = big + r * phi.cos();
236 p.push(Point3::new(
237 rho * theta.cos(),
238 rho * theta.sin(),
239 r * phi.sin(),
240 ));
241 }
242 }
243 let at = |i: usize, j: usize| ((i % n) * m + (j % m)) as u32;
244 let mut idx = Vec::with_capacity(n * m * 6);
245 for i in 0..n {
246 for j in 0..m {
247 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
251 idx.extend([a, b, c]);
252 idx.extend([a, c, d]);
253 }
254 }
255 Ok(TriMesh::new(p, idx))
256}
257
258fn ring(radius: Scalar, z: Scalar, n: usize) -> Vec<Point3> {
260 (0..n)
261 .map(|k| {
262 let a = core::f64::consts::TAU * (k as Scalar) / (n as Scalar);
263 Point3::new(radius * a.cos(), radius * a.sin(), z)
264 })
265 .collect()
266}
267
268fn cylinder(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
270 let r = positive(radius, "cylinder radius")?;
271 let h = positive(height, "cylinder height")?;
272 let n = segments(r, tol);
273 let mut p = ring(r, 0.0, n);
274 p.extend(ring(r, h, n));
275 p.push(Point3::new(0.0, 0.0, 0.0));
276 p.push(Point3::new(0.0, 0.0, h));
277 let (bc, tc) = (2 * n, 2 * n + 1);
278 let mut i = Vec::with_capacity(n * 12);
279 for k in 0..n {
280 let (a, b) = (k, (k + 1) % n);
281 i.extend([a as u32, b as u32, (b + n) as u32]);
284 i.extend([a as u32, (b + n) as u32, (a + n) as u32]);
285 i.extend([bc as u32, b as u32, a as u32]);
286 i.extend([tc as u32, (a + n) as u32, (b + n) as u32]);
287 }
288 Ok(TriMesh::new(p, i))
289}
290
291fn cone(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
293 let r = positive(radius, "cone radius")?;
294 let h = positive(height, "cone height")?;
295 let n = segments(r, tol);
296 let mut p = ring(r, 0.0, n);
297 p.push(Point3::new(0.0, 0.0, 0.0));
298 p.push(Point3::new(0.0, 0.0, h));
299 let (base, apex) = (n, n + 1);
300 let mut i = Vec::with_capacity(n * 6);
301 for k in 0..n {
302 let (a, b) = (k as u32, ((k + 1) % n) as u32);
303 i.extend([base as u32, b, a]);
304 i.extend([a, b, apex as u32]);
305 }
306 Ok(TriMesh::new(p, i))
307}
308
309fn sphere(radius: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
311 let r = positive(radius, "sphere radius")?;
312 let n = segments(r, tol);
313 let stacks = (n / 2).max(2);
316 let mut p = Vec::with_capacity((stacks + 1) * n);
317 for i in 0..=stacks {
318 let v = core::f64::consts::PI * (i as Scalar) / (stacks as Scalar);
319 for j in 0..n {
320 let u = core::f64::consts::TAU * (j as Scalar) / (n as Scalar);
321 p.push(Point3::new(
322 r * v.sin() * u.cos(),
323 r * v.sin() * u.sin(),
324 r * v.cos(),
325 ));
326 }
327 }
328 let mut idx = Vec::with_capacity(stacks * n * 6);
329 for i in 0..stacks {
330 for j in 0..n {
331 let jn = (j + 1) % n;
332 let row = |r: usize, k: usize| -> u32 {
335 if r == 0 || r == stacks {
336 (r * n) as u32
337 } else {
338 (r * n + k) as u32
339 }
340 };
341 let a = row(i, j);
342 let b = row(i, jn);
343 let c = row(i + 1, j);
344 let d = row(i + 1, jn);
345 if i > 0 {
347 idx.extend([a, c, b]);
348 }
349 if i + 1 < stacks {
350 idx.extend([b, c, d]);
351 }
352 }
353 }
354 Ok(TriMesh::new(p, idx))
355}