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-volume gate 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>,
) -> Vec<(u32, ProducedElementMeshes)> {
let router = GeometryRouter::with_scale(1.0);
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;
out.push((
id,
produce_element_meshes(
&ElementMeshJob {
id,
ifc_type,
entity: &entity,
kind: ElementJobKind::Product,
element_color: None,
metadata: None,
},
&ctx,
&opts,
&mut decoder,
&router,
),
));
}
out
}
fn cfg(rtc: [f64; 3]) -> Option<GeometryHashConfig> {
Some(GeometryHashConfig {
tolerance: ifc_lite_geometry::DEFAULT_GEOM_HASH_TOLERANCE,
world_rtc: rtc,
})
}
fn box_volume(a: &[f64; 6]) -> f64 {
(a[3] - a[0]).max(0.0) * (a[4] - a[1]).max(0.0) * (a[5] - a[2]).max(0.0)
}
#[test]
fn no_reported_volume_exceeds_its_own_bounding_box() {
let Some(content) = read_fixture() else { return };
let index = Arc::new(build_entity_index(&content));
let mut with_volume = 0usize;
let mut without_volume = 0usize;
let mut boxlike = 0usize;
let mut refused_open = 0usize;
let mut refused_multi_segment = 0usize;
let mut refused_multi_component = 0usize;
for (id, p) in produce_all(&content, &index, cfg([0.0; 3])) {
let Some(closure) = p.geometry_closure else {
assert!(
p.geometry_volume.is_none(),
"#{id}: a volume without a closure verdict means the gate was bypassed"
);
continue;
};
let Some(volume) = p.geometry_volume else {
without_volume += 1;
if !closure.all_closed { refused_open += 1; }
if closure.segments > 1 { refused_multi_segment += 1; }
if !closure.all_single_component { refused_multi_component += 1; }
assert!(
!closure.is_trustworthy_solid(),
"#{id}: the verdict says this IS a single closed solid, so a volume was owed"
);
continue;
};
with_volume += 1;
assert!(
closure.is_trustworthy_solid() && closure.bits() == 0b1111,
"#{id}: a volume shipped with flags {:#06b} — the flags must agree with the gate",
closure.bits()
);
assert!(
volume > 0.0 && volume.is_finite(),
"#{id}: reported a non-positive / non-finite volume {volume}"
);
let aabb = p
.geometry_aabb
.unwrap_or_else(|| panic!("#{id}: a volume without a box"));
let bv = box_volume(&aabb);
assert!(
volume <= bv * (1.0 + 1e-6) + 1e-12,
"#{id}: volume {volume} exceeds its own AABB volume {bv} — impossible for a solid, \
so the gate let through a sum over overlapping pieces"
);
if bv > 1e-9 && (volume / bv - 1.0).abs() < 1e-4 {
boxlike += 1;
}
}
assert!(
with_volume >= 50,
"only {with_volume} elements reported a volume — the gate refuses (almost) everything, \
which passes the impossibility assertion above for the wrong reason"
);
assert!(
without_volume >= 20,
"only {without_volume} elements were refused — the gate is not gating, so the open / \
multi-item cases it exists to refuse are getting numbers anyway"
);
assert!(
refused_open >= 1 && refused_multi_segment >= 1 && refused_multi_component >= 1,
"the three refusal reasons must all occur in this fixture, got open={refused_open} \
multi_segment={refused_multi_segment} multi_component={refused_multi_component}"
);
assert!(
boxlike >= 10,
"only {boxlike} elements matched their bounding box exactly; this fixture is full of \
axis-aligned boxes, so the divergence sum is not reproducing their known volume"
);
}
#[test]
fn reported_volume_is_rtc_invariant() {
let Some(content) = read_fixture() else { return };
let index = Arc::new(build_entity_index(&content));
let plain: FxHashMap<u32, f64> = produce_all(&content, &index, cfg([0.0; 3]))
.into_iter()
.filter_map(|(id, p)| p.geometry_volume.map(|v| (id, v)))
.collect();
let shifted: FxHashMap<u32, f64> = produce_all(&content, &index, cfg([412_000.5, -5_310_000.25, 90.125]))
.into_iter()
.filter_map(|(id, p)| p.geometry_volume.map(|v| (id, v)))
.collect();
assert!(plain.len() >= 50, "expected the fixture's elements, got {}", plain.len());
assert_eq!(
plain.len(),
shifted.len(),
"the RTC must not change WHICH elements are trusted with a volume"
);
for (id, v) in &plain {
let s = shifted
.get(id)
.unwrap_or_else(|| panic!("#{id} missing from the shifted run"));
assert!(
(s - v).abs() <= v.abs() * 1e-9,
"#{id}: volume moved from {v} to {s} under a 5,000 km RTC shift — the accumulator \
is referencing the world origin instead of the geometry"
);
}
}
#[test]
fn no_volume_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);
assert!(!produced.is_empty(), "fixture produced no elements");
for (id, p) in produced {
assert!(
p.geometry_volume.is_none() && p.geometry_closure.is_none(),
"#{id}: hashing is off, so neither volume nor closure verdict may be computed"
);
}
}