use crate::types::mesh::{InstanceRecord, MeshData, RawInstanceOccurrence};
use rustc_hash::FxHashMap;
pub(super) fn finalize_instances(
raw: Vec<RawInstanceOccurrence>,
meshes: &mut Vec<MeshData>,
mapped_item_cache: &ifc_lite_geometry::SharedMappedItemCache,
rtc: [f64; 3],
) -> Vec<InstanceRecord> {
if raw.is_empty() {
return Vec::new();
}
let mut by_source: FxHashMap<u128, Vec<RawInstanceOccurrence>> = FxHashMap::default();
for occ in raw {
by_source.entry(occ.rep_identity).or_default().push(occ);
}
let mut template_by_rep: FxHashMap<u128, usize> = FxHashMap::default();
for (i, m) in meshes.iter().enumerate() {
if m.positions.is_empty() {
continue;
}
if let Some(im) = m.instance.as_ref() {
if im.instanceable {
template_by_rep.entry(im.rep_identity).or_insert(i);
}
}
}
let mut records: Vec<InstanceRecord> = Vec::new();
let mut orphan_flats: Vec<MeshData> = Vec::new();
let mut groups: Vec<(u128, Vec<RawInstanceOccurrence>)> = by_source.into_iter().collect();
groups.sort_by_key(|(rep, _)| *rep);
for (rep, occs) in groups {
if let Some(&t_idx) = template_by_rep.get(&rep) {
if let Some(im) = meshes[t_idx].instance.as_ref() {
let m_ref = ifc_lite_geometry::compose_instance_world_row_major(im);
let template_express_id = meshes[t_idx].express_id;
let mut batch = Vec::with_capacity(occs.len());
let mut all_ok = true;
for occ in &occs {
match ifc_lite_geometry::instance_rel_row_major_f32(
&occ.world_transform,
&m_ref,
rtc,
) {
Some(transform) => batch.push(InstanceRecord {
express_id: occ.express_id,
ifc_type: occ.ifc_type.clone(),
global_id: occ.global_id.clone(),
name: occ.name.clone(),
presentation_layer: occ.presentation_layer.clone(),
color: occ.color,
template_express_id,
rep_identity: rep,
transform,
geometry_item_id: occ.geometry_item_id,
}),
None => {
all_ok = false;
break;
}
}
}
if all_ok {
records.extend(batch);
continue;
}
}
}
if !recover_occurrences_flat(rep, &occs, mapped_item_cache, rtc, &mut orphan_flats) {
tracing::warn!(
source_id = rep as u32,
occurrences = occs.len(),
"instancing: orphan mapped source missing from registry; occurrences dropped"
);
}
}
records.sort_by_key(|r| (r.express_id, r.rep_identity));
orphan_flats.sort_by_key(|m| m.express_id);
meshes.append(&mut orphan_flats);
records
}
pub fn recover_occurrences_flat(
rep: u128,
occs: &[RawInstanceOccurrence],
mapped_item_cache: &ifc_lite_geometry::SharedMappedItemCache,
rtc: [f64; 3],
out: &mut Vec<MeshData>,
) -> bool {
let source_id = rep as u32;
let source = mapped_item_cache
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&source_id)
.cloned();
let Some(source) = source else {
return false;
};
for occ in occs {
let (positions, normals, indices) =
ifc_lite_geometry::bake_source_at_world(&source, &occ.world_transform, rtc);
if positions.is_empty() || indices.is_empty() {
continue;
}
out.push(
MeshData::new(
occ.express_id,
occ.ifc_type.clone(),
positions,
normals,
indices,
occ.color,
)
.with_element_metadata(
occ.global_id.clone(),
occ.name.clone(),
occ.presentation_layer.clone(),
)
.with_style_metadata(None, occ.geometry_item_id, false),
);
}
true
}
#[cfg(test)]
mod orphan_recovery_tests {
use super::*;
use std::sync::{Arc, Mutex};
const REP: u128 = 4242;
const ITEM_ID: u32 = 4638;
fn occurrence(express_id: u32, geometry_item_id: Option<u32>) -> RawInstanceOccurrence {
RawInstanceOccurrence {
express_id,
ifc_type: "IfcFlowFitting".to_string(),
global_id: None,
name: None,
presentation_layer: None,
color: [1.0, 1.0, 1.0, 1.0],
rep_identity: REP,
world_transform: [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
],
geometry_item_id,
}
}
fn cache_with_source() -> ifc_lite_geometry::SharedMappedItemCache {
let mut source = ifc_lite_geometry::Mesh::new();
source.positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
source.normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
source.indices = vec![0, 1, 2];
let map: FxHashMap<u32, Arc<ifc_lite_geometry::Mesh>> =
[(REP as u32, Arc::new(source))].into_iter().collect();
Arc::new(Mutex::new(map))
}
#[test]
fn an_orphan_recovered_flat_reports_its_item_id() {
let mut meshes: Vec<MeshData> = Vec::new();
let records = finalize_instances(
vec![occurrence(7, Some(ITEM_ID))],
&mut meshes,
&cache_with_source(),
[0.0, 0.0, 0.0],
);
assert!(records.is_empty(), "an orphan emits no InstanceRecord");
assert_eq!(meshes.len(), 1, "the orphan must be recovered flat, not dropped");
assert_eq!(
meshes[0].geometry_item_id,
Some(ITEM_ID),
"the recovered orphan lost the item id the occurrence carried"
);
assert_eq!(meshes[0].material_id, None);
}
#[test]
fn an_orphan_with_no_item_stays_absent_rather_than_reporting_zero() {
let mut meshes: Vec<MeshData> = Vec::new();
finalize_instances(
vec![occurrence(7, None)],
&mut meshes,
&cache_with_source(),
[0.0, 0.0, 0.0],
);
assert_eq!(meshes.len(), 1);
assert_eq!(meshes[0].geometry_item_id, None);
assert_eq!(meshes[0].material_id, None);
}
}