use ifc_lite_core::{build_entity_index, has_geometry_by_name, EntityDecoder, EntityScanner};
use ifc_lite_geometry::{collate_refs, GeometryRouter, InstanceMeshRef, InstanceMeta};
use ifc_lite_processing::element::{
produce_element_meshes, ElementJobKind, ElementMeshJob, MeshProductionContext,
MeshProductionOptions,
};
use ifc_lite_processing::MeshData;
use rustc_hash::FxHashMap;
use std::sync::Arc;
type Job = (u32, usize, usize);
type Plan = FxHashMap<u32, (u32, u32)>;
fn synthetic_bytes() -> Vec<u8> {
let path = format!(
"{}/../geometry/tests/fixtures/mapped_instances_synthetic.ifc",
env!("CARGO_MANIFEST_DIR")
);
std::fs::read(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
}
fn scan(
content: &[u8],
index: &Arc<ifc_lite_core::EntityIndex>,
) -> (Vec<Job>, Plan) {
let mut jobs: Vec<(u32, usize, usize)> = Vec::new();
let mut counts: FxHashMap<u32, (u32, u32)> = FxHashMap::default();
let mut decoder = EntityDecoder::with_arc_index(content, index.clone());
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCMAPPEDITEM" {
if let Ok(ent) = decoder.decode_at_with_id(id, start, end) {
if let Some(src) = ent.get_ref(0) {
counts
.entry(src)
.and_modify(|(c, t)| {
*c += 1;
if id < *t {
*t = id;
}
})
.or_insert((1, id));
}
}
} else if has_geometry_by_name(type_name) {
jobs.push((id, start, end));
}
}
let plan: FxHashMap<u32, (u32, u32)> =
counts.into_iter().filter(|(_, (c, _))| *c >= 2).collect();
(jobs, plan)
}
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_err(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 produce(
content: &[u8],
index: &Arc<ifc_lite_core::EntityIndex>,
jobs: &[Job],
router: &GeometryRouter,
) -> (Vec<MeshData>, Vec<ifc_lite_processing::RawInstanceOccurrence>) {
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::default();
let mut meshes = Vec::new();
let mut occ = 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,
);
meshes.extend(produced.meshes);
occ.extend(produced.instance_occurrences);
}
(meshes, occ)
}
#[test]
fn browser_batch_local_instanced_equals_flat_synthetic() {
let content = synthetic_bytes();
let index = Arc::new(build_entity_index(&content));
let (jobs, plan) = scan(&content, &index);
assert!(!plan.is_empty(), "fixture has no repeated mapped source");
let flat_router = GeometryRouter::with_scale(1.0);
let (flat_meshes, flat_occ) = produce(&content, &index, &jobs, &flat_router);
assert!(flat_occ.is_empty(), "flat run must not emit don't-bake occurrences");
let mut flat_by_id: FxHashMap<u32, Vec<[f64; 3]>> = 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_with(|| world_vertices(m));
}
}
let mut bl_router = GeometryRouter::with_scale(1.0);
bl_router.enable_shared_mapped_item_cache(GeometryRouter::new_mapped_item_cache());
bl_router.enable_output_instancing(Arc::new(plan));
bl_router.set_instancing_batch_local(true);
let (bl_meshes, bl_occ) = produce(&content, &index, &jobs, &bl_router);
assert!(!bl_occ.is_empty(), "batch-local run emitted no don't-bake occurrences");
let occ_metas: Vec<InstanceMeta> = bl_occ
.iter()
.map(|o| InstanceMeta {
transform: o.world_transform,
local_transform: None,
canonical_transform: None,
rep_identity: o.rep_identity,
instanceable: true,
})
.collect();
let mut refs: Vec<InstanceMeshRef> = bl_meshes
.iter()
.map(|m| InstanceMeshRef {
positions: &m.positions,
normals: &m.normals,
indices: &m.indices,
origin: m.origin,
instance_meta: m.instance.as_ref(),
entity_id: m.express_id,
color: m.color,
})
.collect();
for (o, meta) in bl_occ.iter().zip(occ_metas.iter()) {
refs.push(InstanceMeshRef {
positions: &[],
normals: &[],
indices: &[],
origin: [0.0, 0.0, 0.0],
instance_meta: Some(meta),
entity_id: o.express_id,
color: o.color,
});
}
let collated = collate_refs(&refs, 2, [0.0, 0.0, 0.0]);
assert!(!collated.templates.is_empty(), "no instanced template formed");
let tol = 1e-6;
let mut checked = 0usize;
for tmpl in &collated.templates {
let template_world = world_vertices(&bl_meshes[tmpl.template_index]);
for o in &tmpl.occurrences {
let entity_id = refs[o.mesh_index].entity_id;
let flat_world = flat_by_id
.get(&entity_id)
.unwrap_or_else(|| panic!("occurrence {entity_id} has no flat counterpart"));
let recomposed: Vec<[f64; 3]> =
template_world.iter().map(|&p| apply(&o.transform, p)).collect();
let err = max_err(&recomposed, flat_world);
assert!(err < tol, "occurrence {entity_id}: world-vertex error {err:.3e} m exceeds 1um");
checked += 1;
}
}
let flat_n = flat_by_id.len();
let bl_n = bl_meshes.iter().filter(|m| m.geometry_class == 0 && !m.positions.is_empty()).count();
let bl_verts: usize = bl_meshes.iter().map(|m| m.positions.len() / 3).sum();
let flat_verts: usize = flat_meshes.iter().map(|m| m.positions.len() / 3).sum();
assert!(bl_n < flat_n, "batch-local materialized ({bl_n}) not fewer than flat ({flat_n})");
assert_eq!(checked, flat_n, "collated occurrences must cover every flat occurrence");
eprintln!(
"[#1623 P3] synthetic batch-local: flat = {flat_n} meshes / {flat_verts} verts; \
batch-local = {bl_n} materialized + {} shard occurrences / {bl_verts} materialized verts \
(reduction: {} meshes, {} verts)",
bl_occ.len(),
flat_n - bl_n,
flat_verts.saturating_sub(bl_verts),
);
}