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]> {
use ifc_lite_core::IfcType;
if let Some(&color) = geometry_styles.get(&geom_id) {
return Some(color);
}
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(underlying_geom_id, geometry_styles, decoder)
{
return Some(color);
}
}
}
}
None
}
pub(crate) fn extract_color_pair_from_styles(
styles_attr: &ifc_lite_core::AttributeValue,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<([f32; 4], Option<[f32; 4]>)> {
if let Some(list) = styles_attr.as_list() {
for item in list {
if let Some(style_id) = item.as_entity_ref() {
if let Some(pair) = extract_color_from_style_assignment(style_id, decoder) {
return Some(pair);
}
}
}
} else if let Some(style_id) = styles_attr.as_entity_ref() {
return extract_color_from_style_assignment(style_id, decoder);
}
None
}
pub(crate) fn extract_color_from_styles(
styles_attr: &ifc_lite_core::AttributeValue,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<[f32; 4]> {
extract_color_pair_from_styles(styles_attr, decoder).map(|(c, _)| c)
}
fn extract_color_from_style_assignment(
style_id: u32,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<([f32; 4], Option<[f32; 4]>)> {
use ifc_lite_core::IfcType;
let style = decoder.decode_by_id(style_id).ok()?;
match style.ifc_type {
IfcType::IfcPresentationStyle => {
let styles_attr = style.get(0)?;
if let Some(list) = styles_attr.as_list() {
for item in list {
if let Some(inner_id) = item.as_entity_ref() {
if let Some(pair) = ifc_lite_processing::style::extract_surface_style_colors(
inner_id, decoder,
) {
return Some(pair);
}
}
}
}
}
IfcType::IfcSurfaceStyle => {
return ifc_lite_processing::style::extract_surface_style_colors(style_id, decoder);
}
_ => {
let styles_attr = style.get(0)?;
if let Some(list) = styles_attr.as_list() {
for item in list {
if let Some(inner_id) = item.as_entity_ref() {
if let Some(pair) = ifc_lite_processing::style::extract_surface_style_colors(
inner_id, decoder,
) {
return Some(pair);
}
}
}
}
}
}
None
}
pub(crate) struct PrePassData {
pub resolved: ifc_lite_processing::prepass::ResolvedPrepass,
pub project_id: Option<u32>,
pub site_position: Option<(u32, usize, usize)>,
pub simple_jobs: Vec<(u32, usize, usize, ifc_lite_core::IfcType)>,
pub complex_jobs: Vec<(u32, usize, usize, ifc_lite_core::IfcType)>,
}
pub(crate) fn combined_pre_pass(
content: &[u8],
decoder: &mut ifc_lite_core::EntityDecoder,
) -> PrePassData {
use ifc_lite_core::EntityScanner;
use ifc_lite_processing::prepass::{resolve_prepass, PrepassSpans, ResolveOptions};
let estimated_elements = content.len() / 2000;
let mut spans = PrepassSpans::default();
let mut project_id: Option<u32> = None;
let mut site_position: Option<(u32, usize, usize)> = None;
let mut simple_jobs = Vec::with_capacity(estimated_elements / 2);
let mut complex_jobs = Vec::with_capacity(estimated_elements / 2);
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
match type_name {
"IFCSTYLEDITEM" => spans.styled_items.push((id, start, end)),
"IFCINDEXEDCOLOURMAP" => spans.indexed_colour_maps.push((id, start, end)),
"IFCMATERIALDEFINITIONREPRESENTATION" => {
spans.material_def_reprs.push((id, start, end))
}
"IFCRELASSOCIATESMATERIAL" => spans.rel_associates_material.push((id, start, end)),
"IFCRELVOIDSELEMENT" => spans.void_rels.push((id, start, end)),
"IFCRELFILLSELEMENT" => spans.fills_rels.push((id, start, end)),
"IFCRELAGGREGATES" => spans.aggregate_rels.push((id, start, end)),
"IFCPROJECT" => {
if project_id.is_none() {
project_id = Some(id);
}
}
"IFCSITE" => {
if site_position.is_none() {
site_position = Some((id, start, end));
}
let ifc_type = ifc_lite_core::IfcType::from_str(type_name);
complex_jobs.push((id, start, end, ifc_type));
}
_ => {
if ifc_lite_core::has_geometry_by_name(type_name) {
let ifc_type = ifc_lite_core::IfcType::from_str(type_name);
if ifc_lite_core::is_simple_geometry_type(type_name) {
simple_jobs.push((id, start, end, ifc_type));
} else {
complex_jobs.push((id, start, end, ifc_type));
}
}
}
}
}
let resolved = resolve_prepass(
&spans,
decoder,
ResolveOptions {
collect_indexed_colour_full: false,
defer_attached_styles: false,
},
);
complex_jobs.extend(collect_type_geometry_jobs(content, decoder));
PrePassData {
resolved,
project_id,
site_position,
simple_jobs,
complex_jobs,
}
}
pub(crate) fn collect_type_geometry_jobs(
content: &[u8],
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Vec<(u32, usize, usize, ifc_lite_core::IfcType)> {
use ifc_lite_core::{EntityScanner, IfcType};
if !content
.windows(b"IFCREPRESENTATIONMAP".len())
.any(|window| window == b"IFCREPRESENTATIONMAP")
{
return Vec::new();
}
let mut referenced: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
let mut candidates: Vec<(u32, usize, usize, IfcType, Vec<u32>)> = Vec::new();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCMAPPEDITEM" {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let Some(source_id) = entity.get_ref(0) {
referenced.insert(source_id);
}
}
} else if type_name.ends_with("TYPE") || type_name.ends_with("STYLE") {
let ifc_type = IfcType::from_str(type_name);
if !ifc_type.is_subtype_of(IfcType::IfcTypeProduct) {
continue;
}
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
let rep_maps: Vec<u32> = entity
.get(6)
.and_then(|a| a.as_list())
.map(|list| list.iter().filter_map(|v| v.as_entity_ref()).collect())
.unwrap_or_default();
if !rep_maps.is_empty() {
candidates.push((id, start, end, ifc_type, rep_maps));
}
}
}
}
candidates
.into_iter()
.filter(|(_, _, _, _, maps)| maps.iter().any(|rm| !referenced.contains(rm)))
.map(|(id, start, end, ifc_type, _)| (id, start, end, ifc_type))
.collect()
}
pub(crate) fn build_referenced_representation_maps(
content: &[u8],
decoder: &mut ifc_lite_core::EntityDecoder,
) -> rustc_hash::FxHashSet<u32> {
use ifc_lite_core::EntityScanner;
let mut referenced: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCMAPPEDITEM" {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let Some(source_id) = entity.get_ref(0) {
referenced.insert(source_id);
}
}
}
}
referenced
}
pub(crate) fn build_instantiated_type_ids(
content: &[u8],
decoder: &mut ifc_lite_core::EntityDecoder,
) -> rustc_hash::FxHashSet<u32> {
use ifc_lite_core::EntityScanner;
let mut instantiated: rustc_hash::FxHashSet<u32> = rustc_hash::FxHashSet::default();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCRELDEFINESBYTYPE" {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let Some(type_id) = entity.get_ref(5) {
instantiated.insert(type_id);
}
}
}
}
instantiated
}
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
}
pub(crate) fn extract_building_rotation_from_site(
site_pos: (u32, usize, usize),
router: &ifc_lite_geometry::GeometryRouter,
decoder: &mut ifc_lite_core::EntityDecoder,
) -> Option<f64> {
let (site_id, start, end) = site_pos;
let site_entity = decoder.decode_at_with_id(site_id, start, end).ok()?;
let matrix = router
.resolve_scaled_placement(&site_entity, decoder)
.ok()?;
ifc_lite_geometry::rotation_angle_about_z(&matrix)
}
#[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);
}
}