use ifc_lite_geometry::{ClippingProcessor, Mesh, Point3, Vector3};
use std::collections::HashMap;
fn boxed(min: [f64; 3], size: [f64; 3]) -> Mesh {
let mx = [min[0] + size[0], min[1] + size[1], min[2] + size[2]];
let c = |i: usize| -> [f64; 2] { [min[i], mx[i]] };
let corners: Vec<Point3<f64>> = [
(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0),
(0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1),
]
.iter()
.map(|&(i, j, k)| Point3::new(c(0)[i], c(1)[j], c(2)[k]))
.collect();
let faces: [[usize; 4]; 6] = [
[0, 3, 2, 1], [4, 5, 6, 7], [0, 1, 5, 4],
[2, 3, 7, 6], [0, 4, 7, 3], [1, 2, 6, 5],
];
let mut m = Mesh::with_capacity(24, 36);
for f in &faces {
let e1 = corners[f[1]] - corners[f[0]];
let e2 = corners[f[2]] - corners[f[0]];
let n = e1.cross(&e2).try_normalize(1e-12).unwrap_or(Vector3::z());
let b = m.vertex_count() as u32;
for &i in f {
m.add_vertex(corners[i], n);
}
m.add_triangle(b, b + 1, b + 2);
m.add_triangle(b, b + 2, b + 3);
}
m
}
fn rotated_boxed(min: [f64; 3], size: [f64; 3], theta: f64) -> Mesh {
let mut m = boxed(min, size);
let (s, c) = theta.sin_cos();
for p in m.positions.chunks_exact_mut(3) {
let (x, y) = (p[0] as f64, p[1] as f64);
p[0] = (c * x - s * y) as f32;
p[1] = (s * x + c * y) as f32;
}
m
}
fn open_edges(m: &Mesh) -> Result<usize, String> {
if m.is_empty() {
return Err("boolean output was empty".to_string());
}
let welded = m.welded_by_position(1e-6);
let mut edges: HashMap<(u32, u32), (u32, u32)> = HashMap::new();
for tri in welded.indices.chunks_exact(3) {
for k in 0..3 {
let (a, b) = (tri[k], tri[(k + 1) % 3]);
if a == b {
return Err(format!("degenerate edge: triangle repeats welded vertex {a}"));
}
let e = edges.entry((a.min(b), a.max(b))).or_insert((0, 0));
if a < b {
e.0 += 1;
} else {
e.1 += 1;
}
}
}
Ok(edges.values().filter(|&&(f, r)| f != 1 || r != 1).count())
}
#[test]
#[ignore = "known open defect, issue #3353 (consolidation half): classify.rs's two \
coincident-face detectors (c_on_or_near_a vs BComponents::surface_normal) \
disagree on a near-degenerate rotated overlap, leaving a redundant triangle \
on both operands and a non-manifold shared edge. NOT the near-coplanar half \
fixed by issue_3353_near_coplanar_rotated_overlap.rs, whose weld leaves this \
case unchanged"]
fn a_rotated_overlapping_union_stays_manifold() {
let clipper = ClippingProcessor::new();
let a_min = [-1.064427873716452, -1.5758991032070164, -2.335934512221897];
let a_size = [1.7582981472721038, 3.7437572581011054, 2.5580013636220693];
let b_min = [-0.468184233137136, -1.8002926870781526, -1.0101295786317475];
let b_size = [1.9952974802528327, 3.404965797097641, 3.2964264954246745];
let theta = 1.3158416849982029;
let a = boxed(a_min, a_size);
let b = rotated_boxed(b_min, b_size, theta);
let out = clipper.union_mesh(&a, &b).expect("union must not error");
match open_edges(&out) {
Ok(0) => {}
Ok(bad) => panic!(
"union of two closed operands came back non-manifold: \
{bad} unmatched directed edges (see issue #3353)"
),
Err(why) => panic!("union of two closed operands came back invalid: {why}"),
}
}