use ifc_lite_processing::{
process_geometry_streaming_filtered_with_options, InstanceRecord, MeshData, OpeningFilterMode,
ProcessingResult, StreamingOptions,
};
use rustc_hash::FxHashMap;
fn sample_bytes() -> Vec<u8> {
let path = format!(
"{}/../../apps/viewer/public/samples/hello-wall.ifc",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
}
fn synthetic_bytes() -> Vec<u8> {
fixture_bytes("mapped_instances_synthetic.ifc")
}
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], enable_instancing: bool) -> ProcessingResult {
process_geometry_streaming_filtered_with_options(
content,
OpeningFilterMode::Default,
StreamingOptions {
enable_instancing,
..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)
}
fn assert_instanced_matches_flat(bytes: &[u8], label: &str) {
let flat = run(bytes, false);
let inst = run(bytes, true);
assert!(
!inst.instances.is_empty(),
"{label}: instancing produced no InstanceRecords — the don't-bake path did not fire \
(sample has no repeated single-solid mapped source?)"
);
let mut flat_by_id: FxHashMap<u32, &MeshData> = FxHashMap::default();
for m in &flat.meshes {
if m.geometry_class == 0 && !m.positions.is_empty() {
flat_by_id.entry(m.express_id).or_insert(m);
}
}
let mut inst_mesh_by_id: FxHashMap<u32, &MeshData> = FxHashMap::default();
for m in &inst.meshes {
if !m.positions.is_empty() {
inst_mesh_by_id.entry(m.express_id).or_insert(m);
}
}
let tol = 1e-6; let mut checked_instances = 0usize;
for rec in &inst.instances {
let template = inst_mesh_by_id.get(&rec.template_express_id).unwrap_or_else(|| {
panic!(
"instance {} references template {} not present in instanced meshes",
rec.express_id, rec.template_express_id
)
});
let flat_occ = flat_by_id.get(&rec.express_id).unwrap_or_else(|| {
panic!("instance {} has no flat counterpart mesh", rec.express_id)
});
let template_world = world_vertices(template);
let recomposed: Vec<[f64; 3]> =
template_world.iter().map(|&p| apply(&rec.transform, p)).collect();
let flat_world = world_vertices(flat_occ);
let err = max_vertex_error(&recomposed, &flat_world);
assert!(
err < tol,
"{label}: instance {} (template {}): world-vertex error {err:.3e} m exceeds 1um",
rec.express_id, rec.template_express_id
);
checked_instances += 1;
}
assert_eq!(checked_instances, inst.instances.len());
for rec in &inst.instances {
let tid = rec.template_express_id;
let inst_t = inst_mesh_by_id[&tid];
let flat_t = flat_by_id
.get(&tid)
.unwrap_or_else(|| panic!("template {tid} missing from flat meshes"));
let err = max_vertex_error(&world_vertices(inst_t), &world_vertices(flat_t));
assert!(
err < tol,
"{label}: template {tid}: instanced vs flat world-vertex error {err:.3e} m exceeds 1um"
);
}
let inst_ids: std::collections::HashSet<u32> = inst
.meshes
.iter()
.map(|m| m.express_id)
.chain(inst.instances.iter().map(|r| r.express_id))
.collect();
for m in &flat.meshes {
if m.geometry_class == 0 && !m.positions.is_empty() {
assert!(
inst_ids.contains(&m.express_id),
"{label}: flat occurrence {} is absent from the instanced output (geometry lost)",
m.express_id
);
}
}
}
fn assert_reduction(bytes: &[u8], label: &str) {
let flat = run(bytes, false);
let inst = run(bytes, true);
let flat_meshes = flat.meshes.iter().filter(|m| !m.positions.is_empty()).count();
let inst_meshes = inst.meshes.iter().filter(|m| !m.positions.is_empty()).count();
let flat_verts: usize = flat.meshes.iter().map(|m| m.positions.len() / 3).sum();
let inst_verts: usize = inst.meshes.iter().map(|m| m.positions.len() / 3).sum();
assert!(!inst.instances.is_empty(), "{label}: don't-bake did not fire");
assert!(
inst_meshes < flat_meshes,
"{label}: instanced materialized meshes ({inst_meshes}) not fewer than flat ({flat_meshes})"
);
assert_eq!(
inst_meshes + inst.instances.len(),
flat_meshes,
"{label}: templates + instances must equal the flat occurrence count"
);
eprintln!(
"[#1623 P2] {label}: flat = {flat_meshes} meshes / {flat_verts} verts; \
instanced = {inst_meshes} templates + {} instance records / {inst_verts} materialized verts \
(materialize reduction: {} meshes, {} verts)",
inst.instances.len(),
flat_meshes - inst_meshes,
flat_verts.saturating_sub(inst_verts),
);
let _: fn(&InstanceRecord) -> u32 = |r| r.express_id;
}
fn mesh_key(m: &MeshData) -> Vec<u8> {
let mut k = Vec::new();
k.extend_from_slice(&m.express_id.to_le_bytes());
k.extend_from_slice(m.ifc_type.as_bytes());
k.push(0);
k.extend_from_slice(&m.geometry_item_id.unwrap_or(u32::MAX).to_le_bytes());
k.push(m.geometry_class);
for c in m.color {
k.extend_from_slice(&c.to_bits().to_le_bytes());
}
for o in m.origin {
k.extend_from_slice(&o.to_bits().to_le_bytes());
}
for p in &m.positions {
k.extend_from_slice(&p.to_bits().to_le_bytes());
}
for n in &m.normals {
k.extend_from_slice(&n.to_bits().to_le_bytes());
}
for i in &m.indices {
k.extend_from_slice(&i.to_le_bytes());
}
k
}
fn mesh_stream(meshes: &[MeshData]) -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = meshes
.iter()
.filter(|m| !m.positions.is_empty())
.map(mesh_key)
.collect();
v.sort();
v
}
fn distinct_colors_by_expr(meshes: &[MeshData]) -> FxHashMap<u32, std::collections::HashSet<[u32; 4]>> {
let mut out: FxHashMap<u32, std::collections::HashSet<[u32; 4]>> = FxHashMap::default();
for m in meshes.iter().filter(|m| !m.positions.is_empty()) {
let bits = [
m.color[0].to_bits(),
m.color[1].to_bits(),
m.color[2].to_bits(),
m.color[3].to_bits(),
];
out.entry(m.express_id).or_default().insert(bits);
}
out
}
fn assert_georef_routes_to_flat(bytes: &[u8], label: &str) {
let flat = run(bytes, false);
let inst = run(bytes, true);
assert_eq!(
inst.mesh_coordinate_space.as_deref(),
Some("site_local"),
"{label}: expected the site-local coordinate tier (the georef path under test)"
);
assert!(
inst.instances.is_empty(),
"{label}: site-local model produced {} InstanceRecords — the guard did not route to flat",
inst.instances.len()
);
let occ = flat.meshes.iter().filter(|m| !m.positions.is_empty()).count();
assert!(
occ >= 2,
"{label}: expected a repeated mapped source (>=2 occurrences), got {occ}"
);
assert_eq!(
mesh_stream(&flat.meshes),
mesh_stream(&inst.meshes),
"{label}: site-local instanced-ON MeshData stream differs from instanced-OFF (byte-identity broken)"
);
eprintln!("[#1623 P2 georef] {label}: site_local, {occ} occurrences, 0 instance records, ON==OFF byte-identical");
}
fn assert_indexed_colour_palette_preserved(bytes: &[u8], label: &str) {
let flat = run(bytes, false);
let inst = run(bytes, true);
assert_eq!(
inst.mesh_coordinate_space.as_deref(),
Some("raw_ifc"),
"{label}: expected the origin tier so the don't-bake plan actually arms (only the \
indexed-colour guard should keep this source flat)"
);
assert!(
inst.instances.is_empty(),
"{label}: indexed-colour source produced {} InstanceRecords — the palette guard did not fire",
inst.instances.len()
);
for (run_label, res) in [("flat", &flat), ("instanced", &inst)] {
for (expr, colors) in distinct_colors_by_expr(&res.meshes) {
assert!(
colors.len() >= 2,
"{label} ({run_label}): occurrence {expr} has {} distinct colour(s); the #858 \
palette split must yield >=2 (palette collapsed?)",
colors.len()
);
}
}
assert_eq!(
mesh_stream(&flat.meshes),
mesh_stream(&inst.meshes),
"{label}: indexed-colour instanced-ON MeshData stream differs from instanced-OFF \
(palette not preserved bit-for-bit)"
);
eprintln!(
"[#858 indexed-colour] {label}: {} occurrence meshes, per-triangle palette preserved, ON==OFF byte-identical",
inst.meshes.iter().filter(|m| !m.positions.is_empty()).count()
);
}
#[test]
fn georef_translated_site_routes_to_flat_byte_identical() {
assert_georef_routes_to_flat(
&fixture_bytes("mapped_instances_site_translated.ifc"),
"site-translated",
);
}
#[test]
fn georef_rotated_site_routes_to_flat_byte_identical() {
assert_georef_routes_to_flat(
&fixture_bytes("mapped_instances_site_rotated.ifc"),
"site-rotated",
);
}
#[test]
fn indexed_colour_source_palette_survives_instancing() {
assert_indexed_colour_palette_preserved(
&fixture_bytes("mapped_instances_indexed_colour.ifc"),
"indexed-colour",
);
}
fn assert_uniform_indexed_colour_instances(bytes: &[u8], label: &str, expected: [f32; 4]) {
assert_instanced_matches_flat(bytes, label);
let flat = run(bytes, false);
let inst = run(bytes, true);
assert_eq!(
inst.mesh_coordinate_space.as_deref(),
Some("raw_ifc"),
"{label}: expected the origin tier so the don't-bake plan arms (only the \
indexed-colour guard could keep this source flat)"
);
let expected_bits = [
expected[0].to_bits(),
expected[1].to_bits(),
expected[2].to_bits(),
expected[3].to_bits(),
];
for (run_label, res) in [("flat", &flat), ("instanced", &inst)] {
for (expr, colors) in distinct_colors_by_expr(&res.meshes) {
assert_eq!(
colors.len(),
1,
"{label} ({run_label}): occurrence {expr} has {} distinct colour(s); a uniform \
indexed-colour source must stay single-colour",
colors.len()
);
assert!(
colors.contains(&expected_bits),
"{label} ({run_label}): occurrence {expr} colour != dominant {expected:?}"
);
}
}
eprintln!(
"[#1807 uniform indexed-colour] {label}: {} instance record(s), single dominant colour preserved",
inst.instances.len()
);
}
#[test]
fn uniform_indexed_colour_source_instances_keeping_dominant_colour() {
assert_uniform_indexed_colour_instances(
&fixture_bytes("mapped_instances_indexed_colour_uniform.ifc"),
"uniform-indexed-colour",
[1.0, 0.0, 0.0, 1.0],
);
}
#[test]
fn instanced_world_triangles_equal_flat_hello_wall() {
assert_instanced_matches_flat(&sample_bytes(), "hello-wall");
}
#[test]
fn instanced_world_triangles_equal_flat_synthetic() {
assert_instanced_matches_flat(&synthetic_bytes(), "synthetic-64");
}
#[test]
fn instancing_reduces_materialized_meshes_hello_wall() {
assert_reduction(&sample_bytes(), "hello-wall");
}
#[test]
fn instancing_reduces_materialized_meshes_synthetic() {
assert_reduction(&synthetic_bytes(), "synthetic-64");
}