use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use rustc_hash::FxHashMap;
use super::TRANSPARENCY_ALPHA_THRESHOLD;
const MAX_MATERIAL_RESOLVE_DEPTH: u8 = 4;
pub fn build_element_material_colors(
material_def_reprs: &FxHashMap<u32, Vec<u32>>,
orphan_styled_items: &FxHashMap<u32, [f32; 4]>,
element_to_material: &FxHashMap<u32, u32>,
decoder: &mut EntityDecoder,
) -> FxHashMap<u32, Vec<[f32; 4]>> {
if element_to_material.is_empty() || orphan_styled_items.is_empty() {
return FxHashMap::default();
}
let material_styles = build_material_style_index(material_def_reprs, orphan_styled_items, decoder);
if material_styles.is_empty() {
return FxHashMap::default();
}
let mut out: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
for (&element_id, &material_select_id) in element_to_material {
let mut colors: Vec<[f32; 4]> = Vec::new();
for material_id in resolve_material_ids(material_select_id, decoder) {
if let Some(mat_colors) = material_styles.get(&material_id) {
colors.extend(mat_colors);
}
}
if !colors.is_empty() {
out.insert(element_id, colors);
}
}
out
}
pub fn flatten_material_color_index(
material_styles: &FxHashMap<u32, Vec<[f32; 4]>>,
) -> FxHashMap<u32, [f32; 4]> {
material_styles
.iter()
.filter_map(|(&mat_id, colors)| pick_opaque_first(colors).map(|c| (mat_id, c)))
.collect()
}
pub fn pick_opaque_first(colors: &[[f32; 4]]) -> Option<[f32; 4]> {
if colors.is_empty() {
return None;
}
Some(
colors
.iter()
.find(|c| c[3] >= TRANSPARENCY_ALPHA_THRESHOLD)
.copied()
.unwrap_or(colors[0]),
)
}
pub fn pick_material_style_for_submesh(
colors: &[[f32; 4]],
prefer_transparent: bool,
) -> Option<[f32; 4]> {
if colors.is_empty() {
return None;
}
let matched = if prefer_transparent {
colors.iter().find(|c| c[3] < TRANSPARENCY_ALPHA_THRESHOLD)
} else {
colors.iter().find(|c| c[3] >= TRANSPARENCY_ALPHA_THRESHOLD)
};
Some(matched.copied().unwrap_or(colors[0]))
}
pub fn resolve_submesh_color(
direct_color: Option<[f32; 4]>,
material_colors: Option<&[[f32; 4]]>,
mat_color_idx: &mut usize,
element_color: [f32; 4],
) -> [f32; 4] {
if let Some(color) = direct_color {
return color;
}
if let Some(colors) = material_colors {
let prefer_transparent = *mat_color_idx % 2 == 0;
*mat_color_idx += 1;
if let Some(color) = pick_material_style_for_submesh(colors, prefer_transparent) {
return color;
}
}
element_color
}
pub fn build_material_style_index(
material_def_reprs: &FxHashMap<u32, Vec<u32>>,
orphan_styled_items: &FxHashMap<u32, [f32; 4]>,
decoder: &mut EntityDecoder,
) -> FxHashMap<u32, Vec<[f32; 4]>> {
let mut material_styles: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
for (&material_id, styled_repr_ids) in material_def_reprs {
for &styled_repr_id in styled_repr_ids {
let Ok(styled_repr) = decoder.decode_by_id(styled_repr_id) else {
continue;
};
for styled_item_id in extract_refs_from_list(&styled_repr, 3) {
if let Some(&color) = orphan_styled_items.get(&styled_item_id) {
material_styles.entry(material_id).or_default().push(color);
}
}
}
}
material_styles
}
pub fn resolve_material_ids(material_select_id: u32, decoder: &mut EntityDecoder) -> Vec<u32> {
resolve_material_ids_inner(material_select_id, decoder, 0)
}
fn resolve_material_ids_inner(
material_select_id: u32,
decoder: &mut EntityDecoder,
depth: u8,
) -> Vec<u32> {
if depth >= MAX_MATERIAL_RESOLVE_DEPTH {
return vec![];
}
let Ok(entity) = decoder.decode_by_id(material_select_id) else {
return vec![];
};
match entity.ifc_type {
IfcType::IfcMaterial => vec![material_select_id],
IfcType::IfcMaterialList => extract_refs_from_list(&entity, 0),
IfcType::IfcMaterialLayerSetUsage => entity
.get_ref(0)
.map(|id| resolve_material_ids_inner(id, decoder, depth + 1))
.unwrap_or_default(),
IfcType::IfcMaterialLayerSet => extract_nested_material_ids(&entity, 0, 0, decoder),
IfcType::IfcMaterialConstituentSet => extract_nested_material_ids(&entity, 2, 2, decoder),
IfcType::IfcMaterialProfileSet => extract_nested_material_ids(&entity, 2, 2, decoder),
IfcType::IfcMaterialProfileSetUsage | IfcType::IfcMaterialProfileSetUsageTapering => entity
.get_ref(0)
.map(|id| resolve_material_ids_inner(id, decoder, depth + 1))
.unwrap_or_default(),
_ => vec![],
}
}
fn extract_nested_material_ids(
entity: &DecodedEntity,
container_list_attr_idx: usize,
material_attr_idx: usize,
decoder: &mut EntityDecoder,
) -> Vec<u32> {
let mut materials = Vec::new();
for container_id in extract_refs_from_list(entity, container_list_attr_idx) {
if let Ok(container) = decoder.decode_by_id(container_id) {
if let Some(mat_id) = container.get_ref(material_attr_idx) {
materials.push(mat_id);
}
}
}
materials
}
fn extract_refs_from_list(entity: &DecodedEntity, index: usize) -> Vec<u32> {
entity
.get(index)
.and_then(|attr| attr.as_list())
.map(|list| list.iter().filter_map(|v| v.as_entity_ref()).collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
const OPAQUE: [f32; 4] = [0.6, 0.6, 0.6, 1.0];
const GLASS: [f32; 4] = [0.5, 0.7, 0.9, 0.4];
const ELEMENT: [f32; 4] = [0.1, 0.1, 0.1, 1.0];
#[test]
fn submesh_direct_style_wins_without_touching_counter() {
let mut idx = 0usize;
let direct = [0.9, 0.2, 0.2, 1.0];
let colors = [OPAQUE, GLASS];
assert_eq!(
resolve_submesh_color(Some(direct), Some(&colors), &mut idx, ELEMENT),
direct
);
assert_eq!(idx, 0, "the alternation counter must not advance when a direct style wins");
}
#[test]
fn submesh_material_alternates_transparent_opaque() {
let colors = [OPAQUE, GLASS];
let mut idx = 0usize;
assert_eq!(resolve_submesh_color(None, Some(&colors), &mut idx, ELEMENT), GLASS);
assert_eq!(resolve_submesh_color(None, Some(&colors), &mut idx, ELEMENT), OPAQUE);
assert_eq!(idx, 2, "counter advances once per material-resolved sub-mesh");
}
#[test]
fn submesh_falls_back_to_element_color() {
let mut idx = 0usize;
assert_eq!(resolve_submesh_color(None, None, &mut idx, ELEMENT), ELEMENT);
assert_eq!(idx, 0, "no material list → counter untouched");
let empty: [[f32; 4]; 0] = [];
assert_eq!(resolve_submesh_color(None, Some(&empty), &mut idx, ELEMENT), ELEMENT);
}
}