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 mesh_to_tris_drops_out_of_range_index_without_panicking() {
let mut m = Mesh::new();
m.positions.extend_from_slice(&[0.0, 0.0, 0.0]);
m.positions.extend_from_slice(&[1.0, 0.0, 0.0]);
m.positions.extend_from_slice(&[0.0, 1.0, 0.0]);
m.indices.extend_from_slice(&[0, 1, 2, 0, 1, 5]);
let tris = mesh_to_tris(&m);
assert_eq!(tris.len(), 1, "malformed triangle (OOB index) must be dropped, not panic");
assert_eq!(tris[0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
}
#[test]
fn mesh_to_tris_drops_non_finite_coordinate_without_panicking() {
let mut m = Mesh::new();
m.positions.extend_from_slice(&[0.0, 0.0, 0.0]);
m.positions.extend_from_slice(&[1.0, 0.0, 0.0]);
m.positions.extend_from_slice(&[0.0, 1.0, 0.0]);
m.positions.extend_from_slice(&[f32::NAN, 0.0, 0.0]);
m.positions.extend_from_slice(&[f32::INFINITY, 0.0, 0.0]);
m.indices
.extend_from_slice(&[0, 1, 2, 0, 1, 3, 0, 1, 4]);
let tris = mesh_to_tris(&m);
assert_eq!(
tris.len(),
1,
"triangles touching a NaN or Inf coordinate must be dropped, not panic"
);
assert_eq!(tris[0], [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
}
#[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"
);
}