use ifc_lite_core::{build_entity_index, has_geometry_by_name, EntityDecoder, EntityScanner};
use ifc_lite_geometry::GeometryRouter;
use ifc_lite_processing::element::{
produce_element_meshes, ElementJobKind, ElementMeshJob, GeometryHashConfig,
MeshProductionContext, MeshProductionOptions, ProducedElementMeshes,
};
use rustc_hash::FxHashMap;
use std::sync::Arc;
const FIXTURE: &str = "../../tests/models/ara3d/AC20-FZK-Haus.ifc";
fn read_fixture() -> Option<Vec<u8>> {
match std::fs::read(FIXTURE) {
Ok(b) => Some(b),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(
"skipping geometry-hash AABB test: fixture missing at {FIXTURE} — \
run `pnpm fixtures` (sha256 in tests/models/manifest.json)"
);
None
}
Err(e) => panic!("failed to read fixture {FIXTURE}: {e}"),
}
}
fn produce_all(
content: &[u8],
index: &Arc<ifc_lite_core::EntityIndex>,
hash: Option<GeometryHashConfig>,
router_rtc: [f64; 3],
) -> Vec<(u32, ProducedElementMeshes)> {
let mut router = GeometryRouter::with_scale(1.0);
router.set_rtc_offset((router_rtc[0], router_rtc[1], router_rtc[2]));
let mut decoder = EntityDecoder::with_arc_index(content, index.clone());
decoder.seed_unit_scales(router.unit_scale(), 1.0);
let void_index = FxHashMap::default();
let geometry_style_index = FxHashMap::default();
let indexed_colour_full = FxHashMap::default();
let element_material_colors = FxHashMap::default();
let texture_index = FxHashMap::default();
let ctx = MeshProductionContext {
void_index: &void_index,
geometry_style_index: &geometry_style_index,
indexed_colour_full: &indexed_colour_full,
element_material_colors: &element_material_colors,
texture_index: &texture_index,
site_local_rotation: None,
};
let opts = MeshProductionOptions { geometry_hash: hash };
let mut jobs: Vec<(u32, usize, usize)> = Vec::new();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if has_geometry_by_name(type_name) {
jobs.push((id, start, end));
}
}
let mut out = Vec::new();
for (id, start, end) in jobs {
let Ok(entity) = decoder.decode_at_with_id(id, start, end) else {
continue;
};
let ifc_type = entity.ifc_type;
let produced = produce_element_meshes(
&ElementMeshJob {
id,
ifc_type,
entity: &entity,
kind: ElementJobKind::Product,
element_color: None,
metadata: None,
},
&ctx,
&opts,
&mut decoder,
&router,
);
out.push((id, produced));
}
out
}
fn cfg(rtc: [f64; 3]) -> Option<GeometryHashConfig> {
Some(GeometryHashConfig {
tolerance: ifc_lite_geometry::DEFAULT_GEOM_HASH_TOLERANCE,
world_rtc: rtc,
})
}
#[test]
fn reported_aabb_is_the_tight_world_extent_of_the_produced_meshes() {
let Some(content) = read_fixture() else { return };
let index = Arc::new(build_entity_index(&content));
let mut checked = 0usize;
for (id, produced) in produce_all(&content, &index, cfg([0.0; 3]), [0.0; 3]) {
let Some(aabb) = produced.geometry_aabb else {
continue;
};
assert!(
produced.geometry_hash.is_some(),
"#{id}: a box without a fingerprint would misalign the parallel FFI arrays"
);
let mut mn = [f64::INFINITY; 3];
let mut mx = [f64::NEG_INFINITY; 3];
let mut verts = 0usize;
for m in &produced.meshes {
for v in m.positions.chunks_exact(3) {
verts += 1;
for k in 0..3 {
let w = v[k] as f64 + m.origin[k];
mn[k] = mn[k].min(w);
mx[k] = mx[k].max(w);
}
}
}
if verts == 0 {
continue;
}
const TOL: f64 = 1.0e-3;
for k in 0..3 {
assert!(
aabb[k] <= mn[k] + TOL,
"#{id} axis {k}: reported min {} is INSIDE the mesh min {} — the box \
would clip real geometry",
aabb[k],
mn[k]
);
assert!(
aabb[k] >= mn[k] - TOL,
"#{id} axis {k}: reported min {} is far below the mesh min {} — the box \
is not tight, so a move of that size would read as no move",
aabb[k],
mn[k]
);
assert!(
aabb[3 + k] >= mx[k] - TOL,
"#{id} axis {k}: reported max {} is INSIDE the mesh max {}",
aabb[3 + k],
mx[k]
);
assert!(
aabb[3 + k] <= mx[k] + TOL,
"#{id} axis {k}: reported max {} is far above the mesh max {}",
aabb[3 + k],
mx[k]
);
}
checked += 1;
}
assert!(
checked > 100,
"only {checked} elements carried a box — the fixture or the hashing switch \
regressed, so the bounds assertions above proved nothing"
);
}
#[test]
fn reported_aabb_is_rtc_invariant() {
let Some(content) = read_fixture() else { return };
let index = Arc::new(build_entity_index(&content));
let plain: FxHashMap<u32, [f64; 6]> = produce_all(&content, &index, cfg([0.0; 3]), [0.0; 3])
.into_iter()
.filter_map(|(id, p)| p.geometry_aabb.map(|a| (id, a)))
.collect();
let shift = [1234.5_f64, -678.25, 90.125];
let shifted: FxHashMap<u32, [f64; 6]> = produce_all(&content, &index, cfg(shift), shift)
.into_iter()
.filter_map(|(id, p)| p.geometry_aabb.map(|a| (id, a)))
.collect();
assert!(plain.len() > 100, "expected the fixture's elements, got {}", plain.len());
assert_eq!(plain.len(), shifted.len(), "the RTC must not change WHICH elements report");
const TOL: f64 = 1.0e-3;
for (id, a) in &plain {
let b = shifted.get(id).unwrap_or_else(|| panic!("#{id} missing from the shifted run"));
for k in 0..3 {
assert!(
(b[k] - a[k]).abs() < TOL && (b[3 + k] - a[3 + k]).abs() < TOL,
"#{id} axis {k}: RTC-shifted box {b:?} is not the unshifted box {a:?} — \
the file's RTC choice leaked into the reported WORLD box, so every \
element of a georeferenced revision would read as moved by {shift:?}"
);
}
}
}
#[test]
fn no_aabb_when_hashing_is_off() {
let Some(content) = read_fixture() else { return };
let index = Arc::new(build_entity_index(&content));
let produced = produce_all(&content, &index, None, [0.0; 3]);
assert!(!produced.is_empty(), "fixture produced no elements");
for (id, p) in produced {
assert!(
p.geometry_aabb.is_none() && p.geometry_hash.is_none(),
"#{id}: hashing is off, so neither fingerprint nor box may be computed"
);
}
}