use rustc_hash::FxHashMap;
use std::cell::RefCell;
use crate::grid::NORMAL_QUANT_F32 as NORMAL_QUANT;
const UV_QUANT: f32 = 1.0e3;
type VKey = (u32, u32, u32, i32, i32, i32, i32, i32);
#[inline]
fn vkey(p: &[f32], n: &[f32], uv: [f32; 2]) -> VKey {
(
p[0].to_bits(),
p[1].to_bits(),
p[2].to_bits(),
(n[0] * NORMAL_QUANT).round() as i32,
(n[1] * NORMAL_QUANT).round() as i32,
(n[2] * NORMAL_QUANT).round() as i32,
(uv[0] * UV_QUANT).round() as i32,
(uv[1] * UV_QUANT).round() as i32,
)
}
#[derive(Default)]
struct WeldScratch {
map: FxHashMap<VKey, u32>,
remap: Vec<u32>,
first_vert: Vec<u32>,
}
thread_local! {
static WELD_SCRATCH: RefCell<Option<WeldScratch>> = const { RefCell::new(None) };
}
pub fn weld_indexed(
positions: &[f32],
normals: &[f32],
uvs: Option<&[f32]>,
indices: &[u32],
) -> Option<(Vec<f32>, Vec<f32>, Option<Vec<f32>>, Vec<u32>)> {
let nverts = positions.len() / 3;
let uv_len_ok = uvs.is_none_or(|u| u.len() == nverts * 2);
if normals.len() != positions.len()
|| nverts == 0
|| !uv_len_ok
|| indices.iter().any(|&i| i as usize >= nverts)
{
return None; }
let mut scratch = WELD_SCRATCH.with(|c| c.borrow_mut().take()).unwrap_or_default();
let WeldScratch { map, remap, first_vert } = &mut scratch;
map.clear();
map.reserve(nverts);
remap.clear();
remap.resize(nverts, 0u32);
first_vert.clear();
for v in 0..nverts {
let p = &positions[v * 3..v * 3 + 3];
let n = &normals[v * 3..v * 3 + 3];
let uv = match uvs {
Some(u) => [u[v * 2], u[v * 2 + 1]],
None => [0.0, 0.0],
};
let id = match map.get(&vkey(p, n, uv)) {
Some(&id) => id,
None => {
let id = first_vert.len() as u32;
first_vert.push(v as u32);
map.insert(vkey(p, n, uv), id);
id
}
};
remap[v] = id;
}
let unique = first_vert.len();
let out = if unique == nverts {
None
} else {
let mut out_pos: Vec<f32> = Vec::with_capacity(unique * 3);
let mut out_nrm: Vec<f32> = Vec::with_capacity(unique * 3);
let mut out_uv: Vec<f32> =
Vec::with_capacity(if uvs.is_some() { unique * 2 } else { 0 });
for &fv in first_vert.iter() {
let fv = fv as usize;
out_pos.extend_from_slice(&positions[fv * 3..fv * 3 + 3]);
out_nrm.extend_from_slice(&normals[fv * 3..fv * 3 + 3]);
if let Some(u) = uvs {
out_uv.extend_from_slice(&u[fv * 2..fv * 2 + 2]);
}
}
let out_idx: Vec<u32> = indices.iter().map(|&i| remap[i as usize]).collect();
Some((out_pos, out_nrm, uvs.map(|_| out_uv), out_idx))
};
WELD_SCRATCH.with(|c| *c.borrow_mut() = Some(scratch));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merges_coplanar_shared_vertices() {
let positions = vec![
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 normals = [0.0f32, 0.0, 1.0].repeat(6); let indices = vec![0, 1, 2, 3, 4, 5];
let (p, n, uv, i) = weld_indexed(&positions, &normals, None, &indices).expect("merged");
assert!(uv.is_none(), "no uvs in, no uvs out");
assert_eq!(p.len() / 3, 4, "6 authored verts -> 4 unique corners");
assert_eq!(n.len(), p.len());
assert_eq!(i.len(), 6, "triangle count unchanged");
for (orig, &ni) in indices.iter().zip(i.iter()) {
let o = *orig as usize * 3;
let w = ni as usize * 3;
assert_eq!(&positions[o..o + 3], &p[w..w + 3], "world position preserved");
}
}
#[test]
fn faceted_plate_welds_to_grid() {
const G: usize = 4;
let mut positions: Vec<f32> = Vec::new();
let mut normals: Vec<f32> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
for i in 0..G {
for j in 0..G {
let base = (positions.len() / 3) as u32;
let (x, y) = (i as f32, j as f32);
for (dx, dy) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
positions.extend_from_slice(&[x + dx, y + dy, 0.0]);
normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
}
let raw_verts = positions.len() / 3;
let (p, _n, _uv, idx) = weld_indexed(&positions, &normals, None, &indices).expect("merged");
assert_eq!(raw_verts, 4 * G * G);
assert_eq!(p.len() / 3, (G + 1) * (G + 1), "welded to unique grid points");
assert_eq!(idx.len(), indices.len(), "triangle count unchanged");
}
#[test]
fn out_of_range_index_is_a_no_op_not_a_panic() {
let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let normals = [0.0f32, 0.0, 1.0].repeat(3);
let indices = vec![0, 1, 9]; assert!(
weld_indexed(&positions, &normals, None, &indices).is_none(),
"malformed input is a no-op (None), not a panic"
);
}
#[test]
fn keeps_creases_split() {
let positions = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let normals = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0];
let indices = vec![0, 1];
assert!(
weld_indexed(&positions, &normals, None, &indices).is_none(),
"distinct normals: nothing merges, weld is a no-op"
);
}
#[test]
fn flat_shaded_cube_keeps_24_verts() {
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
([0.0, 0.0, 1.0], [[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]]),
([0.0, 0.0, -1.0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]),
([1.0, 0.0, 0.0], [[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 1.0, 1.0], [1.0, 0.0, 1.0]]),
([-1.0, 0.0, 0.0], [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]]),
([0.0, 1.0, 0.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]]),
([0.0, -1.0, 0.0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 0.0, 1.0]]),
];
let mut positions: Vec<f32> = Vec::new();
let mut normals: Vec<f32> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
for (nrm, corners) in faces {
let base = (positions.len() / 3) as u32;
for c in corners {
positions.extend_from_slice(&c);
normals.extend_from_slice(&nrm);
}
indices.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
}
assert_eq!(positions.len() / 3, 24, "6 faces * 4 corners = 24 raw verts");
assert!(
weld_indexed(&positions, &normals, None, &indices).is_none(),
"distinct per-face normals: nothing merges, all 24 verts kept (flat shading)"
);
}
#[test]
fn uv_seam_stays_split_and_uvs_stay_aligned() {
let positions = vec![
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, ];
let normals = [0.0f32, 0.0, 1.0].repeat(6);
let uvs = vec![
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, ];
let indices = vec![0, 1, 2, 3, 4, 5];
assert!(
weld_indexed(&positions, &normals, Some(&uvs), &indices).is_none(),
"the UV seam keeps all 6 verts split (nothing merges, UVs stay 1:1)"
);
}
#[test]
fn coplanar_same_uv_still_welds_and_carries_uvs() {
let positions = vec![
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 normals = [0.0f32, 0.0, 1.0].repeat(6);
let uvs = vec![
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, ];
let indices = vec![0, 1, 2, 3, 4, 5];
let (p, _n, uv, _i) =
weld_indexed(&positions, &normals, Some(&uvs), &indices).expect("merged");
let uv = uv.expect("uvs carried through");
assert_eq!(p.len() / 3, 4, "same-uv shared corners still weld to 4");
assert_eq!(uv.len(), (p.len() / 3) * 2, "uvs stay 1:1 with welded positions");
}
#[test]
fn weld_is_idempotent() {
let positions = vec![
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 normals = [0.0f32, 0.0, 1.0].repeat(6);
let indices = vec![0, 1, 2, 3, 4, 5];
let (p1, n1, _uv1, i1) =
weld_indexed(&positions, &normals, None, &indices).expect("first weld merges");
assert_eq!(p1.len() / 3, 4);
assert!(
weld_indexed(&p1, &n1, None, &i1).is_none(),
"second weld of an already-welded mesh is a no-op"
);
}
#[test]
fn deterministic_and_first_seen_order() {
let positions = vec![9.0, 9.0, 9.0, 0.0, 0.0, 0.0, 9.0, 9.0, 9.0];
let normals = vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0];
let indices = vec![0, 1, 2];
let (p1, n1, _uv1, i1) =
weld_indexed(&positions, &normals, None, &indices).expect("merged");
let (p2, n2, _uv2, i2) =
weld_indexed(&positions, &normals, None, &indices).expect("merged");
assert_eq!((&p1, &n1, &i1), (&p2, &n2, &i2), "stable across runs");
assert_eq!(p1.len() / 3, 2, "the repeated vertex 0/2 merges");
assert_eq!(&p1[0..3], &[9.0, 9.0, 9.0]);
assert_eq!(i1, vec![0, 1, 0]);
}
}