use glam::DVec3;
use crate::geometry::tool::ToolPrimitive;
use super::{
EPS_FACE,
concave::{self, ToolBounds},
measure::max_abs_offset,
pow2_scale_norm,
types::{SolidMesh, SolidStatus},
vertices::{
SolidVertex, dedup_origin_indices, dedup_planes, escaping_plane_indices, feasible_vertices,
},
};
#[must_use]
pub fn build_solid_mesh(planes: &[(DVec3, f64)]) -> SolidStatus {
let deduped = dedup_planes(planes);
let origin = dedup_origin_indices(planes, &deduped);
let scale = pow2_scale_norm(max_abs_offset(&deduped));
let deduped: Vec<(DVec3, f64)> = deduped.iter().map(|&(n, m)| (n, m / scale)).collect();
let Some(verts) = feasible_vertices(&deduped) else {
let mut escaping: Vec<usize> = escaping_plane_indices(&deduped)
.into_iter()
.map(|i| origin[i])
.collect();
escaping.sort_unstable();
escaping.dedup();
return SolidStatus::Unbounded { escaping };
};
if verts.len() < 4 {
return SolidStatus::Degenerate {
vertex_count: verts.len(),
volume: None,
};
}
let volume: f64 = deduped
.iter()
.map(|&(n, m)| m * face_area(n, m, &verts) / 3.0)
.sum();
if !(volume.is_finite() && volume > 0.0) {
return SolidStatus::Degenerate {
vertex_count: verts.len(),
volume: volume.is_finite().then_some(volume * scale * scale * scale),
};
}
let mut mesh = SolidMesh::default();
for (i, &(normal, offset)) in deduped.iter().enumerate() {
let Some((ring, centroid)) = face_ring(normal, offset, &verts) else {
continue;
};
let facet_idx = origin[i];
let base = u32::try_from(mesh.positions.len()).expect("a mesh's vertex count fits in u32");
mesh.positions.push(centroid * scale);
mesh.normals.push(normal);
mesh.facet_id.push(facet_idx);
for &v in &ring {
mesh.positions.push(v * scale);
mesh.normals.push(normal);
mesh.facet_id.push(facet_idx);
}
let k = u32::try_from(ring.len()).expect("a facet ring's vertex count fits in u32");
for e in 0..k {
let a = base + 1 + e;
let b = base + 1 + (e + 1) % k;
mesh.indices.push(base);
mesh.indices.push(a);
mesh.indices.push(b);
}
let scaled_ring: Vec<DVec3> = ring.iter().map(|&v| v * scale).collect();
mesh.rings.push((facet_idx, scaled_ring));
}
SolidStatus::Closed(mesh)
}
fn face_ring(normal: DVec3, offset: f64, verts: &[SolidVertex]) -> Option<(Vec<DVec3>, DVec3)> {
let on_face: Vec<DVec3> = verts
.iter()
.map(|s| s.v)
.filter(|v| (normal.dot(*v) - offset).abs() <= EPS_FACE)
.collect();
if on_face.len() < 3 {
return None;
}
let seed = if normal.x.abs() <= normal.y.abs() && normal.x.abs() <= normal.z.abs() {
DVec3::X
} else if normal.y.abs() <= normal.z.abs() {
DVec3::Y
} else {
DVec3::Z
};
let basis_u = (seed - normal * normal.dot(seed)).normalize();
let basis_v = normal.cross(basis_u);
let centroid = on_face.iter().copied().sum::<DVec3>() / on_face.len() as f64;
let mut angled: Vec<(f64, DVec3)> = on_face
.into_iter()
.map(|vert| {
let d = vert - centroid;
(basis_v.dot(d).atan2(basis_u.dot(d)), vert)
})
.collect();
angled.sort_by(|x, y| x.0.total_cmp(&y.0));
Some((angled.into_iter().map(|(_, v)| v).collect(), centroid))
}
pub(super) fn face_area(normal: DVec3, offset: f64, verts: &[SolidVertex]) -> f64 {
let Some((ring, centroid)) = face_ring(normal, offset, verts) else {
return 0.0;
};
let mut cross_sum = DVec3::ZERO;
for i in 0..ring.len() {
let a = ring[i] - centroid;
let b = ring[(i + 1) % ring.len()] - centroid;
cross_sum += a.cross(b);
}
0.5 * normal.dot(cross_sum).abs()
}
pub const TOOL_SEGMENTS: usize = if cfg!(target_arch = "wasm32") { 24 } else { 48 };
pub const TOOL_ICOSPHERE_LEVEL: u32 = 2;
#[must_use]
pub fn build_solid_mesh_geom(planes: &[(DVec3, f64)], tools: &[ToolPrimitive]) -> SolidStatus {
let status = build_solid_mesh(planes);
if tools.is_empty() {
return status;
}
match status {
SolidStatus::Closed(mesh) => concave::build(planes, tools, &mesh),
other => other,
}
}
#[must_use]
pub fn tessellate_tool(tool: &ToolPrimitive, segments: usize) -> Vec<(DVec3, f64)> {
concave::polytope(tool, segments).map_or_else(Vec::new, |p| p.planes)
}
#[must_use]
pub fn tool_bounds(tool: &ToolPrimitive, segments: usize) -> Option<ToolBounds> {
concave::bounds(tool, segments)
}
#[must_use]
pub fn mesh_volume(mesh: &SolidMesh) -> f64 {
let Some(origin) = mesh
.rings
.iter()
.find_map(|(_, ring)| ring.first().copied())
else {
return 0.0;
};
let mut acc = 0.0;
for (_, ring) in &mesh.rings {
let Some(&p0) = ring.first() else { continue };
let mut area = DVec3::ZERO;
for w in ring[1..].windows(2) {
area += (w[0] - p0).cross(w[1] - p0);
}
acc += (p0 - origin).dot(area);
}
acc / 6.0
}