use super::arrangement::{boolean, difference_all, union_all, BoolOp, Tri};
use crate::mesh::Mesh;
pub(crate) const SNAP_GRID: f64 = 1.0 / 65536.0;
#[inline]
fn snap(c: f64) -> f64 {
(c / SNAP_GRID).round() * SNAP_GRID
}
pub fn mesh_to_tris(m: &Mesh) -> Vec<Tri> {
let vertex = |i: u32| -> Option<[f64; 3]> {
let b = (i as usize) * 3;
let c = [
*m.positions.get(b)? as f64,
*m.positions.get(b + 1)? as f64,
*m.positions.get(b + 2)? as f64,
];
if !c.iter().all(|v| v.is_finite()) {
return None;
}
Some([snap(c[0]), snap(c[1]), snap(c[2])])
};
m.indices
.chunks_exact(3)
.filter_map(|c| Some([vertex(c[0])?, vertex(c[1])?, vertex(c[2])?]))
.collect()
}
fn face_normal(t: &Tri) -> [f32; 3] {
let e1 = [t[1][0] - t[0][0], t[1][1] - t[0][1], t[1][2] - t[0][2]];
let e2 = [t[2][0] - t[0][0], t[2][1] - t[0][1], t[2][2] - t[0][2]];
let n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 0.0 {
[(n[0] / len) as f32, (n[1] / len) as f32, (n[2] / len) as f32]
} else {
[0.0, 0.0, 1.0]
}
}
pub fn tris_to_mesh(tris: &[Tri]) -> Mesh {
let mut m = Mesh::with_capacity(tris.len() * 3, tris.len() * 3);
for t in tris {
let n = face_normal(t);
let base = (m.positions.len() / 3) as u32;
for p in t {
m.positions
.extend_from_slice(&[p[0] as f32, p[1] as f32, p[2] as f32]);
m.normals.extend_from_slice(&n);
}
m.indices.extend_from_slice(&[base, base + 1, base + 2]);
}
m
}
fn signed_volume6(tris: &[Tri]) -> f64 {
let mut lo = [f64::MAX; 3];
let mut hi = [f64::MIN; 3];
for t in tris {
for v in t {
for k in 0..3 {
lo[k] = lo[k].min(v[k]);
hi[k] = hi[k].max(v[k]);
}
}
}
if tris.is_empty() {
return 0.0;
}
let o = [
(lo[0] + hi[0]) * 0.5,
(lo[1] + hi[1]) * 0.5,
(lo[2] + hi[2]) * 0.5,
];
tris.iter()
.map(|t| {
let a = [t[0][0] - o[0], t[0][1] - o[1], t[0][2] - o[2]];
let b = [t[1][0] - o[0], t[1][1] - o[1], t[1][2] - o[2]];
let c = [t[2][0] - o[0], t[2][1] - o[1], t[2][2] - o[2]];
let cr = [
b[1] * c[2] - b[2] * c[1],
b[2] * c[0] - b[0] * c[2],
b[0] * c[1] - b[1] * c[0],
];
a[0] * cr[0] + a[1] * cr[1] + a[2] * cr[2]
})
.sum()
}
fn orient_outward(mut tris: Vec<Tri>) -> Vec<Tri> {
if signed_volume6(&tris) < 0.0 {
for t in &mut tris {
t.swap(1, 2);
}
}
tris
}
fn promote_cutter_verts_onto_host_faces(cutter: &mut [Tri], host: &[Tri]) {
if cutter.is_empty() || host.is_empty() {
return;
}
let mut extent = 1.0f64;
for t in cutter.iter().chain(host.iter()) {
for v in t {
for &x in v {
extent = extent.max(x.abs());
}
}
}
let band = (8.0 * SNAP_GRID).max(extent * (1.0 / 4_194_304.0));
let band2 = band * band;
struct Face {
t0: [f64; 3],
t1: [f64; 3],
t2: [f64; 3],
n: [f64; 3], nn: f64, }
let faces: Vec<Face> = host
.iter()
.filter_map(|t| {
let e1 = [t[1][0] - t[0][0], t[1][1] - t[0][1], t[1][2] - t[0][2]];
let e2 = [t[2][0] - t[0][0], t[2][1] - t[0][1], t[2][2] - t[0][2]];
let n = [
e1[1] * e2[2] - e1[2] * e2[1],
e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0],
];
let nn = n[0] * n[0] + n[1] * n[1] + n[2] * n[2];
if nn <= 0.0 || !nn.is_finite() {
return None; }
Some(Face { t0: t[0], t1: t[1], t2: t[2], n, nn })
})
.collect();
for t in cutter.iter_mut() {
for v in t.iter_mut() {
let mut best: Option<(f64, &Face)> = None; for f in &faces {
let d = (v[0] - f.t0[0]) * f.n[0]
+ (v[1] - f.t0[1]) * f.n[1]
+ (v[2] - f.t0[2]) * f.n[2];
if d == 0.0 {
continue; }
let d2 = (d * d) / f.nn;
if d2 > band2 {
continue; }
if let Some((bd2, _)) = best {
if d2 >= bd2 {
continue;
}
}
best = Some((d2, f));
}
if let Some((_, f)) = best {
if let Some(w) = exact_on_plane_weld(*v, f.t0, f.t1, f.t2) {
*v = w;
}
}
}
}
}
fn exact_on_plane_weld(v: [f64; 3], t0: [f64; 3], t1: [f64; 3], t2: [f64; 3]) -> Option<[f64; 3]> {
const Q: f64 = 1_048_576.0; const S16: f64 = 65_536.0; const S36: f64 = 68_719_476_736.0; let u = [t1[0] - t0[0], t1[1] - t0[1], t1[2] - t0[2]];
let w = [t2[0] - t0[0], t2[1] - t0[1], t2[2] - t0[2]];
let p = [v[0] - t0[0], v[1] - t0[1], v[2] - t0[2]];
let dot = |a: &[f64; 3], b: &[f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let (uu, ww, uw) = (dot(&u, &u), dot(&w, &w), dot(&u, &w));
let (pu, pw) = (dot(&p, &u), dot(&p, &w));
let det = uu * ww - uw * uw;
if det == 0.0 || !det.is_finite() {
return None; }
let alpha = ((ww * pu - uw * pw) / det * Q).round();
let beta = ((uu * pw - uw * pu) / det * Q).round();
if !alpha.is_finite() || !beta.is_finite() || alpha.abs() > 8.0 * Q || beta.abs() > 8.0 * Q {
return None;
}
let (ai, bi) = (alpha as i128, beta as i128);
let mut out = [0.0f64; 3];
for k in 0..3 {
let (s0, s1, s2) = (t0[k] * S16, t1[k] * S16, t2[k] * S16);
for s in [s0, s1, s2] {
if s.fract() != 0.0 || s.abs() >= 9.0e18 {
return None;
}
}
let (i0, i1, i2) = (s0 as i128, s1 as i128, s2 as i128);
let r36 = (i0 << 20) + ai * (i1 - i0) + bi * (i2 - i0);
let rf = r36 as f64;
if rf as i128 != r36 {
return None; }
out[k] = rf / S36; }
Some(out)
}
pub fn subtract(host: &Mesh, cutter: &Mesh) -> Mesh {
let h = orient_outward(mesh_to_tris(host));
let mut c = mesh_to_tris(cutter);
promote_cutter_verts_onto_host_faces(&mut c, &h);
let c = orient_outward(c);
tris_to_mesh(&boolean(&h, &c, BoolOp::Difference))
}
pub fn subtract_many(host: &Mesh, cutters: &[&Mesh]) -> Option<Mesh> {
let h = orient_outward(mesh_to_tris(host));
let comp_tris: Vec<Vec<Tri>> = cutters
.iter()
.map(|m| {
let mut c = mesh_to_tris(m);
promote_cutter_verts_onto_host_faces(&mut c, &h);
orient_outward(c)
})
.collect();
let refs: Vec<&[Tri]> = comp_tris.iter().map(|c| c.as_slice()).collect();
Some(tris_to_mesh(&difference_all(&h, &refs)?))
}
pub fn union(a: &Mesh, b: &Mesh) -> Mesh {
let a = orient_outward(mesh_to_tris(a));
let b = orient_outward(mesh_to_tris(b));
tris_to_mesh(&boolean(&a, &b, BoolOp::Union))
}
pub fn union_many(meshes: &[&Mesh]) -> Mesh {
let tri_lists: Vec<Vec<Tri>> =
meshes.iter().map(|m| orient_outward(mesh_to_tris(m))).collect();
let refs: Vec<&[Tri]> = tri_lists.iter().map(|t| t.as_slice()).collect();
tris_to_mesh(&union_all(&refs))
}
pub fn intersection(a: &Mesh, b: &Mesh) -> Mesh {
let a = orient_outward(mesh_to_tris(a));
let b = orient_outward(mesh_to_tris(b));
tris_to_mesh(&boolean(&a, &b, BoolOp::Intersection))
}
#[cfg(test)]
mod tests {
use super::super::arrangement::cube_mesh;
use super::*;
fn mesh_volume(m: &Mesh) -> f64 {
let vertex = |i: u32| {
let b = (i as usize) * 3;
[
m.positions[b] as f64,
m.positions[b + 1] as f64,
m.positions[b + 2] as f64,
]
};
m.indices
.chunks_exact(3)
.map(|c| {
let (a, bb, cc) = (vertex(c[0]), vertex(c[1]), vertex(c[2]));
let cr = [
bb[1] * cc[2] - bb[2] * cc[1],
bb[2] * cc[0] - bb[0] * cc[2],
bb[0] * cc[1] - bb[1] * cc[0],
];
a[0] * cr[0] + a[1] * cr[1] + a[2] * cr[2]
})
.sum::<f64>()
/ 6.0
}
#[test]
fn snap_reconciles_near_coplanar_and_is_deterministic() {
assert_eq!(super::snap(1.0), super::snap(1.0 + 1e-6));
assert_eq!(super::snap(2.5), super::snap(2.5 - 5e-6));
assert_eq!(super::snap(3.0), 3.0);
assert_eq!(super::snap(0.0), 0.0);
assert_eq!(super::snap(7.0 / 65536.0), 7.0 / 65536.0);
assert_ne!(super::snap(1.0), super::snap(1.0 + 1e-3));
}
#[test]
fn kernel_cuts_a_real_mesh() {
let host = tris_to_mesh(&cube_mesh(0.0, 2.0)); let cutter = tris_to_mesh(&cube_mesh(1.0, 3.0)); let result = subtract(&host, &cutter);
assert!(!result.indices.is_empty(), "subtract produced an empty mesh");
let v = mesh_volume(&result);
assert!((v - 7.0).abs() < 1e-3, "Mesh host−cutter volume = {v}, expected 7");
assert!((mesh_volume(&host) - 8.0).abs() < 1e-4, "host round-trip volume wrong");
}
#[test]
fn kernel_cuts_a_through_wall_opening() {
use super::super::arrangement::box_mesh;
let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [4., 3., 0.2])); let opening = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); let result = subtract(&wall, &opening);
let v = mesh_volume(&result);
assert!((v - 2.2).abs() < 1e-3, "through-opening wall volume = {v}, expected 2.2");
}
#[test]
fn extended_cutter_graze_subtracts_exactly() {
fn mesh_of(vs: &[[f32; 3]], fs: &[[u32; 3]]) -> Mesh {
let mut m = Mesh::new();
for v in vs {
m.positions.extend_from_slice(v);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
for f in fs {
m.indices.extend_from_slice(f);
}
m
}
let host = mesh_of(
&[
[274.05923, 400.96225, 34.600006],
[276.68744, 404.85873, 34.600006],
[276.52164, 404.97058, 34.600006],
[274.00525, 401.2399, 34.600006],
[274.05923, 400.96225, 38.600006],
[276.68744, 404.85873, 38.600006],
[276.52164, 404.97058, 38.600006],
[274.00525, 401.2399, 38.600006],
],
&[
[3, 1, 0], [1, 3, 2], [7, 4, 5], [5, 6, 7], [0, 1, 5], [0, 5, 4],
[1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
],
);
let cutter = mesh_of(
&[
[277.01904, 404.63507, 34.6],
[276.39276, 403.70654, 34.6],
[276.39276, 403.70654, 36.82],
[277.01904, 404.63507, 36.82],
[276.3724, 405.07123, 34.6],
[275.7461, 404.1427, 34.6],
[275.7461, 404.1427, 36.82],
[276.3724, 405.07123, 36.82],
],
&[
[2, 0, 3], [0, 2, 1], [6, 7, 4], [4, 5, 6], [0, 1, 5], [0, 5, 4],
[1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
],
);
assert!((mesh_volume(&host) - 3.680154).abs() < 1e-4, "host operand changed");
assert!((mesh_volume(&cutter) - 1.939390).abs() < 1e-4, "cutter operand changed");
let result = subtract(&host, &cutter);
let v = mesh_volume(&result);
assert!((v - 3.182871).abs() < 1e-3, "subtract volume = {v}, expected ≈3.182871");
let s = 1e5_f32;
let key = |i: u32| {
let b = i as usize * 3;
(
(result.positions[b] * s).round() as i64,
(result.positions[b + 1] * s).round() as i64,
(result.positions[b + 2] * s).round() as i64,
)
};
let mut edges = std::collections::HashMap::new();
for t in result.indices.chunks_exact(3) {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*edges.entry((key(a), key(b))).or_insert(0i32) += 1;
*edges.entry((key(b), key(a))).or_insert(0i32) -= 1;
}
}
let bad = edges.values().filter(|&&c| c != 0).count();
assert_eq!(bad, 0, "result has {bad} unpaired directed edges");
}
fn exact_open_edges(m: &Mesh) -> usize {
use std::collections::HashMap;
let key = |i: u32| {
let b = i as usize * 3;
(
m.positions[b].to_bits(),
m.positions[b + 1].to_bits(),
m.positions[b + 2].to_bits(),
)
};
let mut edges: HashMap<_, i64> = HashMap::new();
for t in m.indices.chunks_exact(3) {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
*edges.entry((key(a), key(b))).or_insert(0) += 1;
*edges.entry((key(b), key(a))).or_insert(0) -= 1;
}
}
edges.values().filter(|&&c| c != 0).count()
}
fn mesh_of(vs: &[[f32; 3]], fs: &[[u32; 3]]) -> Mesh {
let mut m = Mesh::new();
for v in vs {
m.positions.extend_from_slice(v);
m.normals.extend_from_slice(&[0.0, 0.0, 1.0]);
}
for f in fs {
m.indices.extend_from_slice(f);
}
m
}
const PRISM_FACES: [[u32; 3]; 12] = [
[3, 1, 0], [1, 3, 2], [7, 4, 5], [5, 6, 7], [0, 1, 5], [0, 5, 4],
[1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
];
const BOX_FACES: [[u32; 3]; 12] = [
[2, 0, 3], [0, 2, 1], [6, 7, 4], [4, 5, 6], [0, 1, 5], [0, 5, 4],
[1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7],
];
#[test]
fn tilted_flush_recess_cut_is_watertight_198779() {
let host = mesh_of(
&[
[301.04767, 363.11743, 47.6],
[300.70264, 362.6059, 47.6],
[300.84857, 362.50748, 47.6],
[301.24, 363.08783, 47.6],
[301.04767, 363.11743, 50.25],
[300.70264, 362.6059, 50.25],
[300.84857, 362.50748, 50.25],
[301.24, 363.08783, 50.25],
],
&PRISM_FACES,
);
let cutter = mesh_of(
&[
[300.85583, 362.51828, 47.6],
[300.84506, 362.52554, 47.6],
[300.8378, 362.51477, 47.6],
[300.84857, 362.50748, 47.6],
[300.85583, 362.51828, 50.25],
[300.84506, 362.52554, 50.25],
[300.8378, 362.51477, 50.25],
[300.84857, 362.50748, 50.25],
],
&BOX_FACES,
);
let result = subtract(&host, &cutter);
let open = exact_open_edges(&result);
assert_eq!(open, 0, "tilted-flush recess cut left {open} exact-coordinate open edges");
let v = mesh_volume(&result);
assert!(
(v - 0.306705).abs() < 1e-3,
"recess cut volume = {v}, expected ≈0.306705 (analytic; pre-fix 0.107)"
);
}
#[test]
fn native_unit_flush_slant_diff_is_watertight_387738() {
let host = mesh_of(
&[
[478.50012207031250, -0.0000457763671875, 0.0],
[3580.001708984375, -6699.17578125, 167.4681396484375],
[-1764.322265625, -6699.17578125, 167.4681396484375],
[478.50012207031250, -0.0000457763671875, 499.907318115234375],
[-1764.322265625, -6699.17578125, 667.37548828125],
[3580.001708984375, -6699.17578125, 667.37548828125],
],
&[
[0, 1, 2], [3, 4, 5], [2, 1, 5], [2, 5, 4],
[1, 0, 3], [1, 3, 5], [0, 2, 4], [0, 4, 3],
],
);
let cutter = mesh_of(
&[
[-1764.322113037109375, -6699.175338745117188, 283.737548828125],
[478.50012207031250, -0.0000457763671875, 283.737548828125],
[478.50012207031250, -0.0000457763671875, 0.0],
[-1764.322265625, -6699.17529296875, 167.468124389648438],
[3580.0015258789062, -6699.175384521484375, 283.737548828125],
[3580.001708984375, -6699.17529296875, 167.468124389648438],
],
&[
[0, 1, 2], [0, 2, 3], [4, 1, 0], [1, 4, 5],
[1, 5, 2], [5, 3, 2], [4, 0, 3], [4, 3, 5],
],
);
let host_vol = mesh_volume(&host);
let result = subtract(&host, &cutter);
let open = exact_open_edges(&result);
assert_eq!(open, 0, "flush-slant DIFF left {open} exact-coordinate open edges");
let v = mesh_volume(&result);
assert!(
v > 0.0 && v < host_vol,
"DIFF volume {v} not inside (0, host {host_vol})"
);
let expected = 5.868313e9_f64; assert!(
(v - expected).abs() / expected < 1e-5,
"DIFF volume = {v}, expected ≈{expected} (IfcOpenShell oracle)"
);
}
#[test]
fn kernel_cuts_two_sequential_openings() {
use super::super::arrangement::box_mesh;
let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 3., 0.2])); let op1 = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); let op2 = tris_to_mesh(&box_mesh([4., 1., -0.5], [5., 2., 0.7])); let after2 = subtract(&subtract(&wall, &op1), &op2);
let v = mesh_volume(&after2);
assert!((v - 3.2).abs() < 1e-3, "two-opening wall volume = {v}, expected 3.2");
}
#[test]
fn tangential_touch_on_host_diagonal_is_watertight() {
use super::super::arrangement::box_mesh;
let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 0.2, 3.])); let window = tris_to_mesh(&box_mesh([4., -0.3, 0.5], [5., 0.5, 2.0])); let result = subtract(&wall, &window);
let open = exact_open_edges(&result);
assert_eq!(open, 0, "tangential-touch cut left {open} exact open edges");
let v = mesh_volume(&result);
assert!((v - 3.3).abs() < 1e-3, "window cut volume = {v}, expected 3.3");
}
#[test]
fn subtract_many_two_pocket_group_matches_sequential() {
use super::super::arrangement::box_mesh;
let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [6., 0.2, 3.])); let door = tris_to_mesh(&box_mesh([1., -1.0, 0.0], [2., 1.2, 2.5])); let window = tris_to_mesh(&box_mesh([4., -0.3, 0.5], [5., 0.5, 2.0]));
let seq = subtract(&subtract(&wall, &door), &window);
let many = subtract_many(&wall, &[&door, &window]).expect("group must conform");
let (vs, vm) = (mesh_volume(&seq), mesh_volume(&many));
let om = exact_open_edges(&many);
assert_eq!(om, 0, "batched two-pocket cut left {om} exact open edges");
assert!(
(vs - vm).abs() < 1e-6,
"batched volume {vm} != sequential volume {vs} on disjoint cutters"
);
}
#[test]
fn subtract_many_disjoint_openings_matches_sequential() {
use super::super::arrangement::box_mesh;
let wall = tris_to_mesh(&box_mesh([0., 0., 0.], [9., 3., 0.2])); let op1 = tris_to_mesh(&box_mesh([1., 1., -0.5], [2., 2., 0.7])); let mut op2 = tris_to_mesh(&box_mesh([4., 1., -0.5], [5., 2., 0.7])); let op3 = tris_to_mesh(&box_mesh([7., 1., -0.5], [8., 2., 0.7])); for t in op2.indices.chunks_exact_mut(3) {
t.swap(1, 2);
}
let batched = subtract_many(&wall, &[&op1, &op2, &op3])
.expect("disjoint box group must conform");
let v = mesh_volume(&batched);
assert!((v - 4.8).abs() < 1e-3, "batched 3-opening wall volume = {v}, expected 4.8");
let open = exact_open_edges(&batched);
assert_eq!(open, 0, "batched cut left {open} exact-coordinate open edges");
let seq = subtract(&subtract(&subtract(&wall, &op1), &op2), &op3);
let vs = mesh_volume(&seq);
assert!(
(v - vs).abs() < 1e-6,
"batched volume {v} != sequential volume {vs} on disjoint cutters"
);
}
}