use ifc_lite_geometry::{bake_source_at_world, SharedMappedItemCache};
use ifc_lite_processing::{MeshData, RawInstanceOccurrence};
use rustc_hash::FxHashMap;
pub(super) struct TemplateInfo {
pub eligible: bool,
}
pub(super) struct ShardOccurrence {
pub entity_id: u32,
pub color: [f32; 4],
pub rep_identity: u128,
pub world_transform: [f64; 16],
}
pub(super) fn resolve_batch_occurrences(
raw: Vec<RawInstanceOccurrence>,
template_by_rep: &FxHashMap<u128, TemplateInfo>,
mapped_item_cache: &SharedMappedItemCache,
rtc: [f64; 3],
min_occurrences: usize,
recovered_flats: &mut Vec<MeshData>,
) -> Vec<ShardOccurrence> {
if raw.is_empty() {
return Vec::new();
}
let mut by_rep: FxHashMap<u128, Vec<RawInstanceOccurrence>> = FxHashMap::default();
for occ in raw {
by_rep.entry(occ.rep_identity).or_default().push(occ);
}
let mut groups: Vec<(u128, Vec<RawInstanceOccurrence>)> = by_rep.into_iter().collect();
groups.sort_by_key(|(rep, _)| *rep);
let mut shard: Vec<ShardOccurrence> = Vec::new();
for (rep, occs) in groups {
let keep = template_by_rep
.get(&rep)
.is_some_and(|t| t.eligible)
&& (occs.len() + 1) >= min_occurrences;
if keep {
for occ in occs {
shard.push(ShardOccurrence {
entity_id: occ.express_id,
color: occ.color,
rep_identity: rep,
world_transform: occ.world_transform,
});
}
} else {
recover_flat(rep, &occs, mapped_item_cache, rtc, recovered_flats);
}
}
shard.sort_by_key(|o| (o.entity_id, o.rep_identity));
shard
}
fn recover_flat(
rep: u128,
occs: &[RawInstanceOccurrence],
mapped_item_cache: &SharedMappedItemCache,
rtc: [f64; 3],
out: &mut Vec<MeshData>,
) {
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;
};
for occ in occs {
let (positions, normals, indices) =
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(),
),
);
}
}
#[cfg(test)]
mod resolve_batch_occurrences_tests {
use super::*;
use std::sync::{Arc, Mutex};
const MIN_OCCURRENCES: usize = 4;
const REP: u128 = 42;
fn make_occ(express_id: 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,
],
}
}
fn empty_cache() -> SharedMappedItemCache {
Arc::new(Mutex::new(FxHashMap::default()))
}
fn shard_len_for(occ_count: u32) -> usize {
let raw: Vec<RawInstanceOccurrence> = (0..occ_count).map(make_occ).collect();
let mut template_by_rep = FxHashMap::default();
template_by_rep.insert(REP, TemplateInfo { eligible: true });
let mut recovered_flats = Vec::new();
let shard = resolve_batch_occurrences(
raw,
&template_by_rep,
&empty_cache(),
[0.0, 0.0, 0.0],
MIN_OCCURRENCES,
&mut recovered_flats,
);
shard.len()
}
#[test]
fn one_below_threshold_recovers_flat_not_shard() {
let occ_count = (MIN_OCCURRENCES - 2) as u32;
assert_eq!(shard_len_for(occ_count), 0);
}
#[test]
fn exactly_at_threshold_is_kept() {
let occ_count = (MIN_OCCURRENCES - 1) as u32;
assert_eq!(shard_len_for(occ_count), occ_count as usize);
}
#[test]
fn one_above_threshold_is_kept() {
let occ_count = MIN_OCCURRENCES as u32;
assert_eq!(shard_len_for(occ_count), occ_count as usize);
}
}