use std::collections::{HashMap, HashSet};
use super::types::{AttributeData, MeshData};
const FACE_CORNERS: [[[u32; 3]; 4]; 6] = [
[[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]], [[1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1]], [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]], [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0]], [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], ];
#[non_exhaustive]
#[derive(Default)]
pub struct SparseVolumeGridData {
pub origin: [f32; 3],
pub cell_size: f32,
pub active_cells: Vec<[u32; 3]>,
pub cell_scalars: HashMap<String, Vec<f32>>,
pub node_scalars: HashMap<String, Vec<f32>>,
pub cell_colors: HashMap<String, Vec<[f32; 4]>>,
}
pub(crate) fn extract_sparse_boundary(data: &SparseVolumeGridData) -> MeshData {
if data.active_cells.is_empty() || data.cell_size <= 0.0 {
return MeshData::default();
}
let s = data.cell_size;
let [ox, oy, oz] = data.origin;
let active_set: HashSet<[u32; 3]> = data.active_cells.iter().copied().collect();
let (max_ci, max_cj, _max_ck) = data
.active_cells
.iter()
.fold((0u32, 0u32, 0u32), |(mi, mj, mk), &[ci, cj, ck]| {
(mi.max(ci), mj.max(cj), mk.max(ck))
});
let node_w = (max_ci + 2) as usize; let node_h = (max_cj + 2) as usize;
let mut node_to_vi: HashMap<[u32; 3], u32> = HashMap::new();
let mut positions: Vec<[f32; 3]> = Vec::new();
struct BoundaryQuad {
cell_idx: usize,
vi: [u32; 4],
node_keys: [[u32; 3]; 4],
}
let mut boundary_quads: Vec<BoundaryQuad> = Vec::new();
for (cell_idx, &[ci, cj, ck]) in data.active_cells.iter().enumerate() {
for face_dir in 0usize..6 {
let neighbor_active = match face_dir {
0 => ci > 0 && active_set.contains(&[ci - 1, cj, ck]), 1 => active_set.contains(&[ci + 1, cj, ck]), 2 => cj > 0 && active_set.contains(&[ci, cj - 1, ck]), 3 => active_set.contains(&[ci, cj + 1, ck]), 4 => ck > 0 && active_set.contains(&[ci, cj, ck - 1]), 5 => active_set.contains(&[ci, cj, ck + 1]), _ => unreachable!(),
};
if neighbor_active {
continue; }
let corners = &FACE_CORNERS[face_dir];
let mut vi = [0u32; 4];
let mut node_keys = [[0u32; 3]; 4];
for (k, &[di, dj, dk]) in corners.iter().enumerate() {
let nk: [u32; 3] = [ci + di, cj + dj, ck + dk];
node_keys[k] = nk;
let next_idx = positions.len() as u32;
let vi_k = *node_to_vi.entry(nk).or_insert_with(|| {
positions.push([
ox + nk[0] as f32 * s,
oy + nk[1] as f32 * s,
oz + nk[2] as f32 * s,
]);
next_idx
});
vi[k] = vi_k;
}
boundary_quads.push(BoundaryQuad {
cell_idx,
vi,
node_keys,
});
}
}
let n_verts = positions.len();
let n_quads = boundary_quads.len();
let mut indices: Vec<u32> = Vec::with_capacity(n_quads * 6);
let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
for quad in &boundary_quads {
let [va, vb, vc, vd] = quad.vi;
indices.push(va);
indices.push(vb);
indices.push(vc);
indices.push(va);
indices.push(vc);
indices.push(vd);
for &[v0, v1, v2] in &[[va, vb, vc], [va, vc, vd]] {
let pa = positions[v0 as usize];
let pb = positions[v1 as usize];
let pc = positions[v2 as usize];
let ab = [
(pb[0] - pa[0]) as f64,
(pb[1] - pa[1]) as f64,
(pb[2] - pa[2]) as f64,
];
let ac = [
(pc[0] - pa[0]) as f64,
(pc[1] - pa[1]) as f64,
(pc[2] - pa[2]) as f64,
];
let n = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
for &vi in &[v0, v1, v2] {
let acc = &mut normal_accum[vi as usize];
acc[0] += n[0];
acc[1] += n[1];
acc[2] += n[2];
}
}
}
let normals: Vec<[f32; 3]> = normal_accum
.iter()
.map(|n| {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 1e-12 {
[
(n[0] / len) as f32,
(n[1] / len) as f32,
(n[2] / len) as f32,
]
} else {
[0.0, 1.0, 0.0]
}
})
.collect();
let mut attributes: HashMap<String, AttributeData> = HashMap::new();
for (name, cell_vals) in &data.cell_scalars {
let face_scalars: Vec<f32> = boundary_quads
.iter()
.flat_map(|q| {
let v = cell_vals.get(q.cell_idx).copied().unwrap_or(0.0);
[v, v] })
.collect();
attributes.insert(name.clone(), AttributeData::Face(face_scalars));
}
for (name, cell_vals) in &data.cell_colors {
let face_colors: Vec<[f32; 4]> = boundary_quads
.iter()
.flat_map(|q| {
let c = cell_vals.get(q.cell_idx).copied().unwrap_or([1.0; 4]);
[c, c] })
.collect();
attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
}
for (name, node_vals) in &data.node_scalars {
let face_scalars: Vec<f32> = boundary_quads
.iter()
.flat_map(|q| {
let avg = {
let mut sum = 0.0f32;
for &[ni, nj, nk] in &q.node_keys {
let idx =
nk as usize * node_w * node_h + nj as usize * node_w + ni as usize;
sum += node_vals.get(idx).copied().unwrap_or(0.0);
}
sum / 4.0
};
[avg, avg] })
.collect();
attributes.insert(name.clone(), AttributeData::Face(face_scalars));
}
MeshData {
positions,
normals,
indices,
uvs: None,
tangents: None,
attributes,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn single_cell() -> SparseVolumeGridData {
SparseVolumeGridData {
origin: [0.0; 3],
cell_size: 1.0,
active_cells: vec![[0, 0, 0]],
..Default::default()
}
}
fn two_adjacent_cells() -> SparseVolumeGridData {
SparseVolumeGridData {
origin: [0.0; 3],
cell_size: 1.0,
active_cells: vec![[0, 0, 0], [1, 0, 0]],
..Default::default()
}
}
fn three_cells_in_a_line() -> SparseVolumeGridData {
SparseVolumeGridData {
origin: [0.0; 3],
cell_size: 1.0,
active_cells: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
..Default::default()
}
}
#[test]
fn single_active_cell_has_twelve_boundary_triangles() {
let data = single_cell();
let mesh = extract_sparse_boundary(&data);
assert_eq!(
mesh.indices.len(),
36,
"single cell -> 12 boundary triangles (36 indices)"
);
}
#[test]
fn two_adjacent_cells_share_one_face() {
let data = two_adjacent_cells();
let mesh = extract_sparse_boundary(&data);
assert_eq!(
mesh.indices.len(),
60,
"two adjacent cells -> 20 boundary triangles (60 indices)"
);
}
#[test]
fn three_cells_in_a_line_share_two_faces() {
let data = three_cells_in_a_line();
let mesh = extract_sparse_boundary(&data);
assert_eq!(
mesh.indices.len(),
84,
"three cells in a line -> 28 boundary triangles (84 indices)"
);
}
#[test]
fn positions_are_correct() {
let data = SparseVolumeGridData {
origin: [1.0, 2.0, 3.0],
cell_size: 0.5,
active_cells: vec![[0, 0, 0]],
..Default::default()
};
let mesh = extract_sparse_boundary(&data);
assert!(
mesh.positions.contains(&[1.0, 2.0, 3.0]),
"corner [0,0,0] must be at origin"
);
assert!(
mesh.positions.contains(&[1.5, 2.5, 3.5]),
"corner [1,1,1] must be at origin + cell_size"
);
assert_eq!(mesh.positions.len(), 8, "single cell -> 8 unique corners");
}
#[test]
fn normals_have_correct_length() {
let data = single_cell();
let mesh = extract_sparse_boundary(&data);
assert_eq!(mesh.normals.len(), mesh.positions.len());
for n in &mesh.normals {
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!(
(len - 1.0).abs() < 1e-5,
"normal not unit length: {n:?} (len={len})"
);
}
}
#[test]
fn cell_scalar_remaps_to_face_attribute() {
let mut data = single_cell();
data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
let mesh = extract_sparse_boundary(&data);
match mesh.attributes.get("pressure") {
Some(AttributeData::Face(vals)) => {
assert_eq!(vals.len(), 12, "one value per boundary triangle");
for &v in vals {
assert_eq!(v, 42.0, "scalar must match cell value");
}
}
other => panic!("expected Face attribute, got {other:?}"),
}
}
#[test]
fn cell_color_remaps_to_face_color_attribute() {
let mut data = two_adjacent_cells();
data.cell_colors.insert(
"label".to_string(),
vec![[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]],
);
let mesh = extract_sparse_boundary(&data);
match mesh.attributes.get("label") {
Some(AttributeData::FaceColor(colors)) => {
assert_eq!(colors.len(), 20, "20 boundary triangles");
}
other => panic!("expected FaceColor attribute, got {other:?}"),
}
}
#[test]
fn node_scalar_remaps_to_face_attribute() {
let mut data = single_cell();
let node_vals = vec![1.0f32; 8]; data.node_scalars.insert("dist".to_string(), node_vals);
let mesh = extract_sparse_boundary(&data);
match mesh.attributes.get("dist") {
Some(AttributeData::Face(vals)) => {
assert_eq!(vals.len(), 12, "12 boundary triangles");
for &v in vals {
assert!(
(v - 1.0).abs() < 1e-6,
"averaged node scalar must equal 1.0, got {v}"
);
}
}
other => panic!("expected Face attribute, got {other:?}"),
}
}
#[test]
fn empty_active_cells_returns_empty_mesh_data() {
let data = SparseVolumeGridData {
origin: [0.0; 3],
cell_size: 1.0,
..Default::default()
};
let mesh = extract_sparse_boundary(&data);
assert!(mesh.positions.is_empty());
assert!(mesh.indices.is_empty());
}
#[test]
fn cell_size_zero_returns_empty_mesh_data() {
let data = SparseVolumeGridData {
origin: [0.0; 3],
cell_size: 0.0,
active_cells: vec![[0, 0, 0]],
..Default::default()
};
let mesh = extract_sparse_boundary(&data);
assert!(mesh.positions.is_empty());
assert!(mesh.indices.is_empty());
}
}