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
26fn segments_within(radius: Scalar, tolerance: Scalar) -> GeomResult<usize> {
31 if !(tolerance.is_finite() && tolerance > 0.0) {
32 return Err(GeomError::InvalidInput(format!(
33 "chord budget must be positive and finite, got {tolerance}"
34 )));
35 }
36 let n = segments(radius, tolerance);
37 if n == 4096 && radius * (1.0 - (core::f64::consts::PI / 4096.0).cos()) > tolerance {
38 return Err(GeomError::BudgetExceeded {
39 resource: "primitive surface segments",
40 });
41 }
42 Ok(n)
43}
44
45pub fn tessellate_primitive(primitive: &Primitive, tolerance: Tolerance) -> GeomResult<TriMesh> {
65 let tol = tolerance.linear();
66 match primitive {
67 Primitive::Block { x, y, z } => block(*x, *y, *z),
68 Primitive::Sphere { radius } => sphere(*radius, tol),
69 Primitive::Cylinder { radius, height } => cylinder(*radius, *height, tol),
70 Primitive::Cone { radius, height } => cone(*radius, *height, tol),
71 Primitive::Pyramid { x, y, height } => pyramid(*x, *y, *height),
72 Primitive::Torus {
73 major_radius,
74 minor_radius,
75 } => torus(*major_radius, *minor_radius, tol),
76 Primitive::Wedge {
77 x,
78 y,
79 height,
80 top_x_min,
81 top_x_max,
82 top_y_min,
83 top_y_max,
84 } => wedge(
85 [*x, *y, *height],
86 [*top_x_min, *top_x_max],
87 [*top_y_min, *top_y_max],
88 ),
89 _ => Err(GeomError::Unsupported {
92 backend: crate::ScalarBoolean::ID,
93 operation: axiolid_contracts::Operation::Tessellation,
94 }),
95 }
96}
97
98fn positive(value: Scalar, what: &str) -> GeomResult<Scalar> {
100 if !value.is_finite() || value <= 0.0 {
101 return Err(GeomError::InvalidInput(format!(
102 "{what} must be positive and finite, got {value}"
103 )));
104 }
105 Ok(value)
106}
107
108fn block(x: Scalar, y: Scalar, z: Scalar) -> GeomResult<TriMesh> {
110 let (hx, hy, hz) = (
111 positive(x, "block x")? / 2.0,
112 positive(y, "block y")? / 2.0,
113 positive(z, "block z")? / 2.0,
114 );
115 let p = vec![
116 Point3::new(-hx, -hy, -hz),
117 Point3::new(hx, -hy, -hz),
118 Point3::new(hx, hy, -hz),
119 Point3::new(-hx, hy, -hz),
120 Point3::new(-hx, -hy, hz),
121 Point3::new(hx, -hy, hz),
122 Point3::new(hx, hy, hz),
123 Point3::new(-hx, hy, hz),
124 ];
125 let i = vec![
128 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,
129 3, 0, 4, 3, 4, 7,
130 ];
131 Ok(TriMesh::new(p, i))
132}
133
134fn pyramid(x: Scalar, y: Scalar, height: Scalar) -> GeomResult<TriMesh> {
136 let (hx, hy) = (
137 positive(x, "pyramid x")? / 2.0,
138 positive(y, "pyramid y")? / 2.0,
139 );
140 let h = positive(height, "pyramid height")?;
141 let p = vec![
142 Point3::new(-hx, -hy, 0.0),
143 Point3::new(hx, -hy, 0.0),
144 Point3::new(hx, hy, 0.0),
145 Point3::new(-hx, hy, 0.0),
146 Point3::new(0.0, 0.0, h),
147 ];
148 let i = vec![0, 2, 1, 0, 3, 2, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4];
149 Ok(TriMesh::new(p, i))
150}
151
152fn wedge(
160 [x, y, height]: [Scalar; 3],
161 [x0, x1]: [Scalar; 2],
162 [y0, y1]: [Scalar; 2],
163) -> GeomResult<TriMesh> {
164 let (x, y, h) = (
165 positive(x, "wedge x")?,
166 positive(y, "wedge y")?,
167 positive(height, "wedge height")?,
168 );
169 for (value, what) in [
170 (x0, "wedge top x min"),
171 (x1, "wedge top x max"),
172 (y0, "wedge top y min"),
173 (y1, "wedge top y max"),
174 ] {
175 if !value.is_finite() {
176 return Err(GeomError::InvalidInput(format!(
177 "{what} must be finite, got {value}"
178 )));
179 }
180 }
181 if x0 > x1 || y0 > y1 {
182 return Err(GeomError::InvalidInput(format!(
183 "wedge top must have min <= max, got x {x0}..{x1}, y {y0}..{y1}"
184 )));
185 }
186 let corners = [
187 Point3::new(0.0, 0.0, 0.0),
188 Point3::new(x, 0.0, 0.0),
189 Point3::new(x, y, 0.0),
190 Point3::new(0.0, y, 0.0),
191 Point3::new(x0, y0, h),
192 Point3::new(x1, y0, h),
193 Point3::new(x1, y1, h),
194 Point3::new(x0, y1, h),
195 ];
196 const FACES: [[usize; 4]; 6] = [
199 [0, 3, 2, 1],
200 [4, 5, 6, 7],
201 [0, 1, 5, 4],
202 [1, 2, 6, 5],
203 [2, 3, 7, 6],
204 [3, 0, 4, 7],
205 ];
206 let mut p: Vec<Point3> = Vec::with_capacity(8);
207 let index: Vec<u32> = corners
208 .iter()
209 .map(|&c| match p.iter().position(|&q| q == c) {
210 Some(k) => k as u32,
211 None => {
212 p.push(c);
213 (p.len() - 1) as u32
214 }
215 })
216 .collect();
217 let mut i = Vec::with_capacity(36);
218 for face in FACES {
219 let mut ring: Vec<u32> = Vec::with_capacity(4);
220 for corner in face {
221 let v = index[corner];
222 if ring.last() != Some(&v) {
223 ring.push(v);
224 }
225 }
226 if ring.len() > 1 && ring.first() == ring.last() {
227 ring.pop();
228 }
229 if ring.len() < 3 {
230 continue;
231 }
232 for k in 1..ring.len() - 1 {
234 i.extend([ring[0], ring[k], ring[k + 1]]);
235 }
236 }
237 Ok(TriMesh::new(p, i))
238}
239
240fn torus(major: Scalar, minor: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
249 let big = positive(major, "torus major radius")?;
250 let r = positive(minor, "torus minor radius")?;
251 if r >= big {
252 let kind = if r == big {
253 "a horn torus (minor radius equal to major)"
254 } else {
255 "a spindle torus (minor radius above major)"
256 };
257 return Err(GeomError::InvalidInput(format!(
258 "torus minor radius {r} must be below major radius {big}: {kind} \
259 does not bound a two-manifold solid"
260 )));
261 }
262 let n = segments_within(big + r, 0.5 * tol)?;
263 let m = segments_within(r, 0.5 * tol)?;
264 let mut p = Vec::with_capacity(n * m);
265 for i in 0..n {
266 let theta = core::f64::consts::TAU * (i as Scalar) / (n as Scalar);
267 for j in 0..m {
268 let phi = core::f64::consts::TAU * (j as Scalar) / (m as Scalar);
269 let rho = big + r * phi.cos();
270 p.push(Point3::new(
271 rho * theta.cos(),
272 rho * theta.sin(),
273 r * phi.sin(),
274 ));
275 }
276 }
277 let at = |i: usize, j: usize| ((i % n) * m + (j % m)) as u32;
278 let mut idx = Vec::with_capacity(n * m * 6);
279 for i in 0..n {
280 for j in 0..m {
281 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
285 idx.extend([a, b, c]);
286 idx.extend([a, c, d]);
287 }
288 }
289 Ok(TriMesh::new(p, idx))
290}
291
292fn ring(radius: Scalar, z: Scalar, n: usize) -> Vec<Point3> {
294 (0..n)
295 .map(|k| {
296 let a = core::f64::consts::TAU * (k as Scalar) / (n as Scalar);
297 Point3::new(radius * a.cos(), radius * a.sin(), z)
298 })
299 .collect()
300}
301
302fn cylinder(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
314 let r = positive(radius, "cylinder radius")?;
315 let h = positive(height, "cylinder height")?;
316 let n = segments_within(r, tol)?;
317 let mut p = ring(r, 0.0, n);
318 p.extend(ring(r, h, n));
319 p.push(Point3::new(0.0, 0.0, 0.0));
320 p.push(Point3::new(0.0, 0.0, h));
321 let (bc, tc) = (2 * n, 2 * n + 1);
322 let mut i = Vec::with_capacity(n * 12);
323 for k in 0..n {
324 let (a, b) = (k, (k + 1) % n);
325 i.extend([a as u32, b as u32, (b + n) as u32]);
328 i.extend([a as u32, (b + n) as u32, (a + n) as u32]);
329 i.extend([bc as u32, b as u32, a as u32]);
330 i.extend([tc as u32, (a + n) as u32, (b + n) as u32]);
331 }
332 Ok(TriMesh::new(p, i))
333}
334
335fn cone(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
343 let r = positive(radius, "cone radius")?;
344 let h = positive(height, "cone height")?;
345 let n = segments_within(r, tol)?;
346 let mut p = ring(r, 0.0, n);
347 p.push(Point3::new(0.0, 0.0, 0.0));
348 p.push(Point3::new(0.0, 0.0, h));
349 let (base, apex) = (n, n + 1);
350 let mut i = Vec::with_capacity(n * 6);
351 for k in 0..n {
352 let (a, b) = (k as u32, ((k + 1) % n) as u32);
353 i.extend([base as u32, b, a]);
354 i.extend([a, b, apex as u32]);
355 }
356 Ok(TriMesh::new(p, i))
357}
358
359fn sphere(radius: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
363 let r = positive(radius, "sphere radius")?;
364 let n = segments_within(r, 0.5 * tol)?;
365 let stacks = n.div_ceil(2).max(2);
370 let mut p = Vec::with_capacity((stacks + 1) * n);
371 for i in 0..=stacks {
372 let v = core::f64::consts::PI * (i as Scalar) / (stacks as Scalar);
373 for j in 0..n {
374 let u = core::f64::consts::TAU * (j as Scalar) / (n as Scalar);
375 p.push(Point3::new(
376 r * v.sin() * u.cos(),
377 r * v.sin() * u.sin(),
378 r * v.cos(),
379 ));
380 }
381 }
382 let mut idx = Vec::with_capacity(stacks * n * 6);
383 for i in 0..stacks {
384 for j in 0..n {
385 let jn = (j + 1) % n;
386 let row = |r: usize, k: usize| -> u32 {
389 if r == 0 || r == stacks {
390 (r * n) as u32
391 } else {
392 (r * n + k) as u32
393 }
394 };
395 let a = row(i, j);
396 let b = row(i, jn);
397 let c = row(i + 1, j);
398 let d = row(i + 1, jn);
399 if i > 0 {
401 idx.extend([a, c, b]);
402 }
403 if i + 1 < stacks {
404 idx.extend([b, c, d]);
405 }
406 }
407 }
408 Ok(TriMesh::new(p, idx))
409}