use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct QuadrangulateConfig {
pub max_planarity_error: f32,
pub prefer_longest_edge: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuadFace {
pub indices: [u32; 4],
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct QuadrangulateResult {
pub quads: Vec<QuadFace>,
pub remaining_tris: Vec<[u32; 3]>,
}
#[allow(dead_code)]
pub fn default_quadrangulate_config() -> QuadrangulateConfig {
QuadrangulateConfig {
max_planarity_error: 0.01,
prefer_longest_edge: true,
}
}
#[allow(dead_code)]
pub fn quadrangulate_mesh(
verts: &[[f32; 3]],
faces: &[[u32; 3]],
cfg: &QuadrangulateConfig,
) -> QuadrangulateResult {
let mut edge_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
for (fi, face) in faces.iter().enumerate() {
let [a, b, c] = *face;
for (p, q) in [(a, b), (b, c), (c, a)] {
edge_faces.entry(edge_key(p, q)).or_default().push(fi);
}
}
let mut used = vec![false; faces.len()];
let mut quads: Vec<QuadFace> = Vec::new();
let mut remaining_tris: Vec<[u32; 3]> = Vec::new();
let mut candidates: Vec<(f32, usize, usize, [u32; 4])> = Vec::new();
for (&(ea, eb), adj_faces) in &edge_faces {
if adj_faces.len() != 2 {
continue;
}
let fi = adj_faces[0];
let fj = adj_faces[1];
let opp_i = opposite_vertex(&faces[fi], ea, eb);
let opp_j = opposite_vertex(&faces[fj], ea, eb);
let quad_vs = [opp_i, ea, opp_j, eb];
let v0 = verts[quad_vs[0] as usize];
let v1 = verts[quad_vs[1] as usize];
let v2 = verts[quad_vs[2] as usize];
let v3 = verts[quad_vs[3] as usize];
let arr = [v0, v1, v2, v3];
let err = quad_planarity_error(&arr);
if err > cfg.max_planarity_error {
continue;
}
let score = if cfg.prefer_longest_edge {
let va = verts[ea as usize];
let vb = verts[eb as usize];
edge_length(va, vb)
} else {
0.0
};
candidates.push((score, fi, fj, quad_vs));
}
candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
for (_, fi, fj, quad_vs) in candidates {
if used[fi] || used[fj] {
continue;
}
used[fi] = true;
used[fj] = true;
quads.push(QuadFace { indices: quad_vs });
}
for (fi, face) in faces.iter().enumerate() {
if !used[fi] {
remaining_tris.push(*face);
}
}
QuadrangulateResult { quads, remaining_tris }
}
#[allow(dead_code)]
pub fn quad_count(result: &QuadrangulateResult) -> usize {
result.quads.len()
}
#[allow(dead_code)]
pub fn remaining_tri_count(result: &QuadrangulateResult) -> usize {
result.remaining_tris.len()
}
#[allow(dead_code)]
pub fn quad_to_tris(quad: &QuadFace) -> [[u32; 3]; 2] {
let [a, b, c, d] = quad.indices;
[[a, b, c], [a, c, d]]
}
#[allow(dead_code)]
pub fn is_planar_quad(v: &[[f32; 3]; 4]) -> bool {
quad_planarity_error(v) < 1e-4
}
#[allow(dead_code)]
pub fn quad_planarity_error(v: &[[f32; 3]; 4]) -> f32 {
let v0 = v[0];
let v1 = v[1];
let v2 = v[2];
let v3 = v[3];
let ab = vsub(v1, v0);
let ac = vsub(v2, v0);
let n = cross(ab, ac);
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len < 1e-10 {
return 0.0;
}
let n_unit = [n[0] / len, n[1] / len, n[2] / len];
let d3 = vsub(v3, v0);
(d3[0] * n_unit[0] + d3[1] * n_unit[1] + d3[2] * n_unit[2]).abs()
}
#[allow(dead_code)]
pub fn quads_to_triangle_list(quads: &[QuadFace]) -> Vec<[u32; 3]> {
let mut tris = Vec::with_capacity(quads.len() * 2);
for q in quads {
let [a, b, c, d] = q.indices;
tris.push([a, b, c]);
tris.push([a, c, d]);
}
tris
}
fn edge_key(a: u32, b: u32) -> (u32, u32) {
if a < b { (a, b) } else { (b, a) }
}
fn opposite_vertex(face: &[u32; 3], ea: u32, eb: u32) -> u32 {
for &v in face {
if v != ea && v != eb {
return v;
}
}
face[0]
}
fn vsub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 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],
]
}
fn edge_length(a: [f32; 3], b: [f32; 3]) -> f32 {
let d = vsub(b, a);
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
fn two_tris_square() -> (Vec<[f32; 3]>, Vec<[u32; 3]>) {
let verts = 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 faces = vec![[0, 1, 2], [0, 2, 3]];
(verts, faces)
}
#[test]
fn test_default_config() {
let cfg = default_quadrangulate_config();
assert!(cfg.max_planarity_error > 0.0);
}
#[test]
fn test_quadrangulate_planar_square() {
let (v, f) = two_tris_square();
let cfg = default_quadrangulate_config();
let result = quadrangulate_mesh(&v, &f, &cfg);
assert_eq!(quad_count(&result), 1);
assert_eq!(remaining_tri_count(&result), 0);
}
#[test]
fn test_quad_to_tris() {
let q = QuadFace { indices: [0, 1, 2, 3] };
let [t0, t1] = quad_to_tris(&q);
assert_eq!(t0, [0, 1, 2]);
assert_eq!(t1, [0, 2, 3]);
}
#[test]
fn test_quads_to_triangle_list() {
let q = QuadFace { indices: [0, 1, 2, 3] };
let tris = quads_to_triangle_list(&[q]);
assert_eq!(tris.len(), 2);
}
#[test]
fn test_is_planar_quad_flat() {
let v = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]];
assert!(is_planar_quad(&v));
}
#[test]
fn test_is_planar_quad_non_planar() {
let v = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0]];
assert!(!is_planar_quad(&v));
}
#[test]
fn test_planarity_error_zero_for_flat() {
let v = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [2.0, 2.0, 0.0], [0.0, 2.0, 0.0]];
assert!(quad_planarity_error(&v) < 1e-6);
}
#[test]
fn test_isolated_triangle_not_paired() {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[2.5, 1.0, 0.0],
];
let faces = vec![[0, 1, 2], [3, 4, 5]];
let cfg = default_quadrangulate_config();
let result = quadrangulate_mesh(&verts, &faces, &cfg);
assert_eq!(quad_count(&result), 0);
assert_eq!(remaining_tri_count(&result), 2);
}
#[test]
fn test_non_planar_rejected() {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 5.0], ];
let faces = vec![[0, 1, 2], [0, 2, 3]];
let cfg = QuadrangulateConfig { max_planarity_error: 0.001, prefer_longest_edge: false };
let result = quadrangulate_mesh(&verts, &faces, &cfg);
assert_eq!(quad_count(&result), 0);
assert_eq!(remaining_tri_count(&result), 2);
}
}