use crate::mesh::Mesh;
use std::collections::BTreeMap;
const SNAP_GRID: f64 = 1.0 / 65536.0;
const NORMAL_QUANT: f64 = 1.0e3;
const MAX_OFFSET_JITTER: f64 = 50.0e-6;
const MAX_VERTEX_MOVE: f64 = 200.0e-6;
const POSITION_DEDUP_GRID: f64 = 1.0e-4;
#[inline]
fn snap_grid(c: f64) -> f64 {
(c / SNAP_GRID).round() * SNAP_GRID
}
#[inline]
fn dedup_key(c: f64) -> i64 {
(c / POSITION_DEDUP_GRID).round() as i64
}
#[inline]
fn qnorm(c: f64) -> i64 {
(c * NORMAL_QUANT).round() as i64
}
#[inline]
fn tri_normal(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Option<([f64; 3], f64)> {
let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len <= 0.0 {
return None;
}
Some(([n[0] / len, n[1] / len, n[2] / len], len))
}
pub fn weld_near_coplanar_facets(mesh: &Mesh) -> Mesh {
let vertex_count = mesh.positions.len() / 3;
let tri_count = mesh.indices.len() / 3;
if vertex_count < 3 || tri_count < 2 {
return mesh.clone();
}
let pos = |i: usize| -> [f64; 3] {
[
mesh.positions[i * 3] as f64,
mesh.positions[i * 3 + 1] as f64,
mesh.positions[i * 3 + 2] as f64,
]
};
let mut canon_of: Vec<usize> = vec![0; vertex_count];
let mut canon_pos: Vec<[f64; 3]> = Vec::new();
{
let mut seen: BTreeMap<(i64, i64, i64), usize> = BTreeMap::new();
for i in 0..vertex_count {
let p = pos(i);
let key = (dedup_key(p[0]), dedup_key(p[1]), dedup_key(p[2]));
let id = *seen.entry(key).or_insert_with(|| {
let id = canon_pos.len();
canon_pos.push(p);
id
});
canon_of[i] = id;
}
}
let n_canon = canon_pos.len();
struct Facet {
tri: [usize; 3],
normal: [f64; 3],
offset: f64, area2: f64,
}
let mut facets: Vec<Facet> = Vec::with_capacity(tri_count);
for c in mesh.indices.chunks_exact(3) {
let (i0, i1, i2) = (c[0] as usize, c[1] as usize, c[2] as usize);
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let (a, b, d) = (canon_of[i0], canon_of[i1], canon_of[i2]);
if a == b || b == d || a == d {
continue;
}
if let Some((normal, area2)) = tri_normal(canon_pos[a], canon_pos[b], canon_pos[d]) {
let offset =
normal[0] * canon_pos[a][0] + normal[1] * canon_pos[a][1] + normal[2] * canon_pos[a][2];
facets.push(Facet {
tri: [a, b, d],
normal,
offset,
area2,
});
}
}
if facets.len() < 2 {
return mesh.clone();
}
let mut normal_buckets: BTreeMap<(i64, i64, i64), Vec<usize>> = BTreeMap::new();
for (fi, f) in facets.iter().enumerate() {
let n = f.normal;
let qx = qnorm(n[0]);
let qy = qnorm(n[1]);
let qz = qnorm(n[2]);
let sgn = if qx != 0 {
qx.signum()
} else if qy != 0 {
qy.signum()
} else if qz != 0 {
qz.signum()
} else {
1
};
let key = (qx * sgn, qy * sgn, qz * sgn);
normal_buckets.entry(key).or_default().push(fi);
}
let mut vertex_moves: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_canon];
for fis in normal_buckets.values() {
if fis.len() < 2 {
continue;
}
let ref_n = facets[fis[0]].normal;
let mut keyed: Vec<(f64, usize)> = fis
.iter()
.map(|&fi| {
let n = facets[fi].normal;
let dotv = n[0] * ref_n[0] + n[1] * ref_n[1] + n[2] * ref_n[2];
let off = if dotv < 0.0 {
-facets[fi].offset
} else {
facets[fi].offset
};
(off, fi)
})
.collect();
debug_assert!(
keyed.iter().all(|k| k.0.is_finite()),
"facet offsets must be finite before the deterministic offset sort"
);
keyed.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
let mut cluster_start = 0usize;
let mut process_cluster = |slice: &[(f64, usize)]| {
if slice.len() < 2 {
return;
}
let mut members: Vec<usize> = slice.iter().map(|&(_, fi)| fi).collect();
members.sort_unstable();
let mut acc_n = [0.0f64, 0.0, 0.0];
let mut acc_off = 0.0f64;
let mut wsum = 0.0f64;
for &fi in &members {
let n = facets[fi].normal;
let dotv = n[0] * ref_n[0] + n[1] * ref_n[1] + n[2] * ref_n[2];
let s = if dotv < 0.0 { -1.0 } else { 1.0 };
let w = facets[fi].area2;
acc_n[0] += s * n[0] * w;
acc_n[1] += s * n[1] * w;
acc_n[2] += s * n[2] * w;
acc_off += s * facets[fi].offset * w;
wsum += w;
}
if wsum <= 0.0 {
return;
}
let len = (acc_n[0] * acc_n[0] + acc_n[1] * acc_n[1] + acc_n[2] * acc_n[2]).sqrt();
if len <= 0.0 {
return;
}
let pn = [acc_n[0] / len, acc_n[1] / len, acc_n[2] / len];
let pd = acc_off / wsum;
let mut seen_v: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
for &fi in &members {
for &cv in &facets[fi].tri {
if !seen_v.insert(cv) {
continue;
}
let p = canon_pos[cv];
let dist = p[0] * pn[0] + p[1] * pn[1] + p[2] * pn[2] - pd;
if dist.abs() > MAX_VERTEX_MOVE {
continue; }
let proj = [
p[0] - dist * pn[0],
p[1] - dist * pn[1],
p[2] - dist * pn[2],
];
vertex_moves[cv].push(proj);
}
}
};
for i in 1..keyed.len() {
if keyed[i].0 - keyed[i - 1].0 > MAX_OFFSET_JITTER {
process_cluster(&keyed[cluster_start..i]);
cluster_start = i;
}
}
process_cluster(&keyed[cluster_start..]);
}
let mut new_canon_pos = canon_pos.clone();
let mut any_moved = false;
for cv in 0..n_canon {
let cands = &vertex_moves[cv];
if cands.is_empty() {
continue;
}
let mut s = [0.0f64, 0.0, 0.0];
for c in cands {
s[0] += c[0];
s[1] += c[1];
s[2] += c[2];
}
let inv = 1.0 / cands.len() as f64;
let avg = [s[0] * inv, s[1] * inv, s[2] * inv];
let p = canon_pos[cv];
let d2 = (avg[0] - p[0]).powi(2) + (avg[1] - p[1]).powi(2) + (avg[2] - p[2]).powi(2);
if d2 > MAX_VERTEX_MOVE * MAX_VERTEX_MOVE {
continue;
}
new_canon_pos[cv] = [snap_grid(avg[0]), snap_grid(avg[1]), snap_grid(avg[2])];
any_moved = true;
}
if !any_moved {
return mesh.clone();
}
let mut out = mesh.clone();
for i in 0..vertex_count {
let np = new_canon_pos[canon_of[i]];
out.positions[i * 3] = np[0] as f32;
out.positions[i * 3 + 1] = np[1] as f32;
out.positions[i * 3 + 2] = np[2] as f32;
}
out
}
const SLIVER_ASPECT: f64 = 8.0;
const MAX_BISECT_ROUNDS: usize = 64;
#[inline]
fn aspect(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
let d = |p: [f64; 3], q: [f64; 3]| {
((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt()
};
let (e0, e1, e2) = (d(a, b), d(b, c), d(c, a));
let mn = e0.min(e1).min(e2);
let mx = e0.max(e1).max(e2);
if mn > 1.0e-9 {
mx / mn
} else {
f64::INFINITY
}
}
pub fn refine_high_aspect_slivers(mesh: &Mesh) -> Mesh {
let vertex_count = mesh.positions.len() / 3;
if vertex_count < 3 || mesh.indices.len() < 6 {
return mesh.clone();
}
let pos = |i: usize| -> [f64; 3] {
[
mesh.positions[i * 3] as f64,
mesh.positions[i * 3 + 1] as f64,
mesh.positions[i * 3 + 2] as f64,
]
};
let mut canon_of: Vec<usize> = vec![0; vertex_count];
let mut cpos: Vec<[f64; 3]> = Vec::new();
{
let mut seen: BTreeMap<(i64, i64, i64), usize> = BTreeMap::new();
for i in 0..vertex_count {
let p = pos(i);
let key = (dedup_key(p[0]), dedup_key(p[1]), dedup_key(p[2]));
let id = *seen.entry(key).or_insert_with(|| {
let id = cpos.len();
cpos.push(p);
id
});
canon_of[i] = id;
}
}
let mut tris: Vec<[usize; 3]> = Vec::with_capacity(mesh.indices.len() / 3);
for c in mesh.indices.chunks_exact(3) {
let (i0, i1, i2) = (c[0] as usize, c[1] as usize, c[2] as usize);
if i0 >= vertex_count || i1 >= vertex_count || i2 >= vertex_count {
continue;
}
let (a, b, d) = (canon_of[i0], canon_of[i1], canon_of[i2]);
if a == b || b == d || a == d {
continue;
}
tris.push([a, b, d]);
}
let edge_key = |u: usize, v: usize| -> (usize, usize) {
if u < v {
(u, v)
} else {
(v, u)
}
};
let mut changed_any = false;
for _round in 0..MAX_BISECT_ROUNDS {
let mut edge_tris: BTreeMap<(usize, usize), Vec<usize>> = BTreeMap::new();
for (ti, t) in tris.iter().enumerate() {
for (u, v) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
edge_tris.entry(edge_key(u, v)).or_default().push(ti);
}
}
let mut target: Option<(usize, usize)> = None;
'outer: for (ek, incident) in &edge_tris {
if incident.len() > 2 {
continue;
}
for &ti in incident {
let t = tris[ti];
let a = cpos[t[0]];
let b = cpos[t[1]];
let c = cpos[t[2]];
if aspect(a, b, c) <= SLIVER_ASPECT {
continue;
}
let d = |p: [f64; 3], q: [f64; 3]| {
((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt()
};
let e01 = d(a, b);
let e12 = d(b, c);
let e20 = d(c, a);
let longest = e01.max(e12).max(e20);
let this_len = {
let (x, y) = *ek;
let px = cpos[x];
let py = cpos[y];
d(px, py)
};
if (this_len - longest).abs() < 1.0e-9 {
target = Some(*ek);
break 'outer;
}
}
}
let Some((eu, ev)) = target else {
break; };
let incident = edge_tris.get(&(eu, ev)).cloned().unwrap_or_default();
if incident.is_empty() {
break;
}
let pm = {
let a = cpos[eu];
let b = cpos[ev];
[
snap_grid(0.5 * (a[0] + b[0])),
snap_grid(0.5 * (a[1] + b[1])),
snap_grid(0.5 * (a[2] + b[2])),
]
};
let mid = cpos.len();
cpos.push(pm);
let inc_set: std::collections::BTreeSet<usize> = incident.iter().copied().collect();
let mut new_tris: Vec<[usize; 3]> = Vec::with_capacity(tris.len() + incident.len());
for (ti, t) in tris.iter().enumerate() {
if !inc_set.contains(&ti) {
new_tris.push(*t);
continue;
}
let mut split = false;
for k in 0..3 {
let u = t[k];
let v = t[(k + 1) % 3];
let w = t[(k + 2) % 3];
if edge_key(u, v) == (eu, ev) {
new_tris.push([u, mid, w]);
new_tris.push([mid, v, w]);
split = true;
break;
}
}
if !split {
new_tris.push(*t);
}
}
tris = new_tris;
changed_any = true;
}
if !changed_any {
return mesh.clone();
}
let mut out = Mesh::with_capacity(tris.len() * 3, tris.len() * 3);
for t in &tris {
let a = cpos[t[0]];
let b = cpos[t[1]];
let c = cpos[t[2]];
let n = tri_normal(a, b, c).map(|(n, _)| n).unwrap_or([0.0, 0.0, 1.0]);
let base = (out.positions.len() / 3) as u32;
for p in [a, b, c] {
out.positions
.extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
out.normals
.extend_from_slice(&[n[0] as f32, n[1] as f32, n[2] as f32]);
}
out.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
out.rtc_applied = mesh.rtc_applied;
out
}
#[cfg(test)]
mod tests {
use super::*;
fn mesh_from_tris(tris: &[[[f64; 3]; 3]]) -> Mesh {
let mut m = Mesh::new();
for t in tris {
let base = (m.positions.len() / 3) as u32;
for p in t {
m.positions
.extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
}
fn vert(m: &Mesh, i: usize) -> [f64; 3] {
[
m.positions[i * 3] as f64,
m.positions[i * 3 + 1] as f64,
m.positions[i * 3 + 2] as f64,
]
}
fn distinct_offset_buckets(m: &Mesh) -> usize {
use std::collections::BTreeSet;
let mut set: BTreeSet<i64> = BTreeSet::new();
for c in m.indices.chunks_exact(3) {
let a = vert(m, c[0] as usize);
let b = vert(m, c[1] as usize);
let d = vert(m, c[2] as usize);
if let Some((n, _)) = super::tri_normal(a, b, d) {
let s = if n[0] + n[1] + n[2] < 0.0 { -1.0 } else { 1.0 };
let off = (n[0] * a[0] + n[1] * a[1] + n[2] * a[2]) * s;
set.insert((off * 1.0e6).round() as i64);
}
}
set.len()
}
#[test]
fn welds_offset_jitter_not_distinct_plane() {
let j = 15.0e-6;
let jitter = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[1.0, 0.0, j], [1.0, 1.0, j], [0.0, 1.0, j]],
]);
assert_eq!(
distinct_offset_buckets(&jitter),
2,
"pre-weld the two facets must sit on distinct 1µm offset buckets"
);
let welded = weld_near_coplanar_facets(&jitter);
assert_eq!(
distinct_offset_buckets(&welded),
1,
"15µm offset jitter must weld to ONE offset bucket"
);
let distinct = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[1.0, 0.0, 0.4], [1.0, 1.0, 0.4], [0.0, 1.0, 0.4]],
]);
let welded_d = weld_near_coplanar_facets(&distinct);
assert_eq!(
distinct_offset_buckets(&welded_d),
2,
"0.4m-apart planes must NOT merge"
);
}
#[test]
fn welds_small_angle_not_real_feature() {
let small = (0.09_f64).to_radians().tan();
let big = (0.5_f64).to_radians().tan();
let jitter = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, small]],
]);
let welded = weld_near_coplanar_facets(&jitter);
assert_eq!(
distinct_offset_buckets(&welded),
1,
"0.09° + same-bucket-normal jitter must weld coplanar"
);
let feature = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, big]],
]);
let before = distinct_offset_buckets(&feature);
let welded_f = weld_near_coplanar_facets(&feature);
let after = distinct_offset_buckets(&welded_f);
assert_eq!(
before, after,
"a real 0.5° feature must NOT weld (distinct normal bucket)"
);
}
#[test]
fn flat_pair_is_noop_topology() {
let flat = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
]);
let welded = weld_near_coplanar_facets(&flat);
assert_eq!(welded.indices, flat.indices, "topology must be preserved");
assert_eq!(welded.positions.len(), flat.positions.len());
}
#[test]
fn weld_is_deterministic() {
let j = 15.0e-6;
let m = mesh_from_tris(&[
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
[[1.0, 0.0, j], [1.0, 1.0, j], [0.0, 1.0, j]],
[[2.0, 0.0, j], [3.0, 0.0, 0.0], [2.0, 1.0, j]],
]);
let a = weld_near_coplanar_facets(&m);
let b = weld_near_coplanar_facets(&m);
assert_eq!(a.positions, b.positions);
assert_eq!(a.indices, b.indices);
}
}