use crate::linalg::Vec3;
use crate::manifold::Manifold;
use crate::types::{BooleanEngine, Error, MeshGL};
fn spiky_dodecahedron(spike_height: f64) -> Manifold {
let phi: f64 = (1.0 + 5.0_f64.sqrt()) / 2.0;
let inv_phi = 1.0 / phi;
let scale = 0.5;
let raw_verts: [(f64, f64, f64); 20] = [
(1.0, 1.0, 1.0), (1.0, 1.0, -1.0), (1.0, -1.0, 1.0), (1.0, -1.0, -1.0),
(-1.0, 1.0, 1.0), (-1.0, 1.0, -1.0), (-1.0, -1.0, 1.0), (-1.0, -1.0, -1.0),
(0.0, inv_phi, phi), (0.0, inv_phi, -phi), (0.0, -inv_phi, phi), (0.0, -inv_phi, -phi),
(inv_phi, phi, 0.0), (-inv_phi, phi, 0.0), (inv_phi, -phi, 0.0), (-inv_phi, -phi, 0.0),
(phi, 0.0, inv_phi), (phi, 0.0, -inv_phi), (-phi, 0.0, inv_phi), (-phi, 0.0, -inv_phi),
];
let faces: [[usize; 5]; 12] = [
[0, 8, 10, 2, 16], [0, 16, 17, 1, 12], [0, 12, 13, 4, 8],
[1, 17, 3, 11, 9], [1, 9, 5, 13, 12], [2, 10, 6, 15, 14],
[2, 14, 3, 17, 16], [4, 13, 5, 19, 18], [4, 18, 6, 10, 8],
[5, 9, 11, 7, 19], [6, 18, 19, 7, 15], [3, 14, 15, 7, 11],
];
let verts: Vec<(f64, f64, f64)> =
raw_verts.iter().map(|&(x, y, z)| (x * scale, y * scale, z * scale)).collect();
let mut positions: Vec<f32> = Vec::with_capacity(32 * 3);
let mut tri_verts: Vec<u32> = Vec::with_capacity(60 * 3);
for &(x, y, z) in &verts {
positions.extend([x as f32, y as f32, z as f32]);
}
for face in &faces {
let cx: f64 = face.iter().map(|&i| verts[i].0).sum::<f64>() / 5.0;
let cy: f64 = face.iter().map(|&i| verts[i].1).sum::<f64>() / 5.0;
let cz: f64 = face.iter().map(|&i| verts[i].2).sum::<f64>() / 5.0;
let len = (cx * cx + cy * cy + cz * cz).sqrt();
let (nx, ny, nz) = (cx / len, cy / len, cz / len);
let spike_idx = (positions.len() / 3) as u32;
positions.extend([
(cx + nx * spike_height) as f32,
(cy + ny * spike_height) as f32,
(cz + nz * spike_height) as f32,
]);
for j in 0..5 {
tri_verts.extend([spike_idx, face[j] as u32, face[(j + 1) % 5] as u32]);
}
}
let mut mesh = MeshGL::default();
mesh.num_prop = 3;
mesh.vert_properties = positions;
mesh.tri_verts = tri_verts;
Manifold::from_mesh_gl(&mesh)
}
fn color(m: &Manifold, rgba: [f64; 4]) -> Manifold {
m.set_properties(4, move |new_prop, _pos, _old| {
new_prop.copy_from_slice(&rgba);
})
}
const BLUE: [f64; 4] = [0.27, 0.53, 0.80, 1.0];
const RED: [f64; 4] = [0.85, 0.25, 0.25, 0.6];
fn colored_spiky_pair() -> (Manifold, Manifold) {
let a = color(&spiky_dodecahedron(0.4), BLUE);
let b = color(&spiky_dodecahedron(0.4), RED)
.rotate(30.0, 45.0, 60.0)
.translate(Vec3::new(0.3, 0.0, 0.0));
(a, b)
}
fn position_set(gl: &MeshGL) -> Vec<[f64; 3]> {
let np = gl.num_prop as usize;
let n = gl.vert_properties.len() / np;
let mut out: Vec<[f64; 3]> = (0..n)
.map(|i| {
[
gl.vert_properties[i * np] as f64,
gl.vert_properties[i * np + 1] as f64,
gl.vert_properties[i * np + 2] as f64,
]
})
.collect();
out.sort_by(|a, b| a.partial_cmp(b).unwrap());
out.dedup();
out
}
#[test]
fn constant_colors_survive_robust_union() {
let (a, b) = colored_spiky_pair();
let out = a.union_with_engine(&b, BooleanEngine::Robust);
assert_eq!(out.status(), Error::NoError);
assert_eq!(
out.as_impl().num_prop, 4,
"robust output must keep the 4 color channels"
);
let gl = out.get_mesh_gl(-1);
assert_eq!(gl.num_prop, 7, "xyz + rgba");
let n = gl.vert_properties.len() / 7;
let (mut blue, mut red) = (0usize, 0usize);
for i in 0..n {
let c: Vec<f64> = (3..7).map(|k| gl.vert_properties[i * 7 + k] as f64).collect();
let is = |rgba: [f64; 4]| (0..4).all(|k| (c[k] - rgba[k]).abs() < 1e-6);
assert!(
is(BLUE) || is(RED),
"vertex {i} color {c:?} is neither operand color"
);
if is(BLUE) { blue += 1 } else { red += 1 }
}
assert!(blue > 0 && red > 0, "both operands must contribute vertices");
}
#[test]
fn robust_matches_exact_vertices_and_colors() {
let (a, b) = colored_spiky_pair();
let exact = a.union_with_engine(&b, BooleanEngine::Exact);
let robust = a.union_with_engine(&b, BooleanEngine::Robust);
assert_eq!(robust.status(), Error::NoError);
let ge = exact.get_mesh_gl(-1);
let gr = robust.get_mesh_gl(-1);
let pe = position_set(&ge);
let pr = position_set(&gr);
assert_eq!(
pe.len(),
pr.len(),
"engines must agree on the distinct output vertex count"
);
let close = |x: &[f64; 3], y: &[f64; 3]| {
(0..3).all(|k| (x[k] - y[k]).abs() <= 1e-6 * x[k].abs().max(1.0))
};
for (x, y) in pe.iter().zip(pr.iter()) {
assert!(close(x, y), "position mismatch: {x:?} vs {y:?}");
}
let np = 7usize;
let key = |p: [f32; 3]| (p[0].to_bits(), p[1].to_bits(), p[2].to_bits());
let mut exact_colors: std::collections::BTreeMap<_, Vec<[f32; 4]>> = Default::default();
for i in 0..ge.vert_properties.len() / np {
let p = [ge.vert_properties[i * np], ge.vert_properties[i * np + 1], ge.vert_properties[i * np + 2]];
let c = [
ge.vert_properties[i * np + 3],
ge.vert_properties[i * np + 4],
ge.vert_properties[i * np + 5],
ge.vert_properties[i * np + 6],
];
exact_colors.entry(key(p)).or_default().push(c);
}
for i in 0..gr.vert_properties.len() / np {
let p = [gr.vert_properties[i * np], gr.vert_properties[i * np + 1], gr.vert_properties[i * np + 2]];
let c = [
gr.vert_properties[i * np + 3],
gr.vert_properties[i * np + 4],
gr.vert_properties[i * np + 5],
gr.vert_properties[i * np + 6],
];
let Some(cands) = exact_colors.get(&key(p)) else {
panic!("robust vertex {p:?} not among exact vertices");
};
assert!(
cands.iter().any(|e| (0..4).all(|k| (e[k] - c[k]).abs() < 1e-6)),
"color mismatch at {p:?}: robust {c:?} vs exact {cands:?}"
);
}
}
#[test]
fn interpolated_properties_match_exact() {
let prop_fn = |m: &Manifold| {
m.set_properties(1, |new_prop, pos, _old| {
new_prop[0] = 0.25 + pos.x * 0.5 + pos.y * 0.125 - pos.z * 0.0625;
})
};
let a = prop_fn(&Manifold::cube(Vec3::new(2.0, 2.0, 2.0), true));
let b = prop_fn(&Manifold::sphere(1.3, 8).translate(Vec3::new(0.4, 0.2, 0.1)));
let exact = a.difference_with_engine(&b, BooleanEngine::Exact);
let robust = a.difference_with_engine(&b, BooleanEngine::Robust);
assert_eq!(robust.status(), Error::NoError);
assert_eq!(robust.as_impl().num_prop, 1);
let np = 4usize; let gr = robust.get_mesh_gl(-1);
assert_eq!(gr.num_prop as usize, np);
for i in 0..gr.vert_properties.len() / np {
let (x, y, z) = (
gr.vert_properties[i * np] as f64,
gr.vert_properties[i * np + 1] as f64,
gr.vert_properties[i * np + 2] as f64,
);
let expect = 0.25 + x * 0.5 + y * 0.125 - z * 0.0625;
let got = gr.vert_properties[i * np + 3] as f64;
assert!(
(got - expect).abs() < 1e-5,
"vertex {i} ({x},{y},{z}): prop {got} vs affine {expect}"
);
}
let _ = exact;
}