use ifc_lite_core::{build_entity_index, EntityDecoder, EntityScanner};
use ifc_lite_geometry::GeometryRouter;
use ifc_lite_processing::element::{
produce_element_meshes, ElementJobKind, ElementMeshJob, GeometryHashConfig,
MeshProductionContext, MeshProductionOptions,
};
use ifc_lite_processing::prepass::{resolve_prepass, PrepassSpans, ResolveOptions};
use rustc_hash::FxHashMap;
const FIXTURE: &str = "tests/fixtures/issue_2019_wall_two_overlapping_openings.ifc";
const WALL_ID: u32 = 30;
fn force_sequential_exact_kernel() {
static GATES: std::sync::Once = std::sync::Once::new();
GATES.call_once(|| unsafe {
std::env::set_var("IFC_LITE_VOID_UNION", "0");
std::env::set_var("IFC_LITE_RECT_FAST", "0");
std::env::set_var("IFC_LITE_VOID_2D", "0");
std::env::set_var("IFC_LITE_PRISM_CUT", "0");
});
}
fn production_void_index(content: &str, decoder: &mut EntityDecoder) -> FxHashMap<u32, Vec<u32>> {
let mut spans = PrepassSpans::default();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
match type_name {
"IFCRELVOIDSELEMENT" => spans.void_rels.push((id, start, end)),
"IFCRELFILLSELEMENT" => spans.fills_rels.push((id, start, end)),
"IFCRELAGGREGATES" => spans.aggregate_rels.push((id, start, end)),
_ => {}
}
}
let resolved = resolve_prepass(&spans, decoder, ResolveOptions::default());
resolved.void_index
}
fn with_void_rel_statements_reversed(content: &str) -> String {
let mut lines: Vec<&str> = content.lines().collect();
let rel_line_indices: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, l)| l.contains("IFCRELVOIDSELEMENT"))
.map(|(i, _)| i)
.collect();
let mut texts: Vec<&str> = rel_line_indices.iter().map(|&i| lines[i]).collect();
texts.reverse();
for (slot, text) in rel_line_indices.into_iter().zip(texts.into_iter()) {
lines[slot] = text;
}
let mut out = lines.join("\n");
out.push('\n');
out
}
fn hash_and_aabb_for(
content: &str,
void_index: &FxHashMap<u32, Vec<u32>>,
) -> (Option<u64>, Option<[f64; 6]>, usize) {
let index = std::sync::Arc::new(build_entity_index(content));
let mut decoder = EntityDecoder::with_arc_index(content.as_bytes(), index.clone());
let router = GeometryRouter::with_units(content, &mut decoder);
decoder.seed_unit_scales(router.unit_scale(), 1.0);
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,
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: Some(GeometryHashConfig {
tolerance: ifc_lite_geometry::DEFAULT_GEOM_HASH_TOLERANCE,
world_rtc: [0.0; 3],
}),
};
let entity = decoder.decode_by_id(WALL_ID).expect("wall entity decodes");
let ifc_type = entity.ifc_type;
let produced = produce_element_meshes(
&ElementMeshJob {
id: WALL_ID,
ifc_type,
entity: &entity,
kind: ElementJobKind::Product,
element_color: None,
metadata: None,
},
&ctx,
&opts,
&mut decoder,
&router,
);
let tris: usize = produced.meshes.iter().map(|m| m.indices.len() / 3).sum();
(produced.geometry_hash, produced.geometry_aabb, tris)
}
#[test]
fn sequential_cut_order_is_not_associative() {
force_sequential_exact_kernel();
let content = std::fs::read_to_string(FIXTURE)
.unwrap_or_else(|e| panic!("failed to read fixture {FIXTURE}: {e}"));
let orders: [[u32; 3]; 2] = [[50, 70, 100], [100, 50, 70]];
let mut seen = Vec::new();
for order in orders {
let mut idx: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
idx.insert(WALL_ID, order.to_vec());
let (hash, _aabb, tris) = hash_and_aabb_for(&content, &idx);
eprintln!("order {order:?}: hash={hash:?} tris={tris}");
seen.push((hash, tris));
}
assert_ne!(
seen[0], seen[1],
"expected the sequential exact kernel to be order-sensitive for these \
mutually-overlapping cutters (that is the mechanism #2019's fix \
works around by canonicalising void_index's order, not by changing \
the kernel) — if this now matches, the fixture or the env-var gates \
drifted and this test's premise needs re-checking"
);
}
#[test]
fn world_geometry_hash_is_invariant_to_reordered_void_rel_statements() {
force_sequential_exact_kernel();
let content = std::fs::read_to_string(FIXTURE)
.unwrap_or_else(|e| panic!("failed to read fixture {FIXTURE}: {e}"));
let reordered = with_void_rel_statements_reversed(&content);
assert_ne!(content, reordered, "the reorder helper must actually change the file bytes");
let mut decoder_a = EntityDecoder::new(&content);
let void_index_a = production_void_index(&content, &mut decoder_a);
let mut decoder_b = EntityDecoder::new(&reordered);
let void_index_b = production_void_index(&reordered, &mut decoder_b);
let openings_a = void_index_a.get(&WALL_ID).cloned().unwrap_or_default();
let openings_b = void_index_b.get(&WALL_ID).cloned().unwrap_or_default();
eprintln!("resolve_prepass(original) void_index[{WALL_ID}] = {openings_a:?}");
eprintln!("resolve_prepass(reordered) void_index[{WALL_ID}] = {openings_b:?}");
assert_eq!(
openings_a, openings_b,
"resolve_prepass produced a DIFFERENT opening order for the wall from a \
file that only reordered its IFCRELVOIDSELEMENT statements — void_index \
is not canonicalised (#2019)"
);
assert_eq!(openings_a.len(), 3, "fixture rot: expected 3 openings on wall #{WALL_ID}");
let (hash_uncut, _aabb_uncut, tris_uncut) = hash_and_aabb_for(&content, &FxHashMap::default());
let (hash_a, aabb_a, tris_a) = hash_and_aabb_for(&content, &void_index_a);
let (hash_b, aabb_b, tris_b) = hash_and_aabb_for(&reordered, &void_index_b);
eprintln!("original: hash={hash_a:?} aabb={aabb_a:?} tris={tris_a}");
eprintln!("reordered: hash={hash_b:?} aabb={aabb_b:?} tris={tris_b}");
assert_ne!(
(hash_a, tris_a),
(hash_uncut, tris_uncut),
"sanity check failed: the original file produced the SAME hash/tri \
count as the uncut wall -- the openings never cut anything, so this \
test would prove nothing"
);
assert_eq!(
hash_a, hash_b,
"wall #{WALL_ID}: world geometry hash changed for a file that only \
reordered its IFCRELVOIDSELEMENT statements — the hash is not a \
stable content address for opening-cut geometry (#2019)"
);
assert_eq!(
aabb_a, aabb_b,
"wall #{WALL_ID}: world AABB changed for a file that only reordered \
its IFCRELVOIDSELEMENT statements (#2019)"
);
}