use ifc_lite_core::{build_entity_index, EntityDecoder, EntityScanner, IfcType};
use ifc_lite_geometry::{propagate_voids_to_parts, GeometryRouter, Mesh, RectParam};
use nalgebra::Matrix3;
use rustc_hash::FxHashMap;
const DEFAULT_FIXTURE: &str = "../../tests/models/buildingsmart/wall-with-opening-and-window.ifc";
fn build_void_index(content: &str, decoder: &mut EntityDecoder) -> FxHashMap<u32, Vec<u32>> {
let mut void_index: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
let mut scanner = EntityScanner::new(content);
let mut scan_decoder = EntityDecoder::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCRELVOIDSELEMENT" {
if let Ok(entity) = scan_decoder.decode_at_with_id(id, start, end) {
if let (Some(host_id), Some(opening_id)) = (entity.get_ref(4), entity.get_ref(5)) {
void_index.entry(host_id).or_default().push(opening_id);
}
}
}
}
let _ = propagate_voids_to_parts(&mut void_index, content, decoder);
void_index
}
fn extents_in_frame(mesh: &Mesh, rt: &Matrix3<f64>) -> [f64; 3] {
let mut mn = [f64::INFINITY; 3];
let mut mx = [f64::NEG_INFINITY; 3];
for c in mesh.positions.chunks_exact(3) {
let p = rt * nalgebra::Vector3::new(c[0] as f64, c[1] as f64, c[2] as f64);
for k in 0..3 {
mn[k] = mn[k].min(p[k]);
mx[k] = mx[k].max(p[k]);
}
}
[mx[0] - mn[0], mx[1] - mn[1], mx[2] - mn[2]]
}
fn is_signed_permutation(m: &Matrix3<f64>, tol: f64) -> bool {
let mut used = [false; 3];
for i in 0..3 {
let mut best = 0usize;
let mut best_abs = 0.0;
let mut second = 0.0;
for j in 0..3 {
let a = m[(i, j)].abs();
if a > best_abs {
second = best_abs;
best_abs = a;
best = j;
} else if a > second {
second = a;
}
}
if best_abs < 1.0 - tol || second > tol || used[best] {
return false;
}
used[best] = true;
}
true
}
fn sorted3(mut a: [f64; 3]) -> [f64; 3] {
a.sort_by(|x, y| x.partial_cmp(y).unwrap());
a
}
#[test]
#[ignore = "phase-0 census -- run explicitly with MEASURE_FIXTURE"]
fn parametric_rect_census() {
let fixture = std::env::var("MEASURE_FIXTURE").unwrap_or_else(|_| DEFAULT_FIXTURE.to_string());
if !std::path::Path::new(&fixture).exists() {
eprintln!("skipping: fixture {fixture} not present");
return;
}
let content = std::fs::read_to_string(&fixture).expect("read fixture");
let entity_index = build_entity_index(&content);
let mut decoder = EntityDecoder::with_index(&content, entity_index);
let router = GeometryRouter::with_units(&content, &mut decoder);
let void_index = build_void_index(&content, &mut decoder);
let mut host_ids: Vec<u32> = void_index.keys().copied().collect();
host_ids.sort_unstable();
let mut hosts = 0usize;
let mut host_probe_ok = 0usize; let mut host_box_match = 0usize; let mut openings = 0usize;
let mut opening_probe_ok = 0usize;
let mut opening_frame_match = 0usize;
let mut fireable_hosts = 0usize;
let mut vol_buckets = [0usize; 6]; let mut worst_axis_ratio_examples = 0usize;
for host_id in host_ids {
let Ok(host) = decoder.decode_by_id(host_id) else { continue };
if !matches!(
host.ifc_type,
IfcType::IfcWall | IfcType::IfcWallStandardCase | IfcType::IfcSlab | IfcType::IfcRoof
| IfcType::IfcColumn | IfcType::IfcBeam | IfcType::IfcMember | IfcType::IfcPlate
| IfcType::IfcCovering | IfcType::IfcFooting
) {
continue;
}
hosts += 1;
let Some(hp): Option<RectParam> = router.parametric_rect_probe(&host, &mut decoder) else {
continue;
};
host_probe_ok += 1;
let rt_host = hp.r.transpose();
let mut this_host_box_ok = false;
if let Ok(mesh) = router.process_element(&host, &mut decoder) {
if !mesh.positions.is_empty() {
let e = extents_in_frame(&mesh, &rt_host);
let param = [hp.half[0] * 2.0, hp.half[1] * 2.0, hp.half[2] * 2.0];
let ps = sorted3(param);
let es = sorted3(e);
let vol_param = ps[0] * ps[1] * ps[2];
let vol_mesh = (es[0] * es[1] * es[2]).max(1e-12);
let ratio = vol_param / vol_mesh;
let b = if ratio < 0.5 { 0 } else if ratio < 0.9 { 1 }
else if ratio < 0.98 { 2 } else if ratio < 1.02 { 3 }
else if ratio < 1.1 { 4 } else { 5 };
vol_buckets[b] += 1;
let axis_ok = (0..3).all(|k| {
let lo = ps[k].min(es[k]);
let hi = ps[k].max(es[k]).max(1e-9);
lo / hi > 0.98
});
if axis_ok {
this_host_box_ok = true;
host_box_match += 1;
} else if worst_axis_ratio_examples < 6 {
worst_axis_ratio_examples += 1;
eprintln!(
" box-mismatch host {host_id}: param(sorted)={ps:?} mesh(sorted)={es:?} ratio={ratio:.3}"
);
}
}
}
let mut all_openings_match = true;
let mut host_opening_count = 0usize;
for &opening_id in &void_index[&host_id] {
let Ok(opening) = decoder.decode_by_id(opening_id) else {
all_openings_match = false;
continue;
};
if opening.ifc_type != IfcType::IfcOpeningElement {
continue;
}
host_opening_count += 1;
openings += 1;
let Some(op): Option<RectParam> = router.parametric_rect_probe(&opening, &mut decoder)
else {
all_openings_match = false;
continue;
};
opening_probe_ok += 1;
let m = rt_host * op.r;
if is_signed_permutation(&m, 1.0e-3) {
opening_frame_match += 1;
} else {
all_openings_match = false;
}
}
if this_host_box_ok && host_opening_count > 0 && all_openings_match {
fireable_hosts += 1;
}
}
let pct = |n: usize, d: usize| if d == 0 { 0.0 } else { 100.0 * n as f64 / d as f64 };
eprintln!("\n========= PHASE-0 PARAMETRIC RECT CENSUS =========");
eprintln!("fixture : {fixture}");
eprintln!("void hosts (bldg elems) : {hosts}");
eprintln!("--------------------------------------------------");
eprintln!("HOST is clean rect extrusion (probe ok) : {host_probe_ok} ({:.1}%)", pct(host_probe_ok, hosts));
eprintln!("HOST param-box == mesh AABB (recon pass) : {host_box_match} ({:.1}% of probe-ok)", pct(host_box_match, host_probe_ok));
eprintln!("--------------------------------------------------");
eprintln!("openings probed : {openings}");
eprintln!(" opening clean rect (probe ok): {opening_probe_ok} ({:.1}%)", pct(opening_probe_ok, openings));
eprintln!(" R_open == R_host (frame match): {opening_frame_match} ({:.1}% of all, {:.1}% of probe-ok)",
pct(opening_frame_match, openings), pct(opening_frame_match, opening_probe_ok));
eprintln!("--------------------------------------------------");
eprintln!("FIREABLE hosts (probe+recon+all-openings-match): {fireable_hosts} ({:.1}% of hosts)", pct(fireable_hosts, hosts));
eprintln!("--------- vol(param)/vol(meshAABB) histogram -----");
let labels = ["<0.5", "0.5-0.9", "0.9-0.98", "0.98-1.02", "1.02-1.1", ">1.1"];
for (i, l) in labels.iter().enumerate() {
eprintln!(" {l:>9} : {}", vol_buckets[i]);
}
eprintln!("==================================================\n");
}