const MAX_MAPPED_ITEM_DEPTH: u32 = 32;
pub(crate) fn find_color_for_geometry(
geom_id: u32,
geometry_styles: &rustc_hash::FxHashMap<u32, [f32; 4]>,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<[f32; 4]> {
let mut visited = rustc_hash::FxHashMap::default();
find_color_for_geometry_at(geom_id, geometry_styles, decoder, 0, &mut visited)
}
fn find_color_for_geometry_at(
geom_id: u32,
geometry_styles: &rustc_hash::FxHashMap<u32, [f32; 4]>,
decoder: &mut ifc_lite_core::EntityDecoder,
depth: u32,
visited: &mut rustc_hash::FxHashMap<u32, u32>,
) -> Option<[f32; 4]> {
use ifc_lite_core::IfcType;
if let Some(&color) = geometry_styles.get(&geom_id) {
return Some(color);
}
if depth >= MAX_MAPPED_ITEM_DEPTH {
return None;
}
match visited.get(&geom_id) {
Some(&seen_at) if seen_at <= depth => return None,
_ => visited.insert(geom_id, depth),
};
let geom = decoder.decode_by_id(geom_id).ok()?;
if geom.ifc_type == IfcType::IfcMappedItem {
let map_source_id = geom.get_ref(0)?;
let rep_map = decoder.decode_by_id(map_source_id).ok()?;
let mapped_repr_id = rep_map.get_ref(1)?;
let mapped_repr = decoder.decode_by_id(mapped_repr_id).ok()?;
let items_attr = mapped_repr.get(3)?;
let items_list = items_attr.as_list()?;
for item in items_list {
if let Some(underlying_geom_id) = item.as_entity_ref() {
if let Some(color) = find_color_for_geometry_at(
underlying_geom_id,
geometry_styles,
decoder,
depth + 1,
visited,
) {
return Some(color);
}
}
}
}
None
}
pub(crate) fn resolve_element_color(
entity: &ifc_lite_core::DecodedEntity,
geometry_styles: &rustc_hash::FxHashMap<u32, [f32; 4]>,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<[f32; 4]> {
if geometry_styles.is_empty() {
return None;
}
if let Some(color) = walk_representation_for_direct_color(entity, geometry_styles, decoder) {
return Some(color);
}
geometry_styles.get(&entity.id).copied()
}
fn walk_representation_for_direct_color(
entity: &ifc_lite_core::DecodedEntity,
geometry_styles: &rustc_hash::FxHashMap<u32, [f32; 4]>,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<[f32; 4]> {
let repr_id = entity.get_ref(6)?;
let product_shape = decoder.decode_by_id(repr_id).ok()?;
let reprs_list = product_shape.get(2)?.as_list()?;
for repr_item in reprs_list {
let Some(shape_repr_id) = repr_item.as_entity_ref() else {
continue;
};
let Ok(shape_repr) = decoder.decode_by_id(shape_repr_id) else {
continue;
};
let Some(items_list) = shape_repr.get(3).and_then(|a| a.as_list()) else {
continue;
};
for geom_item in items_list {
let Some(geom_id) = geom_item.as_entity_ref() else {
continue;
};
if let Some(color) = find_color_for_geometry(geom_id, geometry_styles, decoder) {
return Some(color);
}
}
}
None
}
#[cfg(test)]
mod resolve_element_color_tests {
use super::resolve_element_color;
use ifc_lite_core::{build_entity_index, EntityDecoder};
use rustc_hash::FxHashMap;
const WALL_IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('Test'),'2;1');
FILE_NAME('test','2026-05-27',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCWALL('w',$,'Wall',$,$,$,#2,$,.NOTDEFINED.);
#2=IFCPRODUCTDEFINITIONSHAPE($,$,(#3));
#3=IFCSHAPEREPRESENTATION(#4,'Body','SweptSolid',(#5));
#4=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#6,$);
#5=IFCEXTRUDEDAREASOLID(#7,#6,#8,3000.);
#6=IFCAXIS2PLACEMENT3D(#9,$,$);
#7=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,200.,4000.);
#8=IFCDIRECTION((0.,0.,1.));
#9=IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;
"#;
fn decode_wall() -> (EntityDecoder<'static>, ifc_lite_core::DecodedEntity) {
let content: &'static str = Box::leak(WALL_IFC.to_string().into_boxed_str());
let idx = build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, idx);
let wall = decoder.decode_by_id(1).expect("decode wall #1");
(decoder, wall)
}
#[test]
fn empty_geometry_styles_returns_none() {
let (mut decoder, wall) = decode_wall();
let styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
assert_eq!(resolve_element_color(&wall, &styles, &mut decoder), None);
}
#[test]
fn direct_geometry_item_color_resolves_via_rep_walk() {
let (mut decoder, wall) = decode_wall();
let mut styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
styles.insert(5, [0.1, 0.8, 0.2, 1.0]);
assert_eq!(
resolve_element_color(&wall, &styles, &mut decoder),
Some([0.1, 0.8, 0.2, 1.0]),
);
}
#[test]
fn element_id_keyed_material_color_resolves_via_fallback() {
let (mut decoder, wall) = decode_wall();
let mut styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
styles.insert(1, [0.8, 0.2, 0.1, 1.0]);
assert_eq!(
resolve_element_color(&wall, &styles, &mut decoder),
Some([0.8, 0.2, 0.1, 1.0]),
);
}
#[test]
fn direct_color_wins_over_material_fallback() {
let (mut decoder, wall) = decode_wall();
let mut styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
styles.insert(1, [0.8, 0.2, 0.1, 1.0]); styles.insert(5, [0.1, 0.8, 0.2, 1.0]); assert_eq!(
resolve_element_color(&wall, &styles, &mut decoder),
Some([0.1, 0.8, 0.2, 1.0]),
"direct geometry-item colour must win over material fallback",
);
}
#[test]
fn unrelated_colors_yield_none() {
let (mut decoder, wall) = decode_wall();
let mut styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
styles.insert(999, [0.5, 0.5, 0.5, 1.0]);
assert_eq!(resolve_element_color(&wall, &styles, &mut decoder), None);
}
}
#[cfg(test)]
#[path = "color_cycle_tests.rs"]
mod find_color_for_geometry_cycle_tests;