use ifc_lite_geometry::{compose_instance_world_row_major, instance_rel_row_major_f32};
use ifc_lite_processing::{
process_geometry_streaming_filtered_with_options, MeshData, OpeningFilterMode,
ProcessingResult, StreamingOptions,
};
use rustc_hash::FxHashMap;
fn fixture_bytes(name: &str) -> Vec<u8> {
let path = format!(
"{}/../geometry/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
}
fn run(content: &[u8]) -> ProcessingResult {
process_geometry_streaming_filtered_with_options(
content,
OpeningFilterMode::Default,
StreamingOptions::default(),
|_, _, _| {},
|_| {},
|_| {},
)
}
fn world_vertices(m: &MeshData) -> Vec<[f64; 3]> {
let n = m.positions.len() / 3;
(0..n)
.map(|v| {
[
m.origin[0] + m.positions[v * 3] as f64,
m.origin[1] + m.positions[v * 3 + 1] as f64,
m.origin[2] + m.positions[v * 3 + 2] as f64,
]
})
.collect()
}
fn apply(t: &[f32; 16], p: [f64; 3]) -> [f64; 3] {
let (x, y, z) = (p[0], p[1], p[2]);
let wx = t[0] as f64 * x + t[1] as f64 * y + t[2] as f64 * z + t[3] as f64;
let wy = t[4] as f64 * x + t[5] as f64 * y + t[6] as f64 * z + t[7] as f64;
let wz = t[8] as f64 * x + t[9] as f64 * y + t[10] as f64 * z + t[11] as f64;
let ww = t[12] as f64 * x + t[13] as f64 * y + t[14] as f64 * z + t[15] as f64;
[wx / ww, wy / ww, wz / ww]
}
fn max_vertex_error(a: &[[f64; 3]], b: &[[f64; 3]]) -> f64 {
assert_eq!(
a.len(),
b.len(),
"vertex count mismatch ({} vs {})",
a.len(),
b.len()
);
a.iter()
.zip(b)
.map(|(p, q)| ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt())
.fold(0.0f64, f64::max)
}
#[test]
fn multi_item_mapped_per_occurrence_target_collates_under_shared_template() {
let bytes = fixture_bytes("mapped_instances_multi_item.ifc");
let res = run(&bytes);
let mut groups: FxHashMap<u128, Vec<&MeshData>> = FxHashMap::default();
for m in &res.meshes {
if m.positions.is_empty() {
continue;
}
if let Some(im) = m.instance.as_ref() {
if im.instanceable {
groups.entry(im.rep_identity).or_default().push(m);
}
}
}
assert_eq!(
groups.len(),
2,
"expected 2 rep_identity templates (one per source solid), got {} — the \
per-occurrence-target re-hash was not removed",
groups.len()
);
for m in &res.meshes {
assert!(
m.origin.iter().all(|c| c.abs() < 1.0),
"unexpected RTC origin {:?}; reconstruction assumes rtc = 0",
m.origin
);
}
let rtc = [0.0f64; 3];
let tol = 1e-4;
for (rep, occ) in &groups {
assert_eq!(
occ.len(),
4,
"rep_identity {rep:#x}: expected 4 collated occurrences, got {}",
occ.len()
);
let mut seen: Vec<[f64; 16]> = Vec::new();
for m in occ {
let lt = m
.instance
.as_ref()
.unwrap()
.local_transform
.unwrap_or_else(|| {
panic!(
"rep_identity {rep:#x}: occurrence {} lost its MappingTarget \
(local_transform = None)",
m.express_id
)
});
assert!(
!seen.iter().any(|s| s == <),
"rep_identity {rep:#x}: two occurrences share a local_transform — the \
distinct per-occurrence targets were not recorded"
);
seen.push(lt);
}
let reference = occ[0];
let m_ref = compose_instance_world_row_major(reference.instance.as_ref().unwrap());
let ref_world = world_vertices(reference);
for m in &occ[1..] {
let m_k = compose_instance_world_row_major(m.instance.as_ref().unwrap());
let rel = instance_rel_row_major_f32(&m_k, &m_ref, rtc)
.expect("degenerate reference placement");
let recomposed: Vec<[f64; 3]> = ref_world.iter().map(|&p| apply(&rel, p)).collect();
let err = max_vertex_error(&recomposed, &world_vertices(m));
assert!(
err < tol,
"rep_identity {rep:#x}: occurrence {} world-vertex error {err:.3e} m exceeds {tol:.0e}",
m.express_id
);
}
}
}
#[test]
fn nested_mapped_composes_outer_and_inner_targets() {
let bytes = fixture_bytes("mapped_instances_nested.ifc");
let res = run(&bytes);
let mut groups: FxHashMap<u128, Vec<&MeshData>> = FxHashMap::default();
for m in &res.meshes {
if m.positions.is_empty() {
continue;
}
if let Some(im) = m.instance.as_ref() {
if im.instanceable {
groups.entry(im.rep_identity).or_default().push(m);
}
}
}
assert_eq!(
groups.len(),
1,
"expected 1 rep_identity template (single nested source solid), got {}",
groups.len()
);
for m in &res.meshes {
assert!(
m.origin.iter().all(|c| c.abs() < 1.0),
"unexpected RTC origin {:?}; reconstruction assumes rtc = 0",
m.origin
);
}
let rtc = [0.0f64; 3];
let tol = 1e-4;
let occ = groups.values().next().unwrap();
assert_eq!(occ.len(), 3, "expected 3 nested occurrences, got {}", occ.len());
let mut seen: Vec<[f64; 16]> = Vec::new();
for m in occ {
let lt = m
.instance
.as_ref()
.unwrap()
.local_transform
.expect("nested occurrence lost its composed MappingTarget");
assert!(
!seen.iter().any(|s| s == <),
"two nested occurrences share a composed local_transform"
);
seen.push(lt);
}
assert!(
seen.iter().any(|lt| lt[1].abs() > 0.5 || lt[4].abs() > 0.5),
"no composed local_transform carries the outer rotation"
);
let reference = occ[0];
let m_ref = compose_instance_world_row_major(reference.instance.as_ref().unwrap());
let ref_world = world_vertices(reference);
for m in &occ[1..] {
let m_k = compose_instance_world_row_major(m.instance.as_ref().unwrap());
let rel = instance_rel_row_major_f32(&m_k, &m_ref, rtc)
.expect("degenerate reference placement");
let recomposed: Vec<[f64; 3]> = ref_world.iter().map(|&p| apply(&rel, p)).collect();
let err = max_vertex_error(&recomposed, &world_vertices(m));
assert!(
err < tol,
"nested occurrence {} world-vertex error {err:.3e} m exceeds {tol:.0e}",
m.express_id
);
}
}