use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::{Point3, Scalar, Tolerance};
use axiolid_mesh::TriMesh;
use axiolid_primitive::Primitive;
fn segments(radius: Scalar, tolerance: Scalar) -> usize {
if !(radius.is_finite() && radius > 0.0 && tolerance.is_finite() && tolerance > 0.0) {
return 3;
}
let ratio = 1.0 - (tolerance / radius).min(1.0);
let n = (core::f64::consts::PI / ratio.acos().max(1e-9)).ceil();
(n as usize).clamp(3, 4096)
}
pub fn tessellate_primitive(primitive: &Primitive, tolerance: Tolerance) -> GeomResult<TriMesh> {
let tol = tolerance.linear();
match primitive {
Primitive::Block { x, y, z } => block(*x, *y, *z),
Primitive::Sphere { radius } => sphere(*radius, tol),
Primitive::Cylinder { radius, height } => cylinder(*radius, *height, tol),
Primitive::Cone { radius, height } => cone(*radius, *height, tol),
Primitive::Pyramid { x, y, height } => pyramid(*x, *y, *height),
Primitive::Torus {
major_radius,
minor_radius,
} => torus(*major_radius, *minor_radius, tol),
Primitive::Wedge {
x,
y,
height,
top_x_min,
top_x_max,
top_y_min,
top_y_max,
} => wedge(
[*x, *y, *height],
[*top_x_min, *top_x_max],
[*top_y_min, *top_y_max],
),
_ => Err(GeomError::Unsupported {
backend: crate::ScalarBoolean::ID,
operation: axiolid_contracts::Operation::Tessellation,
}),
}
}
fn positive(value: Scalar, what: &str) -> GeomResult<Scalar> {
if !value.is_finite() || value <= 0.0 {
return Err(GeomError::InvalidInput(format!(
"{what} must be positive and finite, got {value}"
)));
}
Ok(value)
}
fn block(x: Scalar, y: Scalar, z: Scalar) -> GeomResult<TriMesh> {
let (hx, hy, hz) = (
positive(x, "block x")? / 2.0,
positive(y, "block y")? / 2.0,
positive(z, "block z")? / 2.0,
);
let p = vec![
Point3::new(-hx, -hy, -hz),
Point3::new(hx, -hy, -hz),
Point3::new(hx, hy, -hz),
Point3::new(-hx, hy, -hz),
Point3::new(-hx, -hy, hz),
Point3::new(hx, -hy, hz),
Point3::new(hx, hy, hz),
Point3::new(-hx, hy, hz),
];
let i = vec![
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,
3, 0, 4, 3, 4, 7,
];
Ok(TriMesh::new(p, i))
}
fn pyramid(x: Scalar, y: Scalar, height: Scalar) -> GeomResult<TriMesh> {
let (hx, hy) = (
positive(x, "pyramid x")? / 2.0,
positive(y, "pyramid y")? / 2.0,
);
let h = positive(height, "pyramid height")?;
let p = vec![
Point3::new(-hx, -hy, 0.0),
Point3::new(hx, -hy, 0.0),
Point3::new(hx, hy, 0.0),
Point3::new(-hx, hy, 0.0),
Point3::new(0.0, 0.0, h),
];
let i = vec![0, 2, 1, 0, 3, 2, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4];
Ok(TriMesh::new(p, i))
}
fn wedge(
[x, y, height]: [Scalar; 3],
[x0, x1]: [Scalar; 2],
[y0, y1]: [Scalar; 2],
) -> GeomResult<TriMesh> {
let (x, y, h) = (
positive(x, "wedge x")?,
positive(y, "wedge y")?,
positive(height, "wedge height")?,
);
for (value, what) in [
(x0, "wedge top x min"),
(x1, "wedge top x max"),
(y0, "wedge top y min"),
(y1, "wedge top y max"),
] {
if !value.is_finite() {
return Err(GeomError::InvalidInput(format!(
"{what} must be finite, got {value}"
)));
}
}
if x0 > x1 || y0 > y1 {
return Err(GeomError::InvalidInput(format!(
"wedge top must have min <= max, got x {x0}..{x1}, y {y0}..{y1}"
)));
}
let corners = [
Point3::new(0.0, 0.0, 0.0),
Point3::new(x, 0.0, 0.0),
Point3::new(x, y, 0.0),
Point3::new(0.0, y, 0.0),
Point3::new(x0, y0, h),
Point3::new(x1, y0, h),
Point3::new(x1, y1, h),
Point3::new(x0, y1, h),
];
const FACES: [[usize; 4]; 6] = [
[0, 3, 2, 1],
[4, 5, 6, 7],
[0, 1, 5, 4],
[1, 2, 6, 5],
[2, 3, 7, 6],
[3, 0, 4, 7],
];
let mut p: Vec<Point3> = Vec::with_capacity(8);
let index: Vec<u32> = corners
.iter()
.map(|&c| match p.iter().position(|&q| q == c) {
Some(k) => k as u32,
None => {
p.push(c);
(p.len() - 1) as u32
}
})
.collect();
let mut i = Vec::with_capacity(36);
for face in FACES {
let mut ring: Vec<u32> = Vec::with_capacity(4);
for corner in face {
let v = index[corner];
if ring.last() != Some(&v) {
ring.push(v);
}
}
if ring.len() > 1 && ring.first() == ring.last() {
ring.pop();
}
if ring.len() < 3 {
continue;
}
for k in 1..ring.len() - 1 {
i.extend([ring[0], ring[k], ring[k + 1]]);
}
}
Ok(TriMesh::new(p, i))
}
fn torus(major: Scalar, minor: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
let big = positive(major, "torus major radius")?;
let r = positive(minor, "torus minor radius")?;
if r >= big {
let kind = if r == big {
"a horn torus (minor radius equal to major)"
} else {
"a spindle torus (minor radius above major)"
};
return Err(GeomError::InvalidInput(format!(
"torus minor radius {r} must be below major radius {big}: {kind} \
does not bound a two-manifold solid"
)));
}
let n = segments(big + r, tol);
let m = segments(r, tol);
let mut p = Vec::with_capacity(n * m);
for i in 0..n {
let theta = core::f64::consts::TAU * (i as Scalar) / (n as Scalar);
for j in 0..m {
let phi = core::f64::consts::TAU * (j as Scalar) / (m as Scalar);
let rho = big + r * phi.cos();
p.push(Point3::new(
rho * theta.cos(),
rho * theta.sin(),
r * phi.sin(),
));
}
}
let at = |i: usize, j: usize| ((i % n) * m + (j % m)) as u32;
let mut idx = Vec::with_capacity(n * m * 6);
for i in 0..n {
for j in 0..m {
let (a, b, c, d) = (at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1));
idx.extend([a, b, c]);
idx.extend([a, c, d]);
}
}
Ok(TriMesh::new(p, idx))
}
fn ring(radius: Scalar, z: Scalar, n: usize) -> Vec<Point3> {
(0..n)
.map(|k| {
let a = core::f64::consts::TAU * (k as Scalar) / (n as Scalar);
Point3::new(radius * a.cos(), radius * a.sin(), z)
})
.collect()
}
fn cylinder(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
let r = positive(radius, "cylinder radius")?;
let h = positive(height, "cylinder height")?;
let n = segments(r, tol);
let mut p = ring(r, 0.0, n);
p.extend(ring(r, h, n));
p.push(Point3::new(0.0, 0.0, 0.0));
p.push(Point3::new(0.0, 0.0, h));
let (bc, tc) = (2 * n, 2 * n + 1);
let mut i = Vec::with_capacity(n * 12);
for k in 0..n {
let (a, b) = (k, (k + 1) % n);
i.extend([a as u32, b as u32, (b + n) as u32]);
i.extend([a as u32, (b + n) as u32, (a + n) as u32]);
i.extend([bc as u32, b as u32, a as u32]);
i.extend([tc as u32, (a + n) as u32, (b + n) as u32]);
}
Ok(TriMesh::new(p, i))
}
fn cone(radius: Scalar, height: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
let r = positive(radius, "cone radius")?;
let h = positive(height, "cone height")?;
let n = segments(r, tol);
let mut p = ring(r, 0.0, n);
p.push(Point3::new(0.0, 0.0, 0.0));
p.push(Point3::new(0.0, 0.0, h));
let (base, apex) = (n, n + 1);
let mut i = Vec::with_capacity(n * 6);
for k in 0..n {
let (a, b) = (k as u32, ((k + 1) % n) as u32);
i.extend([base as u32, b, a]);
i.extend([a, b, apex as u32]);
}
Ok(TriMesh::new(p, i))
}
fn sphere(radius: Scalar, tol: Scalar) -> GeomResult<TriMesh> {
let r = positive(radius, "sphere radius")?;
let n = segments(r, tol);
let stacks = (n / 2).max(2);
let mut p = Vec::with_capacity((stacks + 1) * n);
for i in 0..=stacks {
let v = core::f64::consts::PI * (i as Scalar) / (stacks as Scalar);
for j in 0..n {
let u = core::f64::consts::TAU * (j as Scalar) / (n as Scalar);
p.push(Point3::new(
r * v.sin() * u.cos(),
r * v.sin() * u.sin(),
r * v.cos(),
));
}
}
let mut idx = Vec::with_capacity(stacks * n * 6);
for i in 0..stacks {
for j in 0..n {
let jn = (j + 1) % n;
let row = |r: usize, k: usize| -> u32 {
if r == 0 || r == stacks {
(r * n) as u32
} else {
(r * n + k) as u32
}
};
let a = row(i, j);
let b = row(i, jn);
let c = row(i + 1, j);
let d = row(i + 1, jn);
if i > 0 {
idx.extend([a, c, b]);
}
if i + 1 < stacks {
idx.extend([b, c, d]);
}
}
}
Ok(TriMesh::new(p, idx))
}