use std::collections::HashMap;
use super::types::{AttributeData, MeshData};
pub const TET_SENTINEL: u32 = u32::MAX;
#[non_exhaustive]
#[derive(Default)]
pub struct VolumeMeshData {
pub positions: Vec<[f32; 3]>,
pub cells: Vec<[u32; 8]>,
pub cell_scalars: HashMap<String, Vec<f32>>,
pub cell_colors: HashMap<String, Vec<[f32; 4]>>,
}
const TET_FACES: [[usize; 3]; 4] = [
[1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1], ];
const HEX_FACES: [[usize; 4]; 6] = [
[0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [2, 6, 7, 3], [0, 3, 7, 4], [1, 5, 6, 2], ];
type FaceKey = (u32, u32, u32);
#[inline]
fn face_key(a: u32, b: u32, c: u32) -> FaceKey {
let mut arr = [a, b, c];
arr.sort_unstable();
(arr[0], arr[1], arr[2])
}
struct FaceRecord {
cell_index: usize,
winding: [u32; 3],
count: u32,
cell_centroid: [f32; 3],
}
pub(crate) fn extract_boundary_faces(data: &VolumeMeshData) -> MeshData {
let n_verts = data.positions.len();
let mut face_map: HashMap<FaceKey, FaceRecord> = HashMap::new();
for (cell_idx, cell) in data.cells.iter().enumerate() {
let is_tet = cell[4] == TET_SENTINEL;
if is_tet {
let centroid = {
let mut c = [0.0f32; 3];
for &vi in &cell[0..4] {
let p = data.positions[vi as usize];
c[0] += p[0];
c[1] += p[1];
c[2] += p[2];
}
[c[0] / 4.0, c[1] / 4.0, c[2] / 4.0]
};
for face_local in &TET_FACES {
let a = cell[face_local[0]];
let b = cell[face_local[1]];
let c = cell[face_local[2]];
let key = face_key(a, b, c);
let entry = face_map.entry(key).or_insert(FaceRecord {
cell_index: cell_idx,
winding: [a, b, c],
count: 0,
cell_centroid: centroid,
});
entry.count += 1;
}
} else {
let centroid = {
let mut c = [0.0f32; 3];
for &vi in cell.iter() {
let p = data.positions[vi as usize];
c[0] += p[0];
c[1] += p[1];
c[2] += p[2];
}
[c[0] / 8.0, c[1] / 8.0, c[2] / 8.0]
};
for quad in &HEX_FACES {
let v: [u32; 4] = [cell[quad[0]], cell[quad[1]], cell[quad[2]], cell[quad[3]]];
{
let key = face_key(v[0], v[1], v[2]);
let entry = face_map.entry(key).or_insert(FaceRecord {
cell_index: cell_idx,
winding: [v[0], v[1], v[2]],
count: 0,
cell_centroid: centroid,
});
entry.count += 1;
}
{
let key = face_key(v[0], v[2], v[3]);
let entry = face_map.entry(key).or_insert(FaceRecord {
cell_index: cell_idx,
winding: [v[0], v[2], v[3]],
count: 0,
cell_centroid: centroid,
});
entry.count += 1;
}
}
}
}
let mut boundary: Vec<(usize, [u32; 3], [f32; 3])> = face_map
.into_values()
.filter(|r| r.count == 1)
.map(|r| (r.cell_index, r.winding, r.cell_centroid))
.collect();
boundary.sort_unstable_by_key(|(cell_idx, _, _)| *cell_idx);
for (_, tri, cell_centroid) in &mut boundary {
let pa = data.positions[tri[0] as usize];
let pb = data.positions[tri[1] as usize];
let pc = data.positions[tri[2] as usize];
let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
let normal = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let fc = [
(pa[0] + pb[0] + pc[0]) / 3.0,
(pa[1] + pb[1] + pc[1]) / 3.0,
(pa[2] + pb[2] + pc[2]) / 3.0,
];
let out = [
fc[0] - cell_centroid[0],
fc[1] - cell_centroid[1],
fc[2] - cell_centroid[2],
];
let dot = normal[0] * out[0] + normal[1] * out[1] + normal[2] * out[2];
if dot < 0.0 {
tri.swap(1, 2);
}
}
let n_boundary_tris = boundary.len();
let mut indices: Vec<u32> = Vec::with_capacity(n_boundary_tris * 3);
let mut normal_accum: Vec<[f64; 3]> = vec![[0.0; 3]; n_verts];
for (_, tri, _) in &boundary {
indices.push(tri[0]);
indices.push(tri[1]);
indices.push(tri[2]);
let pa = data.positions[tri[0] as usize];
let pb = data.positions[tri[1] as usize];
let pc = data.positions[tri[2] 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 tri {
let acc = &mut normal_accum[vi as usize];
acc[0] += n[0];
acc[1] += n[1];
acc[2] += n[2];
}
}
let mut 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();
normals.resize(n_verts, [0.0, 1.0, 0.0]);
let mut attributes: HashMap<String, AttributeData> = HashMap::new();
for (name, cell_vals) in &data.cell_scalars {
let face_scalars: Vec<f32> = boundary
.iter()
.map(|(cell_idx, _, _)| cell_vals.get(*cell_idx).copied().unwrap_or(0.0))
.collect();
attributes.insert(name.clone(), AttributeData::Face(face_scalars));
}
for (name, cell_vals) in &data.cell_colors {
let face_colors: Vec<[f32; 4]> = boundary
.iter()
.map(|(cell_idx, _, _)| cell_vals.get(*cell_idx).copied().unwrap_or([1.0; 4]))
.collect();
attributes.insert(name.clone(), AttributeData::FaceColor(face_colors));
}
MeshData {
positions: data.positions.clone(),
normals,
indices,
uvs: None,
tangents: None,
attributes,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn single_tet() -> VolumeMeshData {
VolumeMeshData {
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[0.5, 0.5, 1.0],
],
cells: vec![[
0,
1,
2,
3,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
]],
..Default::default()
}
}
fn two_tets_sharing_face() -> VolumeMeshData {
VolumeMeshData {
positions: vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[0.5, 0.5, 1.0],
[0.5, 0.5, -1.0],
],
cells: vec![
[
0,
1,
2,
3,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
],
[
0,
2,
1,
4,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
TET_SENTINEL,
],
],
..Default::default()
}
}
fn single_hex() -> VolumeMeshData {
VolumeMeshData {
positions: vec![
[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0], ],
cells: vec![[0, 1, 2, 3, 4, 5, 6, 7]],
..Default::default()
}
}
#[test]
fn single_tet_has_four_boundary_faces() {
let data = single_tet();
let mesh = extract_boundary_faces(&data);
assert_eq!(
mesh.indices.len(),
4 * 3,
"single tet -> 4 boundary triangles"
);
}
#[test]
fn two_tets_sharing_face_eliminates_shared_face() {
let data = two_tets_sharing_face();
let mesh = extract_boundary_faces(&data);
assert_eq!(
mesh.indices.len(),
6 * 3,
"two tets sharing a face -> 6 boundary triangles"
);
}
#[test]
fn single_hex_has_twelve_boundary_triangles() {
let data = single_hex();
let mesh = extract_boundary_faces(&data);
assert_eq!(
mesh.indices.len(),
12 * 3,
"single hex -> 12 boundary triangles"
);
}
#[test]
fn normals_have_correct_length() {
let data = single_tet();
let mesh = extract_boundary_faces(&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 || len < 1e-5,
"normal not unit: {n:?}"
);
}
}
#[test]
fn cell_scalar_remaps_to_face_attribute() {
let mut data = single_tet();
data.cell_scalars.insert("pressure".to_string(), vec![42.0]);
let mesh = extract_boundary_faces(&data);
match mesh.attributes.get("pressure") {
Some(AttributeData::Face(vals)) => {
assert_eq!(vals.len(), 4, "one value per boundary triangle");
for &v in vals {
assert_eq!(v, 42.0);
}
}
other => panic!("expected Face attribute, got {other:?}"),
}
}
#[test]
fn cell_color_remaps_to_face_color_attribute() {
let mut data = two_tets_sharing_face();
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_boundary_faces(&data);
match mesh.attributes.get("label") {
Some(AttributeData::FaceColor(colors)) => {
assert_eq!(colors.len(), 6, "6 boundary faces");
}
other => panic!("expected FaceColor attribute, got {other:?}"),
}
}
#[test]
fn positions_preserved_unchanged() {
let data = single_hex();
let mesh = extract_boundary_faces(&data);
assert_eq!(mesh.positions, data.positions);
}
}