use crate::Mesh;
use glam::{Vec3, Vec4};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum Symmetry {
#[default]
Off,
Auto,
Plane(Vec4),
}
const MATCH_FRACTION: f32 = 0.95;
struct PointGrid {
cell: f32,
map: HashMap<[i64; 3], Vec<u32>>,
}
impl PointGrid {
fn new(points: impl Iterator<Item = (u32, Vec3)>, cell: f32) -> PointGrid {
let mut map: HashMap<[i64; 3], Vec<u32>> = HashMap::new();
for (i, p) in points {
map.entry(Self::key(p, cell)).or_default().push(i);
}
PointGrid { cell, map }
}
fn key(p: Vec3, cell: f32) -> [i64; 3] {
[
(p.x / cell).floor() as i64,
(p.y / cell).floor() as i64,
(p.z / cell).floor() as i64,
]
}
fn find(&self, p: Vec3, tol: f32, pts: &[Vec3]) -> Option<u32> {
let k = Self::key(p, self.cell);
let mut best: Option<(f32, u32)> = None;
for dz in -1..=1 {
for dy in -1..=1 {
for dx in -1..=1 {
if let Some(list) = self.map.get(&[k[0] + dx, k[1] + dy, k[2] + dz]) {
for &i in list {
let d = (pts[i as usize] - p).length();
if d <= tol && best.is_none_or(|b| d < b.0) {
best = Some((d, i));
}
}
}
}
}
}
best.map(|b| b.1)
}
}
fn reflect(p: Vec3, plane: Vec4) -> Vec3 {
let n = plane.truncate();
p - n * 2.0 * (n.dot(p) + plane.w)
}
fn match_fraction(points: &[Vec3], plane: Vec4, tol: f32) -> f32 {
let grid = PointGrid::new(points.iter().enumerate().map(|(i, p)| (i as u32, *p)), tol);
let matched = points
.iter()
.filter(|p| grid.find(reflect(**p, plane), tol, points).is_some())
.count();
matched as f32 / points.len().max(1) as f32
}
fn unique_positions(mesh: &Mesh) -> Vec<Vec3> {
let mut seen = std::collections::HashSet::new();
mesh.positions
.iter()
.filter(|p| seen.insert([p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]))
.copied()
.collect()
}
pub fn detect_symmetry(mesh: &Mesh) -> Option<(Vec4, f32)> {
let pts = unique_positions(mesh);
if pts.len() < 4 {
return None;
}
let b = mesh.bounds();
let tol = (b.diagonal() * 1e-4).max(1e-7);
let c = b.center();
[Vec3::X, Vec3::Y, Vec3::Z]
.into_iter()
.map(|n| {
let plane = n.extend(-n.dot(c));
(plane, match_fraction(&pts, plane, tol))
})
.filter(|&(_, f)| f >= MATCH_FRACTION)
.max_by(|a, b| a.1.total_cmp(&b.1))
}
pub(crate) fn resolve_plane(mesh: &Mesh, s: Symmetry) -> Option<Vec4> {
match s {
Symmetry::Off => None,
Symmetry::Auto => detect_symmetry(mesh).map(|p| p.0),
Symmetry::Plane(p) => Some(p.truncate().normalize().extend(p.w / p.truncate().length())),
}
}
pub(crate) fn mirror_map(
positions: &[Vec3],
vpos: &[u32],
np: usize,
plane: Vec4,
diag: f32,
) -> Vec<u32> {
let tol = (diag * 1e-4).max(1e-7);
let mut pts = vec![Vec3::ZERO; np];
for (v, &p) in vpos.iter().enumerate() {
pts[p as usize] = positions[v];
}
let grid = PointGrid::new(pts.iter().enumerate().map(|(i, p)| (i as u32, *p)), tol);
(0..np)
.map(|i| {
let p = pts[i];
if (plane.truncate().dot(p) + plane.w).abs() <= tol {
return i as u32;
}
grid.find(reflect(p, plane), tol, &pts).unwrap_or(u32::MAX)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reduce::{simplify, SimplifyOptions};
fn symmetric_patch(n: u32) -> Mesh {
let mut pos = Vec::new();
for y in 0..=n {
for x in 0..=n {
let fx = x as f32 / n as f32 * 2.0 - 1.0;
let fz = y as f32 / n as f32 * 2.0 - 1.0;
let h = 0.3 * (3.0 * fx).cos() * (2.0 * fz).sin() + 0.2 * fx * fx;
pos.push(Vec3::new(fx, h, fz));
}
}
let mut idx = Vec::new();
for y in 0..n {
for x in 0..n {
let a = y * (n + 1) + x;
idx.extend_from_slice(&[a, a + n + 1, a + 1, a + 1, a + n + 1, a + n + 2]);
}
}
Mesh::new(pos, idx)
}
#[test]
fn detects_x_plane() {
let m = symmetric_patch(20);
let (plane, f) = detect_symmetry(&m).expect("symmetric");
assert!(
(plane.truncate() - Vec3::X).length() < 1e-6 && plane.w.abs() < 1e-5,
"{plane:?}"
);
assert!(f > 0.99);
}
#[test]
fn symmetric_reduction_stays_symmetric() {
let m = symmetric_patch(24);
let r = simplify(
&m,
&SimplifyOptions {
target_ratio: Some(0.2),
symmetry: Symmetry::Auto,
..Default::default()
},
)
.unwrap();
assert!(r.symmetry_plane.is_some());
let pts = unique_positions(&r.mesh);
let frac = match_fraction(&pts, Vec4::new(1.0, 0.0, 0.0, 0.0), 1e-4);
assert!(
frac > 0.97,
"only {frac} of output positions have a mirror partner"
);
let plain = simplify(&m, &SimplifyOptions::ratio(0.2)).unwrap();
let pf = match_fraction(
&unique_positions(&plain.mesh),
Vec4::new(1.0, 0.0, 0.0, 0.0),
1e-4,
);
assert!(frac > pf, "symmetric {frac} vs plain {pf}");
}
#[test]
fn asymmetric_mesh_has_no_plane() {
let mut m = symmetric_patch(10);
for (i, p) in m.positions.iter_mut().enumerate() {
p.y += (i as f32 * 0.37).sin() * 0.1;
}
assert!(detect_symmetry(&m).is_none());
}
}