#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use brepkit_algo::bop::BooleanOp;
use brepkit_algo::gfa;
use brepkit_io::arena_io::deserialize_solid;
use brepkit_topology::Topology;
use brepkit_topology::explorer::solid_faces;
use brepkit_topology::face::FaceId;
use brepkit_topology::solid::SolidId;
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(name)
}
fn load(name: &str, topo: &mut Topology) -> SolidId {
deserialize_solid(&std::fs::read(fixture(name)).unwrap(), topo).unwrap()
}
fn edge_health(topo: &Topology, solid: SolidId) -> (usize, usize) {
type QPoint = (i64, i64, i64);
let scale = 1.0e5;
let q = |p: brepkit_math::vec::Point3| -> QPoint {
(
(p.x() * scale).round() as i64,
(p.y() * scale).round() as i64,
(p.z() * scale).round() as i64,
)
};
let mut faces_per_edge: HashMap<(QPoint, QPoint), HashSet<FaceId>> = HashMap::new();
let mut occ: HashMap<(QPoint, QPoint), usize> = HashMap::new();
for fid in solid_faces(topo, solid).unwrap() {
let face = topo.face(fid).unwrap();
for wid in std::iter::once(face.outer_wire()).chain(face.inner_wires().iter().copied()) {
for oe in topo.wire(wid).unwrap().edges() {
let e = topo.edge(oe.edge()).unwrap();
let a = q(topo.vertex(e.start()).unwrap().point());
let b = q(topo.vertex(e.end()).unwrap().point());
let key = if a <= b { (a, b) } else { (b, a) };
faces_per_edge.entry(key).or_default().insert(fid);
*occ.entry(key).or_insert(0) += 1;
}
}
}
let free = occ.values().filter(|&&c| c == 1).count();
let over = faces_per_edge.values().filter(|f| f.len() > 2).count();
(free, over)
}
fn curved_count(topo: &Topology, solid: SolidId) -> usize {
solid_faces(topo, solid)
.unwrap()
.iter()
.filter(|&&f| topo.face(f).unwrap().surface().type_tag() != "plane")
.count()
}
#[test]
fn wallcut_each_cut_is_clean_individually() {
for tool in ["wallcuthcomb2x1_cut0.bin", "wallcuthcomb2x1_cut1.bin"] {
let mut topo = Topology::new();
let body = load("wallcuthcomb2x1_body.bin", &mut topo);
let t = load(tool, &mut topo);
let r = gfa::boolean(&mut topo, BooleanOp::Cut, body, t).unwrap();
let (free, over) = edge_health(&topo, r);
assert_eq!(free, 0, "{tool} alone must leave a watertight body");
assert_eq!(over, 0, "{tool} alone must leave a manifold body");
assert!(
curved_count(&topo, r) >= 44,
"{tool} alone preserves the lip cones + corner cylinders"
);
}
}
#[test]
fn wallcut_sequential_cuts_stay_watertight() {
let mut topo = Topology::new();
let body = load("wallcuthcomb2x1_body.bin", &mut topo);
let cut0 = load("wallcuthcomb2x1_cut0.bin", &mut topo);
let cut1 = load("wallcuthcomb2x1_cut1.bin", &mut topo);
let after0 = gfa::boolean(&mut topo, BooleanOp::Cut, body, cut0).unwrap();
let (free0, over0) = edge_health(&topo, after0);
assert_eq!(free0, 0, "first wall-cutout cut is clean");
assert_eq!(over0, 0, "first wall-cutout cut is manifold");
let after1 = gfa::boolean(&mut topo, BooleanOp::Cut, after0, cut1).unwrap();
let (free1, over1) = edge_health(&topo, after1);
assert_eq!(
free1, 0,
"sequential wall-cutout cuts must stay watertight; got free={free1}"
);
assert_eq!(
over1, 0,
"sequential wall-cutout cuts must stay manifold; got over={over1}"
);
assert!(
curved_count(&topo, after1) >= 44,
"analytic surfaces (lip cones + corner cylinders) must be preserved"
);
}