use std::fmt::Write as _;
use crate::octree::OctreeNode;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TriMesh {
pub vertices: Vec<[f32; 3]>,
pub indices: Vec<[u32; 3]>,
}
pub fn to_obj(mesh: &TriMesh) -> String {
let mut out = String::new();
for v in &mesh.vertices {
let _ = writeln!(out, "v {} {} {}", v[0], v[1], v[2]);
}
for tri in &mesh.indices {
let _ = writeln!(out, "f {} {} {}", tri[0] + 1, tri[1] + 1, tri[2] + 1);
}
out
}
pub fn to_ply(mesh: &TriMesh) -> Vec<u8> {
let header = format!(
"ply\nformat binary_little_endian 1.0\n\
element vertex {}\nproperty float x\nproperty float y\nproperty float z\n\
element face {}\nproperty list uchar uint vertex_indices\nend_header\n",
mesh.vertices.len(),
mesh.indices.len()
);
let mut out = header.into_bytes();
for v in &mesh.vertices {
for &f in v {
out.extend_from_slice(&f.to_le_bytes());
}
}
for tri in &mesh.indices {
out.push(3u8);
for &i in tri {
out.extend_from_slice(&i.to_le_bytes());
}
}
out
}
pub fn marching_cubes_export_density(octree: &OctreeNode, depth: u8) -> Vec<([f32; 3], f32)> {
octree
.nodes_at_depth(depth)
.into_iter()
.filter(|n| !n.points.is_empty())
.map(|n| {
let c = [n.center[0] as f32, n.center[1] as f32, n.center[2] as f32];
let d = (n.points.len() as f32 / 8.0).min(1.0);
(c, d)
})
.collect()
}
pub fn marching_cubes(octree: &OctreeNode, resolution: usize, isolevel: f32) -> TriMesh {
if resolution == 0 || octree.points.is_empty() && octree.children.is_none() {
return TriMesh::default();
}
let n = resolution + 1;
let total = n * n * n;
if total > 5_000_000 {
return TriMesh::default();
}
let half = octree.half_len;
let world_size = half * 2.0;
let step = world_size / resolution as f64;
let origin = [octree.center[0] - half, octree.center[1] - half, octree.center[2] - half];
let voxel = step;
let mut values = vec![0.0f32; total];
for ix in 0..n {
for iy in 0..n {
for iz in 0..n {
let x = origin[0] + ix as f64 * step;
let y = origin[1] + iy as f64 * step;
let z = origin[2] + iz as f64 * step;
let pts = octree.range_query(
[x - voxel, y - voxel, z - voxel],
[x + voxel, y + voxel, z + voxel],
);
let density = (pts.len() as f32 / 8.0).min(1.0);
values[ix * n * n + iy * n + iz] = density;
}
}
}
let dims = (n, n, n);
let size_f = (
world_size as f32,
world_size as f32,
world_size as f32,
);
let interval = (step as f32, step as f32, step as f32);
let offset = lin_alg::f32::Vec3::new(
origin[0] as f32,
origin[1] as f32,
origin[2] as f32,
);
let mc = match mcubes::MarchingCubes::new(
dims,
size_f,
interval,
offset,
values,
isolevel,
) {
Ok(mc) => mc,
Err(_) => return TriMesh::default(),
};
let mesh = mc.generate(mcubes::MeshSide::Both);
let vertices: Vec<[f32; 3]> = mesh
.vertices
.iter()
.map(|v| [v.posit.x, v.posit.y, v.posit.z])
.collect();
let indices: Vec<[u32; 3]> = mesh
.indices
.chunks_exact(3)
.map(|tri| [tri[0] as u32, tri[1] as u32, tri[2] as u32])
.collect();
TriMesh { vertices, indices }
}
#[cfg(test)]
mod tests {
use super::*;
fn single_triangle() -> TriMesh {
TriMesh {
vertices: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
indices: vec![[0, 1, 2]],
}
}
#[test]
fn obj_has_vertices_and_one_based_faces() {
let obj = to_obj(&single_triangle());
assert!(obj.contains("v 0 0 0"));
assert!(obj.contains("v 1 0 0"));
assert!(obj.contains("f 1 2 3"), "got:\n{obj}");
assert_eq!(obj.lines().filter(|l| l.starts_with("v ")).count(), 3);
assert_eq!(obj.lines().filter(|l| l.starts_with("f ")).count(), 1);
}
#[test]
fn ply_header_and_byte_layout() {
let mesh = single_triangle();
let bytes = to_ply(&mesh);
let header_end = b"end_header\n";
let pos = bytes
.windows(header_end.len())
.position(|w| w == header_end)
.map(|p| p + header_end.len())
.expect("header present");
let header = std::str::from_utf8(&bytes[..pos]).unwrap();
assert!(header.contains("element vertex 3"));
assert!(header.contains("element face 1"));
assert!(header.contains("binary_little_endian"));
let body = bytes.len() - pos;
assert_eq!(body, 3 * 12 + (1 + 12));
let x = f32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
assert_eq!(x, 0.0);
assert_eq!(bytes[pos + 3 * 12], 3u8);
}
#[test]
fn density_export_reports_occupied_voxels() {
let mut octree = OctreeNode::new([0.0; 3], 50.0);
octree.insert([1.0, 1.0, 1.0], 100);
octree.insert([2.0, 2.0, 2.0], 100);
let cells = marching_cubes_export_density(&octree, 0);
assert_eq!(cells.len(), 1, "root holds both points");
assert_eq!(cells[0].0, [0.0, 0.0, 0.0], "root center");
assert!((cells[0].1 - 2.0 / 8.0).abs() < 1e-6, "density = 2/8");
}
#[test]
fn density_export_after_subdivision() {
let mut octree = OctreeNode::new([0.0; 3], 50.0);
for p in [
[10.0, 10.0, 10.0],
[-10.0, 10.0, 10.0],
[10.0, -10.0, 10.0],
[-10.0, -10.0, -10.0],
] {
octree.insert(p, 1);
}
let cells = marching_cubes_export_density(&octree, 1);
assert!(!cells.is_empty(), "occupied depth-1 voxels exist after subdivision");
for (c, _) in &cells {
for axis in c {
assert!(axis.abs() <= 50.0, "center within world: {c:?}");
}
}
}
#[test]
fn marching_cubes_produces_mesh_from_points() {
let mut octree = OctreeNode::new([0.0; 3], 50.0);
for x in -5..=5 {
for y in -5..=5 {
for z in -5..=5 {
octree.insert([x as f64, y as f64, z as f64], 100);
}
}
}
let mesh = marching_cubes(&octree, 16, 0.05);
assert!(!mesh.vertices.is_empty(), "MC should produce vertices from point cluster");
assert!(!mesh.indices.is_empty(), "MC should produce triangles from point cluster");
}
#[test]
fn marching_cubes_empty_octree_returns_empty() {
let octree = OctreeNode::new([0.0; 3], 50.0);
let mesh = marching_cubes(&octree, 8, 0.5);
assert!(mesh.vertices.is_empty());
assert!(mesh.indices.is_empty());
}
#[test]
fn marching_cubes_zero_resolution_returns_empty() {
let mut octree = OctreeNode::new([0.0; 3], 50.0);
octree.insert([1.0, 2.0, 3.0], 8);
let mesh = marching_cubes(&octree, 0, 0.5);
assert!(mesh.vertices.is_empty());
}
}