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> {
62 let tol = tolerance.linear();
63 match primitive {
64 Primitive::Block { x, y, z } => block(*x, *y, *z),
65 Primitive::Sphere { radius } => sphere(*radius, tol),
66 Primitive::Cylinder { radius, height } => cylinder(*radius, *height, tol),
67 Primitive::Cone { radius, height } => cone(*radius, *height, tol),
68 Primitive::Pyramid { x, y, height } => pyramid(*x, *y, *height),
69 Primitive::Torus {
70 major_radius,
71 minor_radius,
72 } => torus(*major_radius, *minor_radius, tol),
73 Primitive::Wedge {
74 x,
75 y,
76 height,
77 top_x_min,
78 top_x_max,
79 top_y_min,
80 top_y_max,
81 } => wedge(
82 [*x, *y, *height],
83 [*top_x_min, *top_x_max],
84 [*top_y_min, *top_y_max],
85 ),
86 _ => Err(GeomError::Unsupported {
89 backend: crate::ScalarBoolean::ID,
90 operation: axiolid_contracts::Operation::Tessellation,
91 }),
92 }
93}
94
95fn positive(value: Scalar, what: &str) -> GeomResult<Scalar> {
97 if !value.is_finite() || value <= 0.0 {
98 return Err(GeomError::InvalidInput(format!(
99 "{what} must be positive and finite, got {value}"
100 )));
101 }
102 Ok(value)
103}
104
105fn block(x: Scalar, y: Scalar, z: Scalar) -> GeomResult<TriMesh> {
107 let (hx, hy, hz) = (
108 positive(x, "block x")? / 2.0,
109 positive(y, "block y")? / 2.0,
110 positive(z, "block z")? / 2.0,
111 );
112 let p = vec![
113 Point3::new(-hx, -hy, -hz),
114 Point3::new(hx, -hy, -hz),
115 Point3::new(hx, hy, -hz),
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 ];
122 let i = vec![
125 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,
126 3, 0, 4, 3, 4, 7,
127 ];
128 Ok(TriMesh::new(p, i))
129}
130
131fn pyramid(x: Scalar, y: Scalar, height: Scalar) -> GeomResult<TriMesh> {
133 let (hx, hy) = (
134 positive(x, "pyramid x")? / 2.0,
135 positive(y, "pyramid y")? / 2.0,
136 );
137 let h = positive(height, "pyramid height")?;
138 let p = vec![
139 Point3::new(-hx, -hy, 0.0),
140 Point3::new(hx, -hy, 0.0),
141 Point3::new(hx, hy, 0.0),
142 Point3::new(-hx, hy, 0.0),
143 Point3::new(0.0, 0.0, h),
144 ];
145 let i = vec![0, 2, 1, 0, 3, 2, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4];
146 Ok(TriMesh::new(p, i))
147}
148
149fn wedge(
157 [x, y, height]: [Scalar; 3],
158 [x0, x1]: [Scalar; 2],
159 [y0, y1]: [Scalar; 2],
160) -> GeomResult<TriMesh> {
161 let (x, y, h) = (
162 positive(x, "wedge x")?,
163 positive(y, "wedge y")?,
164 positive(height, "wedge height")?,
165 );
166 for (value, what) in [
167 (x0, "wedge top x min"),
168 (x1, "wedge top x max"),
169 (y0, "wedge top y min"),
170 (y1, "wedge top y max"),
171 ] {
172 if !value.is_finite() {
173 return Err(GeomError::InvalidInput(format!(
174 "{what} must be finite, got {value}"
175 )));
176 }
177 }
178 if x0 > x1 || y0 > y1 {
179 return Err(GeomError::InvalidInput(format!(
180 "wedge top must have min <= max, got x {x0}..{x1}, y {y0}..{y1}"
181 )));
182 }
183 let corners = [
184 Point3::new(0.0, 0.0, 0.0),
185 Point3::new(x, 0.0, 0.0),
186 Point3::new(x, y, 0.0),
187 Point3::new(0.0, y, 0.0),
188 Point3::new(x0, y0, h),
189 Point3::new(x1, y0, h),
190 Point3::new(x1, y1, h),
191 Point3::new(x0, y1, h),
192 ];
193 const FACES: [[usize; 4]; 6] = [
196 [0, 3, 2, 1],
197 [4, 5, 6, 7],
198 [0, 1, 5, 4],
199 [1, 2, 6, 5],
200 [2, 3, 7, 6],
201 [3, 0, 4, 7],
202 ];
203 let mut p: Vec<Point3> = Vec::with_capacity(8);
204 let index: Vec<u32> = corners
205 .iter()
206 .map(|&c| match p.iter().position(|&q| q == c) {
207 Some(k) => k as u32,
208 None => {
209 p.push(c);
210 (p.len() - 1) as u32
211 }
212 })
213 .collect();
214 let mut i = Vec::with_capacity(36);
215 for face in FACES {
216 let mut ring: Vec<u32> = Vec::with_capacity(4);
217 for corner in face {
218 let v = index[corner];
219 if ring.last() != Some(&v) {
220 ring.push(v);
221 }
222 }
223 if ring.len() > 1 && ring.first() == ring.last() {
224 ring.pop();
225 }
226 if ring.len() < 3 {
227 continue;
228 }
229 for k in 1..ring.len() - 1 {
231 i.extend([ring[0], ring[k], ring[k + 1]]);
232 }
233 }
234 Ok(TriMesh::new(p, i))
235}
236
237fn torus(major: Scalar, minor: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
246 let big = positive(major, "torus major radius")?;
247 let r = positive(minor, "torus minor radius")?;
248 if r >= big {
249 let kind = if r == big {
250 "a horn torus (minor radius equal to major)"
251 } else {
252 "a spindle torus (minor radius above major)"
253 };
254 return Err(GeomError::InvalidInput(format!(
255 "torus minor radius {r} must be below major radius {big}: {kind} \
256 does not bound a two-manifold solid"
257 )));
258 }
259 let n = segments_within(big + r, 0.5 * tol)?;
260 let m = segments_within(r, 0.5 * tol)?;
261 let mut p = Vec::with_capacity(n * m);
262 for i in 0..n {
263 let theta = core::f64::consts::TAU * (i as Scalar) / (n as Scalar);
264 for j in 0..m {
265 let phi = core::f64::consts::TAU * (j as Scalar) / (m as Scalar);
266 let rho = big + r * phi.cos();
267 p.push(Point3::new(
268 rho * theta.cos(),
269 rho * theta.sin(),
270 r * phi.sin(),
271 ));
272 }
273 }
274 let at = |i: usize, j: usize| ((i % n) * m + (j % m)) as u32;
275 let mut idx = Vec::with_capacity(n * m * 6);
276 for i in 0..n {
277 for j in 0..m {
278 let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
282 idx.extend([a, b, c]);
283 idx.extend([a, c, d]);
284 }
285 }
286 Ok(TriMesh::new(p, idx))
287}
288
289fn ring(radius: Scalar, z: Scalar, n: usize) -> Vec<Point3> {
291 (0..n)
292 .map(|k| {
293 let a = core::f64::consts::TAU * (k as Scalar) / (n as Scalar);
294 Point3::new(radius * a.cos(), radius * a.sin(), z)
295 })
296 .collect()
297}
298
299fn cylinder(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
301 let r = positive(radius, "cylinder radius")?;
302 let h = positive(height, "cylinder height")?;
303 let n = segments(r, tol);
304 let mut p = ring(r, 0.0, n);
305 p.extend(ring(r, h, n));
306 p.push(Point3::new(0.0, 0.0, 0.0));
307 p.push(Point3::new(0.0, 0.0, h));
308 let (bc, tc) = (2 * n, 2 * n + 1);
309 let mut i = Vec::with_capacity(n * 12);
310 for k in 0..n {
311 let (a, b) = (k, (k + 1) % n);
312 i.extend([a as u32, b as u32, (b + n) as u32]);
315 i.extend([a as u32, (b + n) as u32, (a + n) as u32]);
316 i.extend([bc as u32, b as u32, a as u32]);
317 i.extend([tc as u32, (a + n) as u32, (b + n) as u32]);
318 }
319 Ok(TriMesh::new(p, i))
320}
321
322fn cone(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
324 let r = positive(radius, "cone radius")?;
325 let h = positive(height, "cone height")?;
326 let n = segments(r, tol);
327 let mut p = ring(r, 0.0, n);
328 p.push(Point3::new(0.0, 0.0, 0.0));
329 p.push(Point3::new(0.0, 0.0, h));
330 let (base, apex) = (n, n + 1);
331 let mut i = Vec::with_capacity(n * 6);
332 for k in 0..n {
333 let (a, b) = (k as u32, ((k + 1) % n) as u32);
334 i.extend([base as u32, b, a]);
335 i.extend([a, b, apex as u32]);
336 }
337 Ok(TriMesh::new(p, i))
338}
339
340fn sphere(radius: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
344 let r = positive(radius, "sphere radius")?;
345 let n = segments_within(r, 0.5 * tol)?;
346 let stacks = n.div_ceil(2).max(2);
351 let mut p = Vec::with_capacity((stacks + 1) * n);
352 for i in 0..=stacks {
353 let v = core::f64::consts::PI * (i as Scalar) / (stacks as Scalar);
354 for j in 0..n {
355 let u = core::f64::consts::TAU * (j as Scalar) / (n as Scalar);
356 p.push(Point3::new(
357 r * v.sin() * u.cos(),
358 r * v.sin() * u.sin(),
359 r * v.cos(),
360 ));
361 }
362 }
363 let mut idx = Vec::with_capacity(stacks * n * 6);
364 for i in 0..stacks {
365 for j in 0..n {
366 let jn = (j + 1) % n;
367 let row = |r: usize, k: usize| -> u32 {
370 if r == 0 || r == stacks {
371 (r * n) as u32
372 } else {
373 (r * n + k) as u32
374 }
375 };
376 let a = row(i, j);
377 let b = row(i, jn);
378 let c = row(i + 1, j);
379 let d = row(i + 1, jn);
380 if i > 0 {
382 idx.extend([a, c, b]);
383 }
384 if i + 1 < stacks {
385 idx.extend([b, c, d]);
386 }
387 }
388 }
389 Ok(TriMesh::new(p, idx))
390}