use std::collections::HashMap;
use crate::triangle_mesh::TriangleMesh;
use oxiphysics_core::math::Vec3;
#[inline]
fn vec3_to_arr(v: Vec3) -> [f64; 3] {
[v.x, v.y, v.z]
}
#[inline]
fn arr_to_vec3(a: [f64; 3]) -> Vec3 {
Vec3::new(a[0], a[1], a[2])
}
#[inline]
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
fn len3(a: [f64; 3]) -> f64 {
dot3(a, a).sqrt()
}
#[inline]
fn normalize3(a: [f64; 3]) -> [f64; 3] {
let l = len3(a);
if l < f64::EPSILON {
[0.0, 0.0, 0.0]
} else {
[a[0] / l, a[1] / l, a[2] / l]
}
}
#[inline]
fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
len3(sub3(a, b))
}
#[inline]
fn midpoint(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
scale3(add3(a, b), 0.5)
}
pub struct UniformRemesher {
pub target_edge_length: f64,
}
impl UniformRemesher {
pub fn new(target_edge_length: f64) -> Self {
Self { target_edge_length }
}
pub fn remesh(&self, mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
isotropic_remesh(mesh, self.target_edge_length, iterations)
}
}
pub fn isotropic_remesh(mesh: &TriangleMesh, target_len: f64, iterations: usize) -> TriangleMesh {
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
let orig_verts = verts.clone();
let orig_tris = tris.clone();
let high = target_len * 4.0 / 3.0;
let low = target_len * 4.0 / 5.0;
for _ in 0..iterations {
split_long_edges(&mut verts, &mut tris, high);
collapse_short_edges(&mut verts, &mut tris, low);
flip_for_valence(&mut tris, verts.len());
laplacian_smooth_surface(&mut verts, &tris);
project_to_surface(&mut verts, &orig_verts, &orig_tris);
}
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris)
}
fn split_long_edges(verts: &mut Vec<[f64; 3]>, tris: &mut Vec<[usize; 3]>, max_len: f64) {
let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
let mut new_tris: Vec<[usize; 3]> = Vec::new();
let mut keep = vec![true; tris.len()];
for (ti, tri) in tris.iter().enumerate() {
let [a, b, c] = *tri;
let pa = verts[a];
let pb = verts[b];
let pc = verts[c];
let lab = dist3(pa, pb);
let lbc = dist3(pb, pc);
let lca = dist3(pc, pa);
let (longest, va, vb, vopp) = if lab >= lbc && lab >= lca {
(lab, a, b, c)
} else if lbc >= lab && lbc >= lca {
(lbc, b, c, a)
} else {
(lca, c, a, b)
};
if longest > max_len {
let key = (va.min(vb), va.max(vb));
let mid = if let Some(&existing) = edge_midpoints.get(&key) {
existing
} else {
let idx = verts.len();
verts.push(midpoint(verts[va], verts[vb]));
edge_midpoints.insert(key, idx);
idx
};
new_tris.push([va, mid, vopp]);
new_tris.push([mid, vb, vopp]);
keep[ti] = false;
}
}
let orig: Vec<[usize; 3]> = tris
.iter()
.enumerate()
.filter_map(|(i, &t)| if keep[i] { Some(t) } else { None })
.collect();
*tris = orig;
tris.extend(new_tris);
}
fn collapse_short_edges(verts: &mut [[f64; 3]], tris: &mut Vec<[usize; 3]>, min_len: f64) {
let n = verts.len();
let mut remap: Vec<usize> = (0..n).collect();
let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
for tri in tris.iter() {
for k in 0..3 {
let a = tri[k];
let b = tri[(k + 1) % 3];
let key = (a.min(b), a.max(b));
edge_set.insert(key, true);
}
}
for (a, b) in edge_set.keys() {
let ca = remap[*a];
let cb = remap[*b];
if ca == cb {
continue;
}
let d = dist3(verts[ca], verts[cb]);
if d < min_len {
let mp = midpoint(verts[ca], verts[cb]);
verts[ca] = mp;
for r in remap.iter_mut() {
if *r == cb {
*r = ca;
}
}
}
}
let new_tris: Vec<[usize; 3]> = tris
.iter()
.map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
.filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
.collect();
*tris = new_tris;
}
fn flip_for_valence(tris: &mut [[usize; 3]], n_verts: usize) {
let mut valence = vec![0usize; n_verts];
for tri in tris.iter() {
for &vi in tri.iter() {
if vi < n_verts {
valence[vi] += 1;
}
}
}
let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
for (ti, tri) in tris.iter().enumerate() {
for k in 0..3 {
let a = tri[k];
let b = tri[(k + 1) % 3];
let opp = tri[(k + 2) % 3];
let key = (a.min(b), a.max(b));
edge_map.entry(key).or_default().push((ti, opp));
}
}
for ((ea, eb), entries) in &edge_map {
if entries.len() != 2 {
continue;
}
let (ti, oc) = entries[0];
let (tj, od) = entries[1];
let dev_before = valence_deviation(valence[*ea], 6)
+ valence_deviation(valence[*eb], 6)
+ valence_deviation(valence[oc], 6)
+ valence_deviation(valence[od], 6);
let dev_after = valence_deviation(valence[*ea] - 1, 6)
+ valence_deviation(valence[*eb] - 1, 6)
+ valence_deviation(valence[oc] + 1, 6)
+ valence_deviation(valence[od] + 1, 6);
if dev_after < dev_before {
tris[ti] = [*ea, oc, od];
tris[tj] = [*eb, od, oc];
valence[*ea] -= 1;
valence[*eb] -= 1;
valence[oc] += 1;
valence[od] += 1;
}
}
}
fn valence_deviation(v: usize, ideal: usize) -> i64 {
let d = v as i64 - ideal as i64;
d * d
}
fn laplacian_smooth_surface(verts: &mut [[f64; 3]], tris: &[[usize; 3]]) {
let n = verts.len();
let mut sums = vec![[0.0_f64; 3]; n];
let mut counts = vec![0usize; n];
for tri in tris {
for k in 0..3 {
let vi = tri[k];
for (j, &vj) in tri.iter().enumerate() {
if j != k {
sums[vi] = add3(sums[vi], verts[vj]);
counts[vi] += 1;
}
}
}
}
for i in 0..n {
if counts[i] > 0 {
let avg = scale3(sums[i], 1.0 / counts[i] as f64);
let delta = sub3(avg, verts[i]);
verts[i] = add3(verts[i], scale3(delta, 0.5));
}
}
}
fn project_to_surface(verts: &mut [[f64; 3]], orig_verts: &[[f64; 3]], orig_tris: &[[usize; 3]]) {
for v in verts.iter_mut() {
let mut best_dist = f64::INFINITY;
let mut best_point = *v;
for tri in orig_tris {
let a = orig_verts[tri[0]];
let b = orig_verts[tri[1]];
let c = orig_verts[tri[2]];
let cp = closest_point_on_triangle(*v, a, b, c);
let d = dist3(*v, cp);
if d < best_dist {
best_dist = d;
best_point = cp;
}
}
*v = best_point;
}
}
fn closest_point_on_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
let ab = sub3(b, a);
let ac = sub3(c, a);
let ap = sub3(p, a);
let d1 = dot3(ab, ap);
let d2 = dot3(ac, ap);
if d1 <= 0.0 && d2 <= 0.0 {
return a;
}
let bp = sub3(p, b);
let d3 = dot3(ab, bp);
let d4 = dot3(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
return b;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
return add3(a, scale3(ab, v));
}
let cp = sub3(p, c);
let d5 = dot3(ab, cp);
let d6 = dot3(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
return c;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
return add3(a, scale3(ac, w));
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return add3(b, scale3(sub3(c, b), w));
}
let denom = 1.0 / (va + vb + vc);
let v = vb * denom;
let w = vc * denom;
add3(add3(a, scale3(ab, v)), scale3(ac, w))
}
pub struct LoopSubdivision;
impl LoopSubdivision {
pub fn subdivide(mesh: &TriangleMesh) -> TriangleMesh {
let n_orig = mesh.vertices.len();
let mut new_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
let get_or_create_midpoint = |a: usize,
b: usize,
verts: &mut Vec<[f64; 3]>,
map: &mut HashMap<(usize, usize), usize>|
-> usize {
let key = (a.min(b), a.max(b));
if let Some(&idx) = map.get(&key) {
return idx;
}
let mid = midpoint(verts[a], verts[b]);
let idx = verts.len();
verts.push(mid);
map.insert(key, idx);
idx
};
let mut new_tris: Vec<[usize; 3]> = Vec::with_capacity(mesh.indices.len() * 4);
for tri in &mesh.indices {
let [a, b, c] = *tri;
let mab = get_or_create_midpoint(a, b, &mut new_verts, &mut edge_midpoints);
let mbc = get_or_create_midpoint(b, c, &mut new_verts, &mut edge_midpoints);
let mca = get_or_create_midpoint(c, a, &mut new_verts, &mut edge_midpoints);
new_tris.push([a, mab, mca]);
new_tris.push([mab, b, mbc]);
new_tris.push([mca, mbc, c]);
new_tris.push([mab, mbc, mca]);
}
let mut neighbours: Vec<Vec<usize>> = vec![Vec::new(); n_orig];
for tri in &mesh.indices {
let [a, b, c] = *tri;
if !neighbours[a].contains(&b) {
neighbours[a].push(b);
}
if !neighbours[a].contains(&c) {
neighbours[a].push(c);
}
if !neighbours[b].contains(&a) {
neighbours[b].push(a);
}
if !neighbours[b].contains(&c) {
neighbours[b].push(c);
}
if !neighbours[c].contains(&a) {
neighbours[c].push(a);
}
if !neighbours[c].contains(&b) {
neighbours[c].push(b);
}
}
for i in 0..n_orig {
let k = neighbours[i].len();
if k == 0 {
continue;
}
let beta = if k == 3 {
3.0 / 16.0
} else {
3.0 / (8.0 * k as f64)
};
let self_weight = 1.0 - k as f64 * beta;
let mut new_pos = scale3(new_verts[i], self_weight);
for &nb in &neighbours[i] {
new_pos = add3(new_pos, scale3(new_verts[nb], beta));
}
new_verts[i] = new_pos;
}
let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, new_tris)
}
}
pub struct CatmullClark;
impl CatmullClark {
pub fn subdivide_quad_mesh(
verts: &[[f64; 3]],
quads: &[[usize; 4]],
) -> (Vec<[f64; 3]>, Vec<[usize; 4]>) {
let n_orig = verts.len();
let mut new_verts: Vec<[f64; 3]> = verts.to_vec();
let face_point_start = new_verts.len();
let mut face_points: Vec<usize> = Vec::with_capacity(quads.len());
for quad in quads {
let fp = scale3(
quad.iter()
.fold([0.0_f64; 3], |acc, &vi| add3(acc, verts[vi])),
0.25,
);
face_points.push(new_verts.len());
new_verts.push(fp);
}
let mut edge_map: HashMap<(usize, usize), Vec<usize>> = HashMap::new(); for (qi, quad) in quads.iter().enumerate() {
for k in 0..4 {
let a = quad[k];
let b = quad[(k + 1) % 4];
let key = (a.min(b), a.max(b));
edge_map.entry(key).or_default().push(qi);
}
}
let edge_point_start = new_verts.len();
let mut edge_to_idx: HashMap<(usize, usize), usize> = HashMap::new();
for (&(ea, eb), face_idxs) in &edge_map {
let mut ep = scale3(add3(verts[ea], verts[eb]), 0.5);
if face_idxs.len() == 2 {
let fp0 = new_verts[face_point_start + face_idxs[0]];
let fp1 = new_verts[face_point_start + face_idxs[1]];
ep = scale3(add3(add3(verts[ea], verts[eb]), add3(fp0, fp1)), 0.25);
}
edge_to_idx.insert((ea, eb), new_verts.len());
edge_to_idx.insert((eb, ea), new_verts.len());
new_verts.push(ep);
}
let _ = edge_point_start;
let mut vertex_face_points: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
let mut vertex_edge_mids: Vec<Vec<[f64; 3]>> = vec![Vec::new(); n_orig];
for (qi, quad) in quads.iter().enumerate() {
let fp = new_verts[face_point_start + qi];
for k in 0..4 {
let vi = quad[k];
vertex_face_points[vi].push(fp);
let nb = quad[(k + 1) % 4];
let em = scale3(add3(verts[vi], verts[nb]), 0.5);
vertex_edge_mids[vi].push(em);
let nb2 = quad[(k + 3) % 4];
let em2 = scale3(add3(verts[vi], verts[nb2]), 0.5);
vertex_edge_mids[vi].push(em2);
}
}
for i in 0..n_orig {
let n = vertex_face_points[i].len();
if n == 0 {
continue;
}
let n_f = n as f64;
let f_avg = scale3(
vertex_face_points[i]
.iter()
.fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
1.0 / n_f,
);
let e_count = vertex_edge_mids[i].len();
let e_avg = if e_count > 0 {
scale3(
vertex_edge_mids[i]
.iter()
.fold([0.0_f64; 3], |acc, &x| add3(acc, x)),
1.0 / e_count as f64,
)
} else {
verts[i]
};
let v_new = scale3(
add3(
add3(f_avg, scale3(e_avg, 2.0)),
scale3(verts[i], (n_f - 3.0).max(0.0)),
),
1.0 / n_f,
);
new_verts[i] = v_new;
}
let mut new_quads: Vec<[usize; 4]> = Vec::with_capacity(quads.len() * 4);
for (qi, quad) in quads.iter().enumerate() {
let fp_idx = face_point_start + qi;
for k in 0..4 {
let vi = quad[k];
let ea = quad[k];
let eb = quad[(k + 1) % 4];
let ea2 = quad[(k + 3) % 4];
let eb2 = quad[k];
let ep1 = *edge_to_idx.get(&(ea.min(eb), ea.max(eb))).unwrap_or(&vi);
let ep2 = *edge_to_idx
.get(&(ea2.min(eb2), ea2.max(eb2)))
.unwrap_or(&vi);
new_quads.push([vi, ep1, fp_idx, ep2]);
}
}
(new_verts, new_quads)
}
}
pub fn feature_preserving_remesh(
mesh: &TriangleMesh,
target_len: f64,
iterations: usize,
feature_angle_rad: f64,
) -> TriangleMesh {
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
let feature_edges = detect_feature_edges(&verts, &tris, feature_angle_rad);
let orig_verts = verts.clone();
let orig_tris = tris.clone();
let high = target_len * 4.0 / 3.0;
let low = target_len * 4.0 / 5.0;
for _ in 0..iterations {
split_long_edges(&mut verts, &mut tris, high);
collapse_short_edges_preserving(&mut verts, &mut tris, low, &feature_edges);
flip_for_valence(&mut tris, verts.len());
laplacian_smooth_surface(&mut verts, &tris);
project_to_surface(&mut verts, &orig_verts, &orig_tris);
}
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris)
}
fn detect_feature_edges(
verts: &[[f64; 3]],
tris: &[[usize; 3]],
threshold_rad: f64,
) -> HashMap<(usize, usize), bool> {
let mut edge_faces: HashMap<(usize, usize), Vec<[f64; 3]>> = HashMap::new();
for tri in tris {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
let e1 = sub3(b, a);
let e2 = sub3(c, a);
let n = normalize3(cross3(e1, e2));
for k in 0..3 {
let va = tri[k];
let vb = tri[(k + 1) % 3];
let key = (va.min(vb), va.max(vb));
edge_faces.entry(key).or_default().push(n);
}
}
let mut features = HashMap::new();
let cos_thresh = threshold_rad.cos();
for ((ea, eb), normals) in &edge_faces {
let is_feature = if normals.len() == 2 {
let d = dot3(normals[0], normals[1]);
d < cos_thresh
} else {
true };
features.insert((*ea, *eb), is_feature);
}
features
}
fn collapse_short_edges_preserving(
verts: &mut [[f64; 3]],
tris: &mut Vec<[usize; 3]>,
min_len: f64,
feature_edges: &HashMap<(usize, usize), bool>,
) {
let n = verts.len();
let mut remap: Vec<usize> = (0..n).collect();
let mut edge_set: HashMap<(usize, usize), bool> = HashMap::new();
for tri in tris.iter() {
for k in 0..3 {
let a = tri[k];
let b = tri[(k + 1) % 3];
let key = (a.min(b), a.max(b));
edge_set.insert(key, true);
}
}
for (a, b) in edge_set.keys() {
let ca = remap[*a];
let cb = remap[*b];
if ca == cb {
continue;
}
if feature_edges
.get(&(*a.min(b), *a.max(b)))
.copied()
.unwrap_or(false)
{
continue;
}
let d = dist3(verts[ca], verts[cb]);
if d < min_len {
let mp = midpoint(verts[ca], verts[cb]);
verts[ca] = mp;
for r in remap.iter_mut() {
if *r == cb {
*r = ca;
}
}
}
}
let new_tris: Vec<[usize; 3]> = tris
.iter()
.map(|tri| [remap[tri[0]], remap[tri[1]], remap[tri[2]]])
.filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
.collect();
*tris = new_tris;
}
pub fn flip_edges_for_quality(tris: &mut [[usize; 3]], verts: &[[f64; 3]]) {
let n_verts = verts.len();
let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
for (ti, tri) in tris.iter().enumerate() {
for k in 0..3 {
let a = tri[k];
let b = tri[(k + 1) % 3];
let opp = tri[(k + 2) % 3];
let key = (a.min(b), a.max(b));
edge_map.entry(key).or_default().push((ti, opp));
}
}
for ((ea, eb), entries) in &edge_map {
if entries.len() != 2 {
continue;
}
let (ti, oc) = entries[0];
let (tj, od) = entries[1];
if *ea >= n_verts || *eb >= n_verts || oc >= n_verts || od >= n_verts {
continue;
}
let before_min = min_triangle_angle(verts[*ea], verts[*eb], verts[oc])
.min(min_triangle_angle(verts[*ea], verts[*eb], verts[od]));
let after_min = min_triangle_angle(verts[oc], verts[od], verts[*ea])
.min(min_triangle_angle(verts[oc], verts[od], verts[*eb]));
if after_min > before_min + 1e-6 {
tris[ti] = [*ea, oc, od];
tris[tj] = [*eb, od, oc];
}
}
}
fn min_triangle_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
let ab = sub3(b, a);
let ac = sub3(c, a);
let bc = sub3(c, b);
let lab = len3(ab);
let lac = len3(ac);
let lbc = len3(bc);
if lab < 1e-12 || lac < 1e-12 || lbc < 1e-12 {
return 0.0;
}
let cos_a = (dot3(ab, ac) / (lab * lac)).clamp(-1.0, 1.0);
let cos_b = (dot3(scale3(ab, -1.0), bc) / (lab * lbc)).clamp(-1.0, 1.0);
let cos_c = (dot3(scale3(ac, -1.0), scale3(bc, -1.0)) / (lac * lbc)).clamp(-1.0, 1.0);
cos_a.acos().min(cos_b.acos()).min(cos_c.acos())
}
pub fn mesh_quality_min_angle(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
if tris.is_empty() {
return 0.0;
}
let ideal = std::f64::consts::PI / 3.0;
let min_q = tris
.iter()
.map(|tri| {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
let min_ang = min_triangle_angle(a, b, c);
min_ang / ideal
})
.fold(f64::INFINITY, f64::min);
min_q.min(1.0)
}
pub fn mesh_aspect_ratio_avg(verts: &[[f64; 3]], tris: &[[usize; 3]]) -> f64 {
if tris.is_empty() {
return 0.0;
}
let ratios: Vec<f64> = tris
.iter()
.map(|tri| {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
let lab = len3(sub3(b, a));
let lbc = len3(sub3(c, b));
let lca = len3(sub3(a, c));
let longest = lab.max(lbc).max(lca);
let s = (lab + lbc + lca) * 0.5; let area_sq = s * (s - lab) * (s - lbc) * (s - lca);
if area_sq <= 0.0 || s < 1e-12 {
return f64::INFINITY;
}
let area = area_sq.sqrt();
let inradius = area / s;
longest / (2.0 * inradius)
})
.collect();
let finite: Vec<f64> = ratios.into_iter().filter(|x| x.is_finite()).collect();
if finite.is_empty() {
0.0
} else {
finite.iter().sum::<f64>() / finite.len() as f64
}
}
pub fn tangent_laplacian_smooth(mesh: &TriangleMesh, iterations: usize) -> TriangleMesh {
let orig_verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let tris = &mesh.indices;
let mut verts = orig_verts.clone();
for _ in 0..iterations {
laplacian_smooth_surface(&mut verts, tris);
project_to_surface(&mut verts, &orig_verts, tris);
}
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris.clone())
}
pub fn collapse_edge(verts: &mut [[f64; 3]], tris: &mut Vec<[usize; 3]>, v0: usize, v1: usize) {
let mid = midpoint(verts[v0], verts[v1]);
verts[v0] = mid;
let new_tris: Vec<[usize; 3]> = tris
.iter()
.map(|tri| {
[
if tri[0] == v1 { v0 } else { tri[0] },
if tri[1] == v1 { v0 } else { tri[1] },
if tri[2] == v1 { v0 } else { tri[2] },
]
})
.filter(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2])
.collect();
*tris = new_tris;
}
pub fn laplacian_smooth(mesh: &TriangleMesh, iterations: usize, lambda: f64) -> TriangleMesh {
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let tris = &mesh.indices;
for _ in 0..iterations {
let n = verts.len();
let mut sums = vec![[0.0_f64; 3]; n];
let mut counts = vec![0usize; n];
for tri in tris.iter() {
for k in 0..3 {
let vi = tri[k];
for (j, &vj) in tri.iter().enumerate() {
if j != k {
sums[vi] = add3(sums[vi], verts[vj]);
counts[vi] += 1;
}
}
}
}
for i in 0..n {
if counts[i] > 0 {
let avg = scale3(sums[i], 1.0 / counts[i] as f64);
let delta = sub3(avg, verts[i]);
verts[i] = add3(verts[i], scale3(delta, lambda));
}
}
}
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris.clone())
}
pub fn delaunay_edge_flip(tris: &mut [[usize; 3]], verts: &[[f64; 3]]) {
let mut changed = true;
let max_passes = 32;
let mut pass = 0;
while changed && pass < max_passes {
changed = false;
pass += 1;
let mut edge_map: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
for (ti, tri) in tris.iter().enumerate() {
for k in 0..3 {
let a = tri[k];
let b = tri[(k + 1) % 3];
let opp = tri[(k + 2) % 3];
let key = (a.min(b), a.max(b));
edge_map.entry(key).or_default().push((ti, opp));
}
}
for ((ea, eb), entries) in &edge_map {
if entries.len() != 2 {
continue;
}
let (ti, oc) = entries[0];
let (tj, od) = entries[1];
if *ea >= verts.len() || *eb >= verts.len() || oc >= verts.len() || od >= verts.len() {
continue;
}
if point_in_circumcircle(verts[*ea], verts[*eb], verts[oc], verts[od]) {
tris[ti] = [*ea, oc, od];
tris[tj] = [*eb, od, oc];
changed = true;
}
}
}
}
fn point_in_circumcircle(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> bool {
let ax = a[0] - d[0];
let az = a[2] - d[2];
let bx = b[0] - d[0];
let bz = b[2] - d[2];
let cx = c[0] - d[0];
let cz = c[2] - d[2];
let det = ax * (bz * (cx * cx + cz * cz) - cz * (bx * bx + bz * bz))
- az * (bx * (cx * cx + cz * cz) - cx * (bx * bx + bz * bz))
+ (ax * ax + az * az) * (bx * cz - bz * cx);
det > 0.0
}
pub fn coarsen_mesh(mesh: &TriangleMesh, target_len: f64) -> TriangleMesh {
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
for _ in 0..10 {
let before = tris.len();
collapse_short_edges(&mut verts, &mut tris, target_len);
if tris.len() == before {
break;
}
}
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris)
}
pub fn adaptive_refine_by_curvature(mesh: &TriangleMesh, threshold: f64) -> TriangleMesh {
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris: Vec<[usize; 3]> = mesh.indices.clone();
let face_normals: Vec<[f64; 3]> = tris
.iter()
.map(|tri| {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
normalize3(cross3(sub3(b, a), sub3(c, a)))
})
.collect();
let n = verts.len();
let mut vertex_faces: Vec<Vec<usize>> = vec![Vec::new(); n];
for (ti, tri) in tris.iter().enumerate() {
for &vi in tri.iter() {
vertex_faces[vi].push(ti);
}
}
let vertex_curvature: Vec<f64> = (0..n)
.map(|vi| {
let faces = &vertex_faces[vi];
if faces.len() < 2 {
return 0.0;
}
let mut max_angle: f64 = 0.0;
for i in 0..faces.len() {
for j in (i + 1)..faces.len() {
let d = dot3(face_normals[faces[i]], face_normals[faces[j]]).clamp(-1.0, 1.0);
let angle = d.acos();
if angle > max_angle {
max_angle = angle;
}
}
}
max_angle
})
.collect();
let mut new_tris: Vec<[usize; 3]> = Vec::new();
let mut kept_tris: Vec<[usize; 3]> = Vec::new();
for tri in &tris {
let max_curv = vertex_curvature[tri[0]]
.max(vertex_curvature[tri[1]])
.max(vertex_curvature[tri[2]]);
if max_curv > threshold {
let a = tri[0];
let b = tri[1];
let c = tri[2];
let lab = dist3(verts[a], verts[b]);
let lbc = dist3(verts[b], verts[c]);
let lca = dist3(verts[c], verts[a]);
let (va, vb, vc) = if lab >= lbc && lab >= lca {
(a, b, c)
} else if lbc >= lab && lbc >= lca {
(b, c, a)
} else {
(c, a, b)
};
let mid_idx = verts.len();
verts.push(midpoint(verts[va], verts[vb]));
new_tris.push([va, mid_idx, vc]);
new_tris.push([mid_idx, vb, vc]);
} else {
kept_tris.push(*tri);
}
}
tris = kept_tris;
tris.extend(new_tris);
let vertices: Vec<Vec3> = verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, tris)
}
pub fn vertex_clustering_decimate(mesh: &TriangleMesh, cells_per_axis: usize) -> TriangleMesh {
if cells_per_axis == 0 || mesh.vertices.is_empty() {
return mesh.clone();
}
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut bbmin = verts[0];
let mut bbmax = verts[0];
for &v in &verts {
for k in 0..3 {
if v[k] < bbmin[k] {
bbmin[k] = v[k];
}
if v[k] > bbmax[k] {
bbmax[k] = v[k];
}
}
}
let extent = [
bbmax[0] - bbmin[0] + 1e-8,
bbmax[1] - bbmin[1] + 1e-8,
bbmax[2] - bbmin[2] + 1e-8,
];
let c = cells_per_axis as f64;
let cell_of = |v: [f64; 3]| -> usize {
let ix = ((v[0] - bbmin[0]) / extent[0] * c).floor() as usize;
let iy = ((v[1] - bbmin[1]) / extent[1] * c).floor() as usize;
let iz = ((v[2] - bbmin[2]) / extent[2] * c).floor() as usize;
let ix = ix.min(cells_per_axis - 1);
let iy = iy.min(cells_per_axis - 1);
let iz = iz.min(cells_per_axis - 1);
ix + cells_per_axis * (iy + cells_per_axis * iz)
};
let n_cells = cells_per_axis * cells_per_axis * cells_per_axis;
let mut cell_sums: Vec<[f64; 3]> = vec![[0.0; 3]; n_cells];
let mut cell_counts: Vec<usize> = vec![0; n_cells];
let mut vertex_cell: Vec<usize> = vec![0; verts.len()];
for (i, &v) in verts.iter().enumerate() {
let cell = cell_of(v);
vertex_cell[i] = cell;
for k in 0..3 {
cell_sums[cell][k] += v[k];
}
cell_counts[cell] += 1;
}
let mut cell_to_vert: Vec<Option<usize>> = vec![None; n_cells];
let mut new_verts: Vec<[f64; 3]> = Vec::new();
for cell in 0..n_cells {
if cell_counts[cell] > 0 {
cell_to_vert[cell] = Some(new_verts.len());
let cnt = cell_counts[cell] as f64;
new_verts.push([
cell_sums[cell][0] / cnt,
cell_sums[cell][1] / cnt,
cell_sums[cell][2] / cnt,
]);
}
}
let new_tris: Vec<[usize; 3]> = mesh
.indices
.iter()
.filter_map(|tri| {
let a = cell_to_vert[vertex_cell[tri[0]]]?;
let b = cell_to_vert[vertex_cell[tri[1]]]?;
let c = cell_to_vert[vertex_cell[tri[2]]]?;
if a == b || b == c || a == c {
return None;
}
Some([a, b, c])
})
.collect();
let vertices: Vec<Vec3> = new_verts.iter().map(|&a| arr_to_vec3(a)).collect();
TriangleMesh::new(vertices, new_tris)
}
pub fn saliency_weighted_remesh(
mesh: &TriangleMesh,
saliency: &[f64],
iterations: usize,
) -> TriangleMesh {
if mesh.vertices.is_empty() {
return mesh.clone();
}
let tri_saliency: Vec<f64> = mesh
.indices
.iter()
.map(|tri| {
let s = [
if tri[0] < saliency.len() {
saliency[tri[0]]
} else {
0.5
},
if tri[1] < saliency.len() {
saliency[tri[1]]
} else {
0.5
},
if tri[2] < saliency.len() {
saliency[tri[2]]
} else {
0.5
},
];
(s[0] + s[1] + s[2]) / 3.0
})
.collect();
let avg_sal = if tri_saliency.is_empty() {
0.5
} else {
tri_saliency.iter().sum::<f64>() / tri_saliency.len() as f64
};
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let avg_edge: f64 = {
let total: f64 = mesh
.indices
.iter()
.map(|tri| {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
(dist3(a, b) + dist3(b, c) + dist3(c, a)) / 3.0
})
.sum();
if mesh.indices.is_empty() {
1.0
} else {
total / mesh.indices.len() as f64
}
};
let target = avg_edge * (1.5 - avg_sal.clamp(0.0, 1.0));
isotropic_remesh(mesh, target.max(1e-6), iterations)
}
#[cfg(test)]
mod tests {
use super::*;
fn simple_quad_mesh() -> TriangleMesh {
let verts = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
];
let tris = vec![[0usize, 1, 2], [0, 2, 3]];
TriangleMesh::new(verts, tris)
}
#[test]
fn test_loop_subdivision_face_count() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
assert_eq!(
sub.indices.len(),
mesh.indices.len() * 4,
"Loop subdivision should quadruple the face count"
);
}
#[test]
fn test_loop_subdivision_vertex_count_increases() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
assert!(
sub.vertices.len() > mesh.vertices.len(),
"Loop subdivision should add vertices"
);
}
#[test]
fn test_catmull_clark_face_count() {
let verts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
assert_eq!(
new_quads.len(),
quads.len() * 4,
"Catmull-Clark should quadruple the quad count"
);
}
#[test]
fn test_catmull_clark_two_quads() {
let verts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[2.0, 1.0, 0.0],
];
let quads: Vec<[usize; 4]> = vec![[0, 1, 4, 3], [1, 2, 5, 4]];
let (_, new_quads) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
assert_eq!(new_quads.len(), quads.len() * 4);
}
#[test]
fn test_isotropic_remesh_returns_mesh() {
let mesh = simple_quad_mesh();
let result = isotropic_remesh(&mesh, 0.5, 2);
assert!(
!result.indices.is_empty(),
"remeshed mesh should have faces"
);
assert!(
!result.vertices.is_empty(),
"remeshed mesh should have vertices"
);
}
#[test]
fn test_uniform_remesher() {
let mesh = simple_quad_mesh();
let remesher = UniformRemesher::new(0.4);
let result = remesher.remesh(&mesh, 1);
assert!(!result.vertices.is_empty());
}
#[test]
fn test_loop_subdivision_twice() {
let mesh = simple_quad_mesh();
let sub1 = LoopSubdivision::subdivide(&mesh);
let sub2 = LoopSubdivision::subdivide(&sub1);
assert_eq!(sub2.indices.len(), mesh.indices.len() * 16);
}
#[test]
fn test_closest_point_on_triangle() {
let a = [0.0_f64, 0.0, 0.0];
let b = [1.0, 0.0, 0.0];
let c = [0.0, 1.0, 0.0];
let p = [0.25, 0.25, 1.0];
let cp = closest_point_on_triangle(p, a, b, c);
assert!((cp[0] - 0.25).abs() < 1e-10);
assert!((cp[1] - 0.25).abs() < 1e-10);
assert!(cp[2].abs() < 1e-10);
}
#[test]
fn test_feature_preserving_remesh_returns_mesh() {
let mesh = simple_quad_mesh();
let result = feature_preserving_remesh(&mesh, 0.4, 1, 1.0_f64.to_radians());
assert!(!result.vertices.is_empty());
assert!(!result.indices.is_empty());
}
#[test]
fn test_feature_preserving_remesh_no_degenerate_faces() {
let mesh = simple_quad_mesh();
let result = feature_preserving_remesh(&mesh, 0.3, 2, 45.0_f64.to_radians());
let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
for tri in &result.indices {
let a = verts[tri[0]];
let b = verts[tri[1]];
let c = verts[tri[2]];
let e1 = sub3(b, a);
let e2 = sub3(c, a);
let cross = cross3(e1, e2);
let area = len3(cross) * 0.5;
assert!(area >= 0.0, "negative area: {}", area);
}
}
#[test]
fn test_edge_flip_preserves_vertex_count() {
let mesh = simple_quad_mesh();
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = mesh.indices.clone();
let n_verts_before = verts.len();
flip_edges_for_quality(&mut tris, &verts);
assert_eq!(verts.len(), n_verts_before);
let _ = verts; }
#[test]
fn test_edge_flip_preserves_face_count() {
let mesh = simple_quad_mesh();
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = mesh.indices.clone();
let n_tris_before = tris.len();
flip_edges_for_quality(&mut tris, &verts);
assert_eq!(tris.len(), n_tris_before);
}
#[test]
fn test_mesh_quality_equilateral() {
let s = 1.0_f64;
let h = s * (3.0_f64.sqrt()) / 2.0;
let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
let tris = vec![[0usize, 1, 2]];
let quality = mesh_quality_min_angle(&verts, &tris);
assert!(quality > 0.9, "equilateral triangle quality={}", quality);
}
#[test]
fn test_mesh_quality_degenerate() {
let verts = vec![
[0.0_f64, 0.0, 0.0],
[1.0, 0.0, 0.0],
[100.0, 0.0, 0.0], ];
let tris = vec![[0usize, 1, 2]];
let quality = mesh_quality_min_angle(&verts, &tris);
assert!(
quality < 0.5,
"degenerate triangle quality should be low, got {}",
quality
);
}
#[test]
fn test_tangent_smooth_returns_mesh() {
let mesh = simple_quad_mesh();
let result = tangent_laplacian_smooth(&mesh, 3);
assert_eq!(result.vertices.len(), mesh.vertices.len());
assert_eq!(result.indices.len(), mesh.indices.len());
}
#[test]
fn test_catmull_clark_double_subdivision() {
let verts: Vec<[f64; 3]> = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let quads: Vec<[usize; 4]> = vec![[0, 1, 2, 3]];
let (v1, q1) = CatmullClark::subdivide_quad_mesh(&verts, &quads);
let (_, q2) = CatmullClark::subdivide_quad_mesh(&v1, &q1);
assert_eq!(q2.len(), quads.len() * 16); }
#[test]
fn test_laplacian_smooth_moves_vertices() {
let mesh = simple_quad_mesh();
let result = laplacian_smooth(&mesh, 3, 0.5);
let orig_sum: f64 = mesh.vertices.iter().map(|v| v.norm()).sum();
let new_sum: f64 = result.vertices.iter().map(|v| v.norm()).sum();
let _ = (orig_sum, new_sum);
assert_eq!(result.vertices.len(), mesh.vertices.len());
}
#[test]
fn test_laplacian_smooth_zero_iterations_unchanged() {
let mesh = simple_quad_mesh();
let result = laplacian_smooth(&mesh, 0, 0.5);
for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
assert!((a.x - b.x).abs() < 1e-12);
assert!((a.y - b.y).abs() < 1e-12);
assert!((a.z - b.z).abs() < 1e-12);
}
}
#[test]
fn test_laplacian_smooth_preserves_face_count() {
let mesh = simple_quad_mesh();
let result = laplacian_smooth(&mesh, 5, 0.3);
assert_eq!(result.indices.len(), mesh.indices.len());
}
#[test]
fn test_delaunay_flip_preserves_vertices() {
let mesh = simple_quad_mesh();
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = mesh.indices.clone();
let before = verts.len();
delaunay_edge_flip(&mut tris, &verts);
assert_eq!(verts.len(), before);
}
#[test]
fn test_delaunay_flip_preserves_face_count() {
let mesh = simple_quad_mesh();
let verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = mesh.indices.clone();
let before = tris.len();
delaunay_edge_flip(&mut tris, &verts);
assert_eq!(tris.len(), before);
}
#[test]
fn test_coarsen_reduces_vertex_count() {
let mesh = simple_quad_mesh();
let result = coarsen_mesh(&mesh, 0.8); assert!(result.vertices.len() <= mesh.vertices.len() + 1); }
#[test]
fn test_coarsen_no_degenerate_faces() {
let mesh = simple_quad_mesh();
let result = coarsen_mesh(&mesh, 0.5);
let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
for tri in &result.indices {
assert!(
tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2],
"degenerate face {:?}",
tri
);
assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
}
}
#[test]
fn test_adaptive_refine_increases_faces_for_curved_mesh() {
let mesh = simple_quad_mesh();
let result = adaptive_refine_by_curvature(&mesh, 0.01); assert!(result.indices.len() >= mesh.indices.len());
}
#[test]
fn test_adaptive_refine_high_threshold_no_change() {
let mesh = simple_quad_mesh();
let result = adaptive_refine_by_curvature(&mesh, 1000.0); assert_eq!(result.indices.len(), mesh.indices.len());
}
#[test]
fn test_vertex_clustering_reduces_count() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
let coarse = vertex_clustering_decimate(&sub, 2); assert!(
coarse.vertices.len() < sub.vertices.len(),
"decimation should reduce vertex count"
);
}
#[test]
fn test_vertex_clustering_single_cell_merges_all() {
let mesh = simple_quad_mesh();
let result = vertex_clustering_decimate(&mesh, 1); assert!(result.vertices.len() <= mesh.vertices.len());
}
#[test]
fn test_saliency_remesh_returns_valid_mesh() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
let saliency = vec![0.5_f64; sub.vertices.len()];
let result = saliency_weighted_remesh(&sub, &saliency, 1);
assert!(!result.vertices.is_empty());
let _ = result.indices.len();
}
#[test]
fn test_saliency_remesh_preserves_approximate_vertex_count() {
let mesh = simple_quad_mesh();
let saliency = vec![0.5_f64; mesh.vertices.len()];
let result = saliency_weighted_remesh(&mesh, &saliency, 1);
assert!(result.vertices.len() <= mesh.vertices.len() * 10);
}
#[test]
fn test_mesh_quality_after_laplacian_smooth() {
let mesh = simple_quad_mesh();
let smoothed = laplacian_smooth(&mesh, 5, 0.5);
let verts: Vec<[f64; 3]> = smoothed.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let q = mesh_quality_min_angle(&verts, &smoothed.indices);
assert!((0.0..=1.0 + 1e-6).contains(&q), "quality out of range: {q}");
}
#[test]
fn test_aspect_ratio_equilateral() {
let s = 1.0_f64;
let h = s * 3.0_f64.sqrt() / 2.0;
let verts = vec![[0.0_f64, 0.0, 0.0], [s, 0.0, 0.0], [s / 2.0, h, 0.0]];
let tris = vec![[0usize, 1, 2]];
let ar = mesh_aspect_ratio_avg(&verts, &tris);
assert!(
(ar - 3.0_f64.sqrt()).abs() < 0.01,
"equilateral ar should be sqrt(3)≈1.732, got {ar}"
);
}
#[test]
fn test_collapse_edge_reduces_vertex_usage() {
let mesh = simple_quad_mesh();
let mut verts: Vec<[f64; 3]> = mesh.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = mesh.indices.clone();
let before_faces = tris.len();
collapse_edge(&mut verts, &mut tris, 0, 1);
assert!(tris.len() <= before_faces);
}
#[test]
fn test_loop_subdivision_vertex_positions_reasonable() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
for v in &sub.vertices {
assert!(v.x.is_finite() && v.y.is_finite() && v.z.is_finite());
}
}
#[test]
fn test_isotropic_remesh_zero_iterations_same() {
let mesh = simple_quad_mesh();
let result = isotropic_remesh(&mesh, 0.5, 0);
assert_eq!(result.vertices.len(), mesh.vertices.len());
assert_eq!(result.indices.len(), mesh.indices.len());
}
#[test]
fn test_delaunay_flip_on_subdivided_mesh() {
let mesh = simple_quad_mesh();
let sub = LoopSubdivision::subdivide(&mesh);
let verts: Vec<[f64; 3]> = sub.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
let mut tris = sub.indices.clone();
delaunay_edge_flip(&mut tris, &verts);
for tri in &tris {
assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
}
}
#[test]
fn test_coarsen_mesh_returns_valid_faces() {
let mesh = simple_quad_mesh();
let result = coarsen_mesh(&mesh, 1.5);
let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
for tri in &result.indices {
assert!(
tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len(),
"face {:?} has out-of-bounds index (n_verts={})",
tri,
verts.len()
);
}
}
#[test]
fn test_adaptive_refine_no_degenerate_faces() {
let mesh = simple_quad_mesh();
let result = adaptive_refine_by_curvature(&mesh, 0.1);
let verts: Vec<[f64; 3]> = result.vertices.iter().map(|v| vec3_to_arr(*v)).collect();
for tri in &result.indices {
assert!(tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
assert!(tri[0] < verts.len() && tri[1] < verts.len() && tri[2] < verts.len());
}
}
}